C++左值引用与右值引用

本文翻译自:https://docs.microsoft.com/en-us/cpp/cpp/references-cpp?view=vs-2019,并参考《深入理解C++11》。

  引用,类似于指针,用于存储一个位于内存某处的对象的地址。与指针不同的是,引用在被初始化后不能再指向另一个对象,或设置为null。引用分为两种:左值引用,右值引用,其中左值引用指向一个命名的变量,右值引用指向一个临时对象(temporary object)。操作符&表示左值引用,而&&根据其上下文的不同可表示右值引用或a universal reference。

  注:universal reference:https://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers

  在C++11中,右值引用指的是对一个右值进行引用的类型,为了区别于C++98中的引用类型,称C++98中的引用为“左值引用(lvalue reference)”。右值引用和左值引用都是引用类型,引用类型必须在声明的时候立即初始化,其原因可以理解为引用类型本身并不拥有所绑定的对象的内存,只是该对象的一个别名。左值引用时具名变量值得别名,而右值引用是不具名(匿名)变量的别名。

左值引用

作用:持有一个对象的地址,但是其行为类似于一个对象  

格式:type-id & cast-expression

  我们可以将左值引用看为一个对象的别名。左值引用的声明包含一个可选的说明符列表,后面跟一个引用声明符。左值引用必须被初始化,且不能再指向另外一个对象或设置为null。

  任何可以将其地址转换为一个指定类型指针的对象也可将其地址转换为一个类似的引用对象。例如,任何可以转换为char*的对象的地址也可以转换为char &。

  注意不要将引用声明符(&)与地址操作符(取对象的地址,也是&)混淆。当&前面是一个类型时,例如int或char,则该&是一个引用声明符。如果&前面没有任何类型,则该&用于取一个对象的地址。

  • 例子:

   The following example demonstrates the reference declarator by declaring a Person object and a reference to that object. Because rFriend is a reference to myFriend, updating either variable changes the same object.

// reference_declarator.cpp
// compile with: /EHsc
// Demonstrates the reference declarator.
#include <iostream>
using namespace std;

struct Person
{
    char* Name;
    short Age;
};

int main()
{
   // Declare a Person object.
   Person myFriend;

   // Declare a reference to the Person object.
   Person& rFriend = myFriend;

   // Set the fields of the Person object.
   // Updating either variable changes the same object.
   myFriend.Name = "Bill";
   rFriend.Age = 40;

   // Print the fields of the Person object to the console.
   cout << rFriend.Name << " is " << myFriend.Age << endl;
}

  输出:

Bill is 40

右值引用

  右值引用指向临时对象。在C++11之前,可以通过一个左值引用指向一个临时对象,但是这个左值引用必须是const:

string getName()
{
    return “Alex”;
}
const string& name = getName();

       这也表明,临时对象并不是立即被销毁(destructed),这由C++保证,但它仍然是一个临时对象,你不能修改它的值。

       在C++11中,引入了右值引入,可以通过一个可变引用指向rvalue,但是不能绑定到lvalue,因此右值引用可以检测一个值是否是临时对象。

       类似于左值引用使用&,右值引用使用&&,可以是const/non-const

 

  持有一个指向右值表达式的引用。

  •   格式:type-id && cast-expression

  右值引用可以用于区分一个表达式是左值还是右值。左值引用与右值引用在句法上语法上类似,但是遵从不同的规则。下面章节用于描述右值引用是如何支持移动语义(move semantics)和完美转发(perfect forwarding)的实现的。

左值引用绑定到右值

  通常情况下右值引用不能绑定到任何的左值,例如下面代码无法通过编译:

int c;
int && d = c;

  但是,C++98标准中就出现的左值引用是否可以绑定到右值(由右值进行初始化)?

T& e = ReturnRvalue();
const T& f = ReturnRvalue();

  上述代码中,e的初始化会导致编译错误,而f则可以通过编译。C++98中的左值引用可以接受:非常量左值、常量左值。右值对其进行初始化。而使用右值对其进行初始化时,常量左值引用还可以像右值引用一样延长右值的声明周期,但是必须是const。

  看如下代码有何区别?

const bool& judgement = true;
const bool judgement = true;

  第一个使用常量左值引用来绑定右值,该语句执行后右值true并没有被销毁,而第二个表达式在使用右值true构造judgement后,右值true就会被销毁了。

 

判断类型

