c++ 11 左值,右值,std::move,std::forward

最近在看c++ 11标准中的左值,右值相关的知识,完美传递,通用引用。将通过实例代码,来学习其中的规则。水平有限,欢迎探讨。

环境介绍

Windows 10,vs2017 ;ubuntu18.04.1(WSL); g++ 7.4.0
编译器对标准的支持

概念

手册地址
右值是在c++11标准才提出来的,原文

& attr(optional) declarator (1)
&& attr(optional) declarator (2) (since C++11)

  1. Lvalue reference declarator: the declaration S& D; declares D as an lvalue reference to the type determined by decl-specifier-seq S.
  2. Rvalue reference declarator: the declaration S&& D; declares D as an rvalue reference to the type determined by decl-specifier-seq S.
    右值引用可用于为临时对象延长生存期(注意,左值引用亦能延长临时对象生存期,但不能通过左值引用修改它们)

实例代码:

#include <iostream>

using namespace std;

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

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

template<typename T>
void TestForward(T && v) {
	print(v);
	print(std::forward<T>(v));
	print(std::move(v));
}
void test_rvalue() {
	std::string s1 = "s1";
	std::string s2 = "s2";
	std::string &&s3 = s1 + s2;// s1和s2累加之后产生的临时对象将可以被s3格式变量接受

	// std::string &&s4 = s1; // 不被允许的语法,不能直接将左值赋值给右值

	print(s1);//lvalue
	print(s2);//lvalue
	print(s3);//lvalue
	print(s1 + s2);//rvalue
}
int main() {
    test_rvalue() ;
	TestForward(11);// void TestForward<int>(int &&v)
	int x = 11;
	TestForward(x);// void TestForward<int &>(int &v)
	TestForward(std::forward<int>(x));// void TestForward<int>(int &&v)
	return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 2.8)
project(modern_cpp)
file(GLOB_RECURSE SRC_CODE src *.cpp *.h *.hpp )
set(CMAKE_CXX_STANDARD 14)
add_executable(test_smart_ptr ${SRC_CODE})

reference collapsing(引用坍塌,引用折叠)

这段代码里面在void TestForward函数的参数表,使用了通用引用方式参数。

通用引用(universal reference)是Scott Meyers在C++ and Beyond 2012演讲中自创的一个词,用来特指一种引用的类型。参考地址

里面存在多个&&,在函数推导的时候,将会造成引用折叠(坍缩)。之后将会获得一个简化的版本的函数出来。在进入TestForward函数中,将会使用到T,可能就不是预想的那个类型了。所以看懂这段函数需要动一些脑筋。
TestForward(std::forward(x));
这个语句将将x转换成一个int &&的右值对象传到TestForward中,&& &&经过引用折叠之后,将会还保持&&,也就是保持了右值引用。
TestForward(x);
将会把x以int &方式带入,& &&引用折叠之后,将会得到&

参考资料

Concise explanation of reference collapsing rules requested:
(1) A& & -> A& , (2) A& && -> A& , (3) A&& & -> A& , and (4) A&& && -> A&&
可以这样记住,就是只要参与其中的,有单个&,一定就是&,只有将两个&& &&,才会产生&&右值引用。

右值

void test_rvalue() {
	std::string s1 = "s1";
	std::string s2 = "s2";
	std::string &&s3 = s1 + s2;// s1和s2累加之后产生的临时对象将可以被s3格式变量接受

	// std::string &&s4 = s1; // 不被允许的语法,不能直接将左值赋值给右值

	print(s1);//lvalue
	print(s2);//lvalue
	print(s3);//lvalue
	print(s1 + s2);//rvalue
}

s3最后还是被推导成左值,右值是一种无法取到地址的,临时的

移动

源代码中
TestForward(11);
TestForward(x);
TestForward(std::forward(x));
TestForward(std::forward<int&>(x));
在执行过程中,将全部输出:
rvalue
也就是说,std::move,将会强行将变量引用转换成右值引用。
如果编写代码不注意,将会造成很多右值被赋值给左值,并且通过左值再去做传递,这样的情况下,将会造成很多不必要的对象深度拷贝。

#include <iostream>
#include <list>

using namespace std;

struct A 
{
	A(int _age):age(_age) { cout << "A::A(int _age)\n"; }
	A() { cout << "A::A()\n"; }
	~A() { cout << "A::~A()\n"; }
	int age{ 0 };
};

void test_move(std::list<A> &ret_lst) {
	std::list<A> tmp_lst;
	for (int i = 0; i< 100;i++)
	{
		tmp_lst.emplace_back(i);
	}
	cout << "emplace_back finish\n";
	ret_lst = std::move(tmp_lst);
	cout << "ret_lst size: " << ret_lst.size() << "\n";
	cout << "tmp_lst size: " << tmp_lst.size() << "\n";
}

int main() {
	std::list<A> ret_lst;
	test_move(ret_lst);
	return 0;
}

代码中将tmp_lst指针拿出来,并且将,比那种深度拷贝要速度快很多。可以理解成,将一个左值转换成右值。而且会清空掉之前左值里面的内容(身体被掏空)。

转发引用

源代码中
TestForward(11); // rvalue
TestForward(x); // lvalue
TestForward(std::forward(x)); //
TestForward(std::forward<int&>(x));
在执行过程中,将全部输出:
rvalue
概念
此示例演示参数到类 T 构造函数实参的完美转发。并展示参数包的完美转发。

#include <iostream>
#include <memory>
#include <utility>
 
struct A {
    A(int&& n) { std::cout << "rvalue overload, n=" << n << "\n"; }
    A(int& n)  { std::cout << "lvalue overload, n=" << n << "\n"; }
};
 
class B {
public:
    template<class T1, class T2, class T3>
    B(T1&& t1, T2&& t2, T3&& t3) :
        a1_{std::forward<T1>(t1)},
        a2_{std::forward<T2>(t2)},
        a3_{std::forward<T3>(t3)}
    {
    }
 
private:
    A a1_, a2_, a3_;
};
 
template<class T, class U>
std::unique_ptr<T> make_unique1(U&& u)
{
    return std::unique_ptr<T>(new T(std::forward<U>(u)));
}
 
template<class T, class... U>
std::unique_ptr<T> make_unique2(U&&... u)
{
    return std::unique_ptr<T>(new T(std::forward<U>(u)...));
}
 
int main()
{   
    auto p1 = make_unique1<A>(2); // 右值
    int i = 1;
    auto p2 = make_unique1<A>(i); // 左值
 
    std::cout << "B\n";
    auto t = make_unique2<B>(2, i, 3);
}

完美转发其实是建立在reference collapsing之上的,每次std::forward将会按照变量输出值,做推论(是左值还是右值),而不会如同std::move,粗暴将类型直接强转成右值。然后通过T&&的reference collapsing,就能完美的将数据全部都通过模板函数全部都初始化到对象中。p1,p2这一级的初始化,会按照左值或者右值来处理,而最后make_unique2的时候,也在B的构造器,按照之前的左值、右值将按照数据完美传递到B对象中的A成员变量。

发布了76 篇原创文章 · 获赞 13 · 访问量 13万+

猜你喜欢

转载自blog.csdn.net/erlang_hell/article/details/102981336