【C++基本语法】面试高频之:左值右值将亡值,std::move,std::forward示例和详解

【C++基本语法】系列用来记录C++常用基本语法,惯用法。
本文主要回忆左值,右值,move, forward等一组概念,我的理解还比较浅,这里结合几篇文章做个总结。

1. 左值和右值

关于左右值的概念众说纷纭,是个好上手但不好说明的概念。这里列举一些见过的说法(不一定对),以供参考:

左值:在等号左边可以被赋值,在内存中必须存在实体;表达式结束后仍然存在的持久对象,使用的是它的地址
右值:在等号右边,可以在内存中也可以在CPU寄存器;表达式结束后就不再存在的临时对象,不可以取地址,没有名字
对于右值不可以取地址这个说法
有意思的是这篇文章C++ / 左值、右值、将亡值 | 谈谈我对它们的深入理解,提供了这样一个视角:认为区分左右值的依据应该是是否能在语言层面上修改数据,能修改的数据就是左值,不能修改的数据就是右值

同样是基于这篇文章,我们看看下面这几个概念。

2. 左值引用和右值引用以及将亡值

引用是别名,不是变量的别名,而是地址的别名,是与地址建立的一种映射关系
引用基本规则:

  • 声明引用的时候必须初始化,绑定后不可再绑定到其他对象;即引用必须初始化,不能对引用重定义
  • 对引用的一切操作,就相当于对原对象的操作

左值引用:type &引用名 = 左值表达式
右值引用:type &&引用名 = 右值表达式

2.1 引用和指针

在高级语言的层面上,引用是对地址的一种封装,而指针呢?指针没有封装地址,直接保存了地址,将地址暴露了出来。

2.2 为什么要有引用?

这篇文章认为,语言的设计者鼓励我们多使用引用,而少使用或者不使用指针,就是为了减少使用者对底层结构(地址)的直接接触,将使用者与地址解耦,减少直接接触地址可能存在的风险。

2.3 右值引用是右值吗?对于它的理解错误会陷入常见的陷阱

根据改文章的结论,右值引用不是右值,是左值。通过下面的代码可以发现:用右值引用作为实参不能调用形参为右值引用的函数

template<typename T>
void print(T & t)
{
    cout<<"lvaule: "<< t <<endl;
}

template<typename T>
void print(T&& t)
{
    cout<<"rvalue: "<< t <<endl;
}
void test_rval_ref()
{
    int a = 10;
    int&& x = 1;
    print(a);
    print(x); //这里本意是调用print(T&&), 但调的是print(T&)
    print(std::move(x)); // 需要通过move才能进入print(T&&)
}
int main()
{
    test_rval_ref();
    return 0;
}

结果

lvaule: 10
lvaule: 1
rvalue: 1

所以我们在工程中,定义了这样的函数func(T&& t),即使它的实参是右值引用,也需要通过std::move做一次类型转换才调用到该函数。我理解是,由于右值引用作为参数传递时,由于实参中有了命名(有左值承接),所以继续往下传或者调用其他函数时,根据C++ 标准的定义,这个参数变成了一个左值。那么他永远不会调用接下来函数的右值版本。
上述文章中进一步描述了int &&x = 1,编译器的工作是:先在可写数据区创建只读数据(1)的一份拷贝,再把这份拷贝的地址给引用x,让引用封装这个地址,所以x引用的并不是只读数据 1 的真正地址,而是它的一份拷贝。感兴趣的可以再深入研究。

2.4 将亡值

一般认为将亡值是右值。常见的将亡值有函数的临时返回值,比如auto x = foo(3);中的foo(3);还有表达式,比如(x+y)。
C++11提出了右值引用,将亡值这一概念才会被提出,两者的关系非常紧密,将亡值是右值,右值引用可以引用右值,当然也能引用将亡值,但是引用后的右值引用却是一个左值,我们可以通过右值引用修改将亡值的数据。虽然将亡值被释放了,但是它的资源却给了右值引用,一般在构造函数中,我们将右值引用得到的将亡值资源转移到我们自己对象上。这里提到移动构造的写法,正确的写出这各种构造和赋值也不是容易的。

3. 引用折叠

听起来复杂,相对好理解,需要自己写个例子试试。摘抄网上的描述:

  • 所有右值引用折叠到右值引用上仍然是一个右值引用。(A&& && 变成 A&&)
  • 所有的其他引用类型之间的折叠都将变成左值引用。 (A& & 变成 A&; A& && 变成 A&; A&& & 变成 A&)

3. std:: move