<type_traits>头文件提供了3个模板类:

  • is_rvalue_reference
  • is_lvalue_reference
  • is_reference

<type_traits> This header defines a series of classes to obtain type information on compile-time.该头文件定义了一系列的类,用于获取编译期对象的类型信息。

 

Move Semantics移动语义

  移动语义(move semantics)的实现依赖于右值引用,move可以明显地提升应用的性能。移动语义让你可以通过代码将资源(例如动态分配的内存)从一个对象转移到另一个对象。移动语义可以工作的原理是:它可以将资源从临时对象(temporary objects)中转移到其他地方,这些临时变量在程序的其他地方不会被获取到(be referenced)。

  为了实现移动语义,你需要给自定义的类提供:移动构造函数(move constructor),另外可选地提供移动赋值构造符(move assignment operator, operator=)。(如果实现了这些函数),持有右值资源的对象的拷贝和赋值操作符会自动使用move semantics。与默认的拷贝构造函数不同的是,编译器并不会提供一个默认的移动构造函数。移动构造函数:https://docs.microsoft.com/en-us/cpp/cpp/move-constructors-and-move-assignment-operators-cpp?view=vs-2019

  你也可以重载普通函数和操作符来使用move semantics。Visual Studio 2010将move semantics引入到C++标准库中。例如:string类实现了使用move semantics的相关操作。例如:

// string_concatenation.cpp
// compile with: /EHsc
#include <iostream>
#include <string>
using namespace std;

int main()
{
   string s = string("h") + "e" + "ll" + "o";
   cout << s << endl;
}

  在Visual Studio 2010之前,string的每一个+操作符都会分配并返回一个新的临时的string对象(an rvalue)。+操作符并不能将一个string扩展到另一个string上,因为它不知道the source string是左值还是右值。如果+左右的两个字符串都是左值,它们可能在程序的其他地方被引用,因此不能修改。通过使用右值引用,可以修改+操作符用于右值,因为右值不会在程序的其他地方被引用。这就可以明显地降低string类必须的动态内存分配。

  当编译器不能使用Reture Value Optimization(RVO,返回值优化),或Named Return Value Optimization(NRVO)时,移动语义也能提升程序性能。在这种情况下,如果返回类型定义了move constructor的话编译器会调用它。

  为了更好地理解移动语义,考虑向vector对象中插入元素这样的一个例子。如果vector对象的容量超出了,vector对象为其存储的元素需要重定位内存,然后将每个元素拷贝到另一个内存地址,以为新插入的元素腾出空间。当插入操作拷贝一个元素时,他会创建一个新的元素,调用拷贝构造函数以将之前的数据拷贝到新元素中,然后再将之前的元素析构掉(destroy)。而移动语义让你可以直接地将元素进行移动,而不需要进行昂贵的内存重定位,和复制操作。为了使用move semantics,你可以写一个move构造函数,用于将数据从一个对象移动到另一个。

  在C++11中,标准库<utility>中提供了函数:std::move,该函数功能为:将一个左值强制转化为右值引用,继而我们可以通过右值引用使用该值,以用于移动语义。从实现上讲,std::move基本等同于一个类型转换:

static_cast<T&&>(lvalue);

   移动语义的一个典型的应用是实现高性能的置换(swap)函数。

template<class T>
void swap(T& a, T& b)
{
    T temp(move(a));
    a = move(b);
    b = move(temp);
}

  如果上述代码中的类型T是可以移动的(有移动构造函数,移动赋值函数),整个过程中代码都只会按照移动语义进行指针交换,不会有资源的释放与申请。如果T是不可移动的却是可拷贝的,则拷贝语义会被用来进行置换,这就与普通的置换语句相同了。

  注意:如果在移动构造函数中抛出异常是很危险的,因为会导致空悬的指针,因此最好给移动构造函数添加一个noexcept关键字,如果移动构造函数抛出异常则直接调用teminate函数终止程序,而不是造成指针空悬的状态。

  在<utility>头文件中定义了:move_if_noexcept,

 Move if noexcept: 

  Returns an rvalue reference to arg, unless copying is a better option than moving to provide at least a strong exception guarantee.该函数在类的移动构造函数没有noexcept关键字修饰时返回一个左值引用从而使变量可以使用拷贝语义,而在类的移动构造函数有noexcept关键字时,返回一个右值引用,从而使变量可以使用移动语义。

#include <utility>

