Non-Profit, International

Spirit unsterblich.

C++ 右值引用和完美转发

字数统计:1658 blog

C++11 开始增加了移动语义和右值引用,这使得函数的重载变得更加复杂:你可能需要单独为右值参数设计一个函数,而这个函数明显在功能和实现上与左值参数是一样的,那么就需要有一个东西去统一函数参数的左值和右值,于是完美转发被设计出来。

std::forward

上一篇文章 C++ std::move 中提到过 std::remove_referencestatic_cast 用于实现 std::move,而 std::forward 也是用这两个组件实现的:


template <typename T>
constexpr T&& forward(std::remove_reference_t<T> &t) noexcept
{
    return static_cast<T&&>(t);
}

template <typename T>
constexpr T&& forward(std::remove_reference_t<T> &&t) noexcept
{
    return static_cast<T&&>(t);
}

std::forward 会将输入的参数原封不动地传递到下一个函数中,如果输入的参数是左值,那么传递给下一个函数的参数的也是左值;如果输入的参数是右值,那么传递给下一个函数的参数的也是右值。

引用折叠

引用折叠是 C++ 为了实现完美转发的语法,由于 C++ 无论在应用上还是语义上都不需要对引用的引用,所以 C++ 选择将引用的引用转化成直接的引用,具体规则如下:

  • T&& && -> T&&
  • T& & -> T&
  • T& && -> T&
  • T&& & -> T&

引用折叠用于模板的参数类型推导,auto 和 decltype。

万能引用

所谓万能引用,实际上是引用折叠的一个部分:

  • T&& && -> T&&
  • T&& & -> T&

换句话说,用右值引用作为参数声明,实参可为左值引用和右值引用,而用左值引用作为参数声明,实参只能为左值引用。

右值引用

右值引用只能绑定到右值上,左值除了可以绑定到左值上,在某些条件下还可以绑定到右值上。这里某些条件绑定右值为:常量左值引用绑定到右值,非常量左值引用不可绑定到右值。


std::string f()
{
    return string("abc");
}

void g()
{
    const std::string &s = f(); // still legal?
    std::cout << s << std::endl;
}

g 是合法的,原因是 s 是个左值,类型是常量引用,而 f() 返回右值,前面提到常量左值引用可以绑定到右值。

可以用下面的例子来说明引用折叠:


template<typename T>
void f(T&& param);

int a;
f(a);   // 传入左值,那么上述的 T&& 是左值引用
f(1);   // 传入右值,那么上述的 T&& 是右值引用

右值引用最常见的特性是延长临时对象的生命周期:


class A
{
public:
    int a = 0;
};

template <class T1, class T2>
void print_is_same()
{
    std::cout << std::is_same<T1, T2>() << std::endl;
}

int main()
{
    A a{}; // 这里使用了复制构造和默认构造,C++17 开始会直接被优化为默认构造(使用了返回值优化)
    A b = A();
    A &&c = A(); // 这里使用了右值引用,b 接管了临时对象,右值引用是左值
    A &d = a;
    print_is_same<A, decltype(a)>();
    print_is_same<A, decltype(b)>();
    print_is_same<A, decltype(c)>();
    print_is_same<A &&, decltype(c)>();
    std::cout << (typeid(a) == typeid(A)) << std::endl;
    std::cout << (typeid(A) == typeid(b)) << std::endl;
    std::cout << (typeid(A) == typeid(c)) << std::endl;
    std::cout << (typeid(A) == typeid(d)) << std::endl;
}

虽然 c 是右值引用,但是在使用的过程中,还是如同普通的左值引用。

尤其需要注意的是,不要对临时对象使用 std::move,因为 std::move 不更改临时对象的生命周期,因此临时对象会在当前语句执行完成时销毁,导致这个引用变为悬垂引用。

完美转发

完美转发是指在一个函数接收右值时,可以将这个右值继续传递给下一个函数。

右值引用是左值 ,所以当右值进入函数后,右值变为具名对象,即左值,此时再次传递这个对象,传递的是左值。

而完美转发是指让右值可以不断地以右值的身份传递下去:


#include <iostream>

void F(int x)
{
    std::cout << "右值" << std::endl;
}
void F(int &&x)
{

    std::cout << "左值" << std::endl;
}

template <class A>
void G(A &&a)
{
    return F(std::forward<A>(a)); //1
    //return F(a); //2
}

int main()
{
    int i = 2;
    G(i); // 正确
    G(5); // 错误
}

这段代码实际上无法编译,但是证实了完美转发的存在,并且包含了右值引用,引用折叠。

分析上面的代码,G(i) 将一个左值传递给了 Ga 为左值引用,并且通过 std::forward 传递给了 void F(int x),中间经历了引用折叠 T&& & -> T&

G(5) 将一个右值传递给了 G5 是右值,通过 std::forward 传递后还是右值,由于 5 既是右值,又是 int,所以编译器无法判断使用 F 的哪个重载版本,于是编译错误。

如果 G 不使用 std::forward,那么无论给 G 传入什么值,都会变为具名 a,即左值,在下一次传递时永远会传递给 void F(int x),因为右值引用永远只能传入右值或者常量。

注意,引用折叠只发生在模板的参数类型推导,autodecltype,所以函数 F 不存在引用折叠,而 G 是函数模板,所以存在引用折叠,也可以做到万能引用。

参考:

若无特殊声明,本人原创文章以 CC BY-SA 3.0 许可协议 提供。