用的很多但不好说清楚的接口,一般都说其等价于static_cast<T&&>(lvalue),它是将一个左值强制转化为右值引用,继而可以通过右值引用使用该值。从下面的文件可以找到其定义(Ubuntu): /usr/include/c++/8/bits/move.h

 /**
   *  @brief  Convert a value to an rvalue.
   *  @param  __t  A thing of arbitrary type.
   *  @return The parameter cast to an rvalue-reference to allow moving it.
  */
  template<typename _Tp>
    constexpr typename std::remove_reference<_Tp>::type&&
    move(_Tp&& __t) noexcept
    { return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }
  • std::remove_reference依靠模板特化的方式分离出参数的基本类型,比如int& 和int&&都能提取出int类型来。
  • move的参数类型是T&&, 加上引用折叠,可以匹配任何类型实参
    所以,某种意义上来说,std::move(lvalue) 就约等于 static_cast<T&&>(lvalue),(但其实这是std::forward的实现)即将左值强制转换为右值。而 std::move 中封装了一个类型提取器 std::remove_reference。(std::remove_reference<_Tp>::type的意思是:对于类型_Tp,移除其引用修饰符后得到的类型。如果_Tp不是一个引用类型,std::remove_reference<_Tp>::type和_Tp是相同的。如果_Tp是一个引用类型,比如int&,那么std::remove_reference<_Tp>::type就是int。
    这是C++11标准引入的类型特征(type traits)的一部分,用于在编译时获取类型的信息。

4. std::forward

一个右值引用参数作为函数的形参,在函数内部再转发该参数的时候它已经变成一个左值,并不是他原来的类型。如果我们需要一种方法能够按照参数原来的类型转发到另一个函数,这种转发类型称为完美转发。

/**
   *  @brief  Forward an lvalue.
   *  @return The parameter cast to the specified type.
   *
   *  This function is used to implement "perfect forwarding".
   */
  template<typename _Tp>
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type& __t) noexcept
    { return static_cast<_Tp&&>(__t); }

4.1 std::forward 示例1- 一个可以调用一切接口的函数

自己写测试代码的时候可能会用的,模板函数,可以调用任何类型的接口。

// 实现一个可以调用一切接口的函数
template <class T, class ... Args>
auto execAnyfunc(T&& t, Args ... args){
    cout<<"execute function: "<<__func__<<endl;
    auto func = std::bind(std::forward<T>(t), std::forward<Args>(args)...);
    func();
}

4.2 std::forward 示例2 - threadpool Enqueue函数

下面是常见的一种线程池enqueue函数实现,里面用forward构造仿函数。
在这里std::forward的作用是确保参数传递给std::bind时,这些参数的左值或右值性质与原始调用者传递给enqueue函数的性质相同。这对于保持效率和正确性尤其重要,特别是当处理那些对左值和右值有特殊操作或优化的类型时。
如果不使用std::forward,所有的参数都会被当作左值处理,即使它们原本是可以被移动的右值。这可能会导致不必要的拷贝,降低效率,特别是对于大型对象或者是只能移动(不能拷贝)的类型,如std::unique_ptr,这样做可能还会导致编译错误。

template<typename F, typename... Args>
        auto enqueue(F &&f, Args &&...args) {
            using return_type = std::invoke_result_t<F, Args...>;
            auto task = std::make_shared<std::packaged_task<return_type()>>(
                    std::bind(std::forward<F>(f), std::forward<Args>(args)...));// 完美转发,构造任务仿函数的指针
            std::future<return_type> res = task->get_future();                  // 获得函数执行的future返回
            {
                std::unique_lock<std::mutex> lock(queue_mutex);if (stop) {
                    throw std::runtime_error("enqueue on stopped Thread pool");
                }
​
                tasks.emplace([task = std::move(task)]() { (*task)(); });// 塞入任务队列
            }                                                            // 入队列后即可解锁
            condition.notify_one();                                      // 仅唤醒一个线程,避免无意义的竞争
            return res;
        }

题外话

这几个知识点是面试中最经常问的,但是其实大多数人都是会用,但说不出来。即使总结了这么多,还是感觉一知半解。

参考链接

https://blog.csdn.net/p942005405/article/details/84644069
https://blog.csdn.net/qq_45698148/article/details/120680378
https://blog.csdn.net/p942005405/article/details/84644101
https://zhuanlan.zhihu.com/p/620583555

  • 14
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
std::move是C++11中的一个标准库函数,用于将一个左值强制转换为右值引用。通过使用std::move,可以告诉编译器一个对象可以被移动而不是复制,从而提高程序的性能。std::move是一个类型转换函数,它不会真正移动数据,只是将左值转换成右值引用。 std::forward也是C++11中的一个标准库函数,用于完美转发参数。当我们希望将一个函数的参数传递给另一个函数时,我们可以使用std::forward来保持参数的左右值属性。std::forward根据传入的参数类型来决定是将参数作为左值引用还是右值引用进行传递。 左右值引用是C++11中引入的一个新的引用类型。左值引用指向一个具名的对象,而右值引用则可以绑定到一个临时对象或将要销毁的对象。左右值引用的一个重要应用是移动语义,通过将资源所有权从一个对象转移到另一个对象,避免了昂贵的资源拷贝操作。 移动构造函数是一种特殊的构造函数,用于在对象的移动操作中进行资源移动而不是拷贝。在C++11中,当一个对象被移动时,编译器会首先尝试调用其移动构造函数。移动构造函数需要一个右值引用作为参数,并将其它对象的资源移动到当前对象中,然后将原来的对象置为有效的但未知的状态。 综上所述,C++11中的std::move和std::forward以及左右值引用与移动构造函数都是为了实现移动语义而引入的新特性。它们可以提高程序的性能,避免不必要的资源拷贝,以及实现更高效的对象移动操作。但是在使用时需要注意正确的使用方式和避免潜在的问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值