#ifdef _NOEXCEPT
#define noexcept _NOEXCEPT
#endif // _NOEXCEPT

struct Maythrow
{
    Maythrow(){}
    Maythrow(const Maythrow&)
    {
        cout << "Maythrow copy constructor" << endl;
    }
    Maythrow(Maythrow&&)
    {
        cout << "Maythrow move constructor" << endl;
    }
};
struct Nothrow
{
    Nothrow(){}
    Nothrow(Nothrow&&) noexcept
    {
        cout << "Nothrow move constructor" << endl;
    }
        Nothrow(const Nothrow&)
    {
        cout << "Nothrow copy constructor" << endl;
    }
};

int main(int argc, char *argv[])
{
    Maythrow m;
    Nothrow n;

    Maythrow mt = move_if_noexcept(m);
    Nothrow nt = move_if_noexcept(n);
}

输出:

 

  move_if_noexcpet是一种牺牲性能以保证安全的做法,且要求开发者对移动构造函数使用noexcept进行描述,否则就会损失性能(使用拷贝构造函数,见上面例子)。

 注意:

  在gcc4.8.5中编译带noexcept的构造函数时,编译正常,但是在Windows环境下使用Visual Studio 2013编译,会报错:error C3646:"noexcept":未知重写说明符,网上查了下资料,需要在代码中添加预处理指令:然后就可以编译成功了,好像是VS2015才支持noexcpet,对于__func__也是到VS2015才完全支持的,见:https://blog.csdn.net/weixin_43956273/article/details/100169367

#ifdef _NOEXCEPT
#define noexcept _NOEXCEPT
#endif // _NOEXCEPT

 

Perfect Fowarding完美转发 

https://blog.csdn.net/u012198575/article/details/83142419

https://docs.microsoft.com/en-us/cpp/cpp/rvalue-reference-declarator-amp-amp?view=vs-2019

  C++11中的一项新技术,指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数,例如:

template<typename T>
void IamForwarding()
{
    IrunCodeActually(t);
}

  上述例子中,IamForwarding是一个转发函数模板,而函数IrunCodeActually是真正执行代码的目标函数。上述代码中只是转发(forwarding)参数,在IrunCodeActaully调用之前就已经进行了一次临时对象拷贝。如果要完美地转发参数,需要使用引用类型,但问题是:目标函数可能接受左值引用,也可能接受右值引用。

  C++11通过引入一条“引用折叠(reference cllapsing)”的语言规则,并结合新的模板推导规则来实现完美转发。

 

       Perfect forwarding用于降低重载函数的需求,并且当你编写一个参数为引用的泛型函数时,且该泛型函数将参数传递(或:forward)给其他函数时,有助于解决forwarding problem。例如:如果泛型函数的参数类型为const T&,那么对该函数的调用不能修改参数的值。如果泛型函数的参数类型为T&,那么不同使用rvalue(例如:临时对象或整数字面常数:temporary object or integer literal)对该函数进行调用。

       对于该类问题的通常解决方法是对该泛型函数进行重载,对于每个参数都重载T& 和const T&的版本。但是,如果函数参数较多的话重载版本会指数型增加。

       右值引用rvalue reference可以让你通过一个版本的函数来接收这两种函数,且可通过forward将这些参数传递给其他函数。

       例如:对于4中类型:W X Y Z,它们的构造函数分别使用不同的const non-const左值引用组合:

struct W
{
   W(int&, int&) {}
};

struct X
{
   X(const int&, int&) {}
};

struct Y
{
   Y(int&, const int&) {}
};

struct Z
{
   Z(const int&, const int&) {}
};

  假如你要编写一个泛型函数来生成这些对象,可以写成下面形式:

template <typename T, typename A1, typename A2>
T* factory(A1& a1, A2& a2)
{
   return new T(a1, a2);
}

  通过如下形式可以调用该泛型函数:

int a = 4, b = 5;
W* pw = factory<W>(a, b);

  但是通过如下形式调用会产生错误,因为参数是rvalue,而泛型函数接受的参数是可修改的左值引用(lvalue references that are modifiable

Z* pz = factory<Z>(2, 2);

  通常解决这类问题的方法是重载:每个参数都有一个A& ,const A&的变化。右值引用则可以让你只写一个版本的函数:

template <typename T, typename A1, typename A2>
T* factory(A1&& a1, A2&& a2)
{
   return new T(std::forward<A1>(a1), std::forward<A2>(a2));
}

这个版本的泛型函数使用右值引用rvalue reference作为factory函数的参数。std::forward函数的作用是forward the parameters of the factory to the constructor of the template class,是将factory函数的参数转发给模板类的构造函数。

 

该版本可以支持如下的调用方式:

int main()
{
   int a = 4, b = 5;
   W* pw = factory<W>(a, b);
   X* px = factory<X>(2, b);
   Y* py = factory<Y>(a, 2);
   Z* pz = factory<Z>(2, 2);

   delete pw;
   delete px;
   delete py;
   delete pz;
}

Additional Properties of Rvalue Reference右值引用的其他特性

   可以重载一个函数,让它分别接受左值引用和右值引用。

  通过重载一个函数,让它分别接受const左值引用和右值引用,你可以通过代码来判断一个表达式是non-modifiable objects(lvalues)还是modifiable tempoary values(rvalues)。只有当一个对象被标记为const,你才能将它传递给一个参数为右值引用的函数。The following example shows the function f, which is overloaded to take an lvalue reference and an rvalue reference. The main function calls f with both lvalues and an rvalue.

// reference-overload.cpp
// Compile with: /EHsc
#include <iostream>
using namespace std;

// A class that contains a memory resource.
class MemoryBlock
{
   // TODO: Add resources for the class here.
};

void f(const MemoryBlock&)
{
   cout << "In f(const MemoryBlock&). This version cannot modify the parameter." << endl;
}

void f(MemoryBlock&&)
{
   cout << "In f(MemoryBlock&&). This version can modify the parameter." << endl;
}

int main()
{
   MemoryBlock block;
   f(block);
   f(MemoryBlock());
}

该例子输出结果如下:

In f(const MemoryBlock&). This version cannot modify the parameter.
In f(MemoryBlock&&). This version can modify the parameter.

  在该例子中,第一个调用f函数传递了一个局部变量(右值)作为参数。第二个调用f函数传递了一个临时对象作为参数。由于临时对象不能在程序的其他地方引用,因此调用重载函数中参数为右值的版本,且该版本中过对于传入参数是可以修改的。

  注意:编译器将一个命名的右值引用视为左值进行处理,并将一个未命名的右值引用视为右值进行处理。The compiler treats a named rvalue reference as an lvalue and an unnamed rvalue reference as an rvalue.

  当你编写一个参数为右值引用的函数时,在函数体内该参数被视为是左值。编译器将命名的右值引用当做一个左值进行处理,这是因为一个明明的对象可以在程序的多处被引用;但是允许程序的多处来修改或删除资源时很危险的。例如:如果程序的多个位置都尝试从一个对象中转移资源,仅仅会有一个对象能成功地转移该资源(source)。

  下面例子定义了一个函数g,该函数重载了一个左值引用和右值引用作为参数。函数f的参数为左值引用(一个明明的右值引用),并返回一个右值引用(一个未命名的右值引用)。在f函数中调用g,overload resolution选择了参数为左值引用版本的函数g,因为f的函数体将其参数视为左值。在main函数中调用g,overload resolution选择了参数为优质引用版本的函数g,因为f函数返回的是一个右值引用。

// named-reference.cpp
// Compile with: /EHsc
#include <iostream>
using namespace std;

// A class that contains a memory resource.
class MemoryBlock
{
   // TODO: Add resources for the class here.
};

void g(const MemoryBlock&)
{
   cout << "In g(const MemoryBlock&)." << endl;
}

void g(MemoryBlock&&)
{
   cout << "In g(MemoryBlock&&)." << endl;
}

MemoryBlock&& f(MemoryBlock&& block)
{
   g(block);
   return move(block);
}

int main()
{
   g(f(MemoryBlock()));
}

上面例子的输出为:

In g(const MemoryBlock&).
In g(MemoryBlock&&).

  在该例子中,mian函数传递一个右值给f函数。f函数体将它的参数当做是左值进行处理,函数f调用g时调用的是g的左值引用的版本,因此第一个打印的是左值引用版本中的内容。

  •   你可以将一个左值转换为右值引用

   C++标准库函数std::move允许你将一个对象转换为该对象的右值引用。另外,你可以使用static_cast关键字将一个左值转换为右值引用,看下面的例子:

// cast-reference.cpp
// Compile with: /EHsc
#include <iostream>
using namespace std;

// A class that contains a memory resource.
class MemoryBlock
{
   // TODO: Add resources for the class here.
};

void g(const MemoryBlock&)
{
   cout << "In g(const MemoryBlock&)." << endl;
}

void g(MemoryBlock&&)
{
   cout << "In g(MemoryBlock&&)." << endl;
}

int main()
{
   MemoryBlock block;
   g(block);
   g(static_cast<MemoryBlock&&>(block));
}

输出:

In g(const MemoryBlock&).
In g(MemoryBlock&&).
  • 函数模板推断它们的模板参数类型,然后使用引用折叠规则。Function template deduce their template argument types and then use reference collapsing rules.

   编写函数模板然后将其参数再传递给其他函数的行为很常见。理解参数为右值引用的函数模板的模板类型推断(template type deduction)的工作原因很重要。

  如果函数参数是一个右值,编译器会推断该参数为一个右值引用。例如,如果你传递一个类型为x的右值引用给一个参数为T&&的模板函数,模板参数推断(template argument deduction)会将T推断为X。因此,参数类型为X&&。如果函数参数是一个左值或常量左值(const lvalue),则编译器会将类型推断为该类型的左值引用或const左值引用。

  下面例子中声明了一个结构体模板,然后将它实例化为各个类型。print_type_and_value函数的参数为右值引用,然后forward该参数给合适的实例化版本的S::print函数。main函数展示了调用S::print函数的多种方式。

// template-type-deduction.cpp
// Compile with: /EHsc
#include <iostream>
#include <string>
using namespace std;

template<typename T> struct S;

// The following structures specialize S by
// lvalue reference (T&), const lvalue reference (const T&),
// rvalue reference (T&&), and const rvalue reference (const T&&).
// Each structure provides a print method that prints the type of
// the structure and its parameter.

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

template<typename T> struct S<const T&> {
   static void print(const T& t)
   {
      cout << "print<const T&>: " << t << endl;
   }
};

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

template<typename T> struct S<const T&&> {
   static void print(const T&& t)
   {
      cout << "print<const T&&>: " << t << endl;
   }
};

// This function forwards its parameter to a specialized
// version of the S type.
template <typename T> void print_type_and_value(T&& t)
{
   S<T&&>::print(std::forward<T>(t));
}

// This function returns the constant string "fourth".
const string fourth() { return string("fourth"); }

int main()
{
   // The following call resolves to:
   // print_type_and_value<string&>(string& && t)
   // Which collapses to:
   // print_type_and_value<string&>(string& t)
   string s1("first");
   print_type_and_value(s1);

   // The following call resolves to:
   // print_type_and_value<const string&>(const string& && t)
   // Which collapses to:
   // print_type_and_value<const string&>(const string& t)
   const string s2("second");
   print_type_and_value(s2);

   // The following call resolves to:
   // print_type_and_value<string&&>(string&& t)
   print_type_and_value(string("third"));

   // The following call resolves to:
   // print_type_and_value<const string&&>(const string&& t)
   print_type_and_value(fourth());
}

上述例子输出为:

print<T&>: first
print<const T&>: second
print<T&&>: third
print<const T&&>: fourth

  为解析每次对函数print_type_and_value函数的调用,编译器首先进行模板变量推断。当编译器将推导的模板参数替换为参数类型时,编译器运用引用折叠规则(reference collapsing rule)。例如,将局部变量s1传递给print_type_and_value函数时,编译器会产生如下的函数签名:

print_type_and_value<string&>(string& && t)

  编译器使用引用折叠规则将签名减少到以下内容:

print_type_and_value<string&>(string& t)

  这个版本的print_type_and_value函数然后forward(转发)它的参数到正确版本的S::print函数。

  下面表格总结了模板变量类型推断的引用折叠规则:

 

Expanded type Collapsed type
T& & T&
T& && T&
T&& & T&
T&& && T&&

  

 

 

   模板变量类型推断是实现perfect forwarding(完美转发)的重要组成部分。

 总结

   右值引用可以用于区分左值,右值。右值引用可以通过消除不必要的内存分配和copy行为来提升你的程序的性能。右值引用还可以让你只写一个版本的函数,就可以接受不同的参数并forward转发该参数到其他函数,就像其他函数被直接调用一样。They also enable you to write one version of a function that accepts arbitrary arguments and forwards them to another function as if the other function had been called directly.

posted @ 2020-07-11 11:15  adfas  阅读(2218)  评论(0编辑  收藏  举报