问题
❤请回答什么叫左值引用,什么叫右值引用。
什么是将亡值,什么是纯右值。
❤移动语义与完美转发了解吗。
什么是引用折叠?forward函数的原理。
❤什么是移动构造和移动赋值?
右值引用
左值和右值
从最简单的字 面理解,无非是表达式等号左边的值为左值,而表达式右边的值为右 值,比如
int x = 1; int y = 3; int z = x + y;
有些 情况下是无法准确区分左值和右值的,比如
int a = 1; int b = a;
这里出现了矛盾,在第 一行代码中我们判断a是一个左值,它却在第二行变成了右值
在C++中所谓的左值一般是指一个指向特定内存的具有名称的值 (具名对象),它有一个相对稳定的内存地址,并且有一段较长的生 命周期。而右值则是不指向稳定内存地址的匿名值(不具名对象), 它的生命周期很短,通常是暂时性的。
基于这一特征,我们可以用取 地址符&来判断左值和右值,能取到内存地址的值为左值,否则为右 值。
还是以上面的代码为例,因为&a和&b都是符合语法规则的,所以 a和b都是左值,而&1在GCC中会给出“lvalue required as unary '&' operand”错误信息以提示程序员&运算符需要的是一个左值。
左值引用和右值引用
非常量左值的引用对象很单纯,它们必须是一个左值
常量左值引用的特性显得更加有趣,它除了能引用左值, 还能够引用右值
右值引用是一种引用右值且只能引用右值的方法
在左值引用声明中,需要在类 型后添加&,而右值引用则是在类型后添加&&,例如:
int i = 0;
int &j = i; // 左值引用
int &&k = 11; // 右值引用
右值引用的特点之一是可以延长右值的生命周期
# include <iostream>
class X {
public:
X() { std::cout << "X ctor" << std::endl; }
X(const X&x) { std::cout << "X copy ctor" << std::endl; }
~X() { std::cout << "X dtor" << std::endl; }
void show() { std::cout << "show X" << std::endl; }
};
X make_x()
{
X x1;
return x1;
}
int main()
{
X &&x2 = make_x();
x2.show();
}
下如果将X &&x2 = make_x()这句代码替换为X x2 = make_x()会发生几次构造。
在 没有进行任何优化的情况下应该是3次构造,首先make_x函数中x1会 默认构造一次,然后return x1会使用复制构造产生临时对象,接着 X x2 = make_x()会使用复制构造将临时对象复制到x2,最后临时 对象被销毁
X ctor
X copy ctor
X dtor
show X
X dtor
/*
以上流程在使用了右值引用以后发生了微妙的变化,让我们编译
运行这段代码。请注意,用GCC编译以上代码需要加上命令行参数
fno-elide-constructors用于关闭函数返回值优化(RVO)。因
为GCC的RVO优化会减少复制构造函数的调用,不利于语言特性实验:
*/
只发生了两次构造。第一次是 make_x函数中x1的默认构造,第二次是return x1引发的复制构 造。不同的是,由于x2是一个右值引用,引用的对象是函数make_x 返回的临时对象,因此该临时对象的生命周期得到延长
右值引用的作用就体现了
这会减少对象复制,提升程序性能
移动语义
移动语义(move semantic):某对象持有的资源或内容转移给另一个对象。
class BigMemoryPool {
public:
static const int PoolSize = 4096;
BigMemoryPool() : pool_(new char[PoolSize]) {}
~BigMemoryPool()
{
if (pool_ != nullptr) {
delete[] pool_;
}
}
BigMemoryPool(BigMemoryPool&& other)
{
std::cout << "move big memory pool." << std::endl;
pool_ = other.pool_;
other.pool_ = nullptr;
}
BigMemoryPool(const BigMemoryPool& other) : pool_(new
char[PoolSize])
{
std::cout << "copy big memory pool." << std::endl;
memcpy(pool_, other.pool_, PoolSize);
}
private:
char *pool_;
};
BigMemoryPool (BigMemoryPool&& other),它的形参是一个 右值引用类型,称为移动构造函数。
对于复制构造函数而言形参是一个左值引用,也就是说函数的实参必 须是一个具名的左值,在复制构造函数中往往进行的是深复制,即在 不能破坏实参对象的前提下复制目标对象。
而移动构造函数恰恰相 反,它接受的是一个右值,其核心思想是通过转移实参对象的数据以 达成构造目标对象的目的,也就是说实参对象是会被修改的。
对于右值,编译器会优先选择使 用移动构造函数去构造目标对象。当移动构造函数不存在的时候才会 退而求其次地使用复制构造函数。
除移动构造函数能实现移动语义以外,移动赋值运算符函数也能完成移动操作
class BigMemoryPool {
public:
…
BigMemoryPool& operator=(BigMemoryPool&& other)
{
std::cout << "move(operator=) big memory pool." <<
std::endl;
if (pool_ != nullptr) {
delete[] pool_;
}
pool_ = other.pool_;
other.pool_ = nullptr;
return *this;
}
private:
char *pool_;
};
int main()
{
BigMemoryPool my_pool;
my_pool = make_pool();
}
/*运行结果
copy big memory pool.
move big memory pool.
move(operator=) big memory pool
*/
值类别
表达式首先被分为了泛左值(glvalue)和右值(rvalue),其中 泛左值被进一步划分为左值和将亡值,右值又被划分为将亡值和纯右 值。理解这些概念的关键在于泛左值、纯右值和将亡值。
1.所谓泛左值是指一个通过评估能够确定对象、位域或函数的标 识的表达式。简单来说,它确定了对象或者函数的标识(具名对 象)。
2.而纯右值是指一个通过评估能够用于初始化对象和位域,或者 能够计算运算符操作数的值的表达式。
3.将亡值属于泛左值的一种,它表示资源可以被重用的对象和位 域,通常这是因为它们接近其生命周期的末尾,另外也可能是经过右值引用的转换产生的。
剩下的两种类别就很容易理解了,其中左值是指非将亡值的泛左 值,而右值则包含了纯右值和将亡值。再次强调,值类别都是表达式 的属性,所以我们常说的左值和右值实际上指的是表达式,不过为了 描述方便我们常常会忽略它。
左值转换为右值
在C++11的标准库中还提供了一个函数模板std::move帮助我们将 左值转换为右值,这个函数内部也是用static_cast做类型转换。只不 过由于它是使用模板实现的函数,因此会根据传参类型自动推导返回 类型,省去了指定转换类型的代码。
int &&k = static_cast<int&&>(i); // 编译成功
完美转发
万能引用
我们知道右值引用只能绑定一个 右值,但是万能引用既可以绑定左值也可以绑定右值
void foo(int &&i) {} // i为右值引用
template<class T>
void bar(T &&t) {} // t为万能引用
int get_val() { return 5; }
int &&x = get_val(); // x为右值引用
auto &&y = get_val(); // y为万能引用
所谓的万能引用是因 为发生了类型推导,在T&&和auto&&的初始化过程中都会发生类型的 推导,如果已经有一个确定的类型,比如int &&,则是右值引用。
在这个推导过程中,初始化的源对象如果是一个左值,则目标对象会 推导出左值引用;反之如果源对象是一个右值,则会推导出右值引 用,不过无论如何都会是一个引用类型。
引用折叠
&& + &&->&& : 右值的右值引用是右值
&& + &->& : 右值的左值引用是左值
& + &&->& : 左值的右值引用是左值
& + &->& : 左值的左值引用是左值
上面的表格显示了引用折叠的推导规则,可以看出在整个推导过 程中,只要有左值引用参与进来,最后推导的结果就是一个左值引用。只有实际类型是一个非引用类型或者右值引用类型时,最后推导 出来的才是一个右值引用。
#include <iostream>
#include <string>
template<class T>
void show_type(T t)
{
std::cout << typeid(t).name() << std::endl;
}
template<class T>
void normal_forwarding(T t)
{
show_type(t);
}
int main()
{
std::string s = "hello world";
normal_forwarding(s);
}
完美转发
万能引用最典型的用途被称为完美转 发。
#include <iostream>
#include <string>
template<class T>
void show_type(T t)
{
std::cout << typeid(t).name() << std::endl;
}
template<class T>
void normal_forwarding(T t)
{
show_type(t);
}
int main()
{
std::string s = "hello world";
normal_forwarding(s);
}
normal_forwarding是一个常规的转发 函数模板,它可以完成字符串的转发任务。
也就是说std::string 在转发过程中会额外发生一次临时对象的复制。其中一个解决办法是 将void normal_forwarding(T t)替换为void normal_ forwarding(T &t),这样就能避免临时对象的复制。
不过这样会 带来另外一个问题,如果传递过来的是一个右值,则该代码无法通过 编译,例如:
std::string get_string()
{
return "hi world";
}
normal_forwarding(get_string()); // 编译失败
万能引用的出现改变了这个尴尬的局面。上文提到过,对于万能 引用的形参来说,如果实参是给左值,则形参被推导为左值引用;反 之如果实参是一个右值,则形参被推导为右值引用,所以下面的代码 无论传递的是左值还是右值都可以被转发,而且不会发生多余的临时 复制:
#include <iostream>
#include <string>
template<class T>
void show_type(T t)
{
std::cout << typeid(t).name() << std::endl;
}
template<class T>
void perfect_forwarding(T &&t)
{
show_type(static_cast<T&&>(t));
}
std::string get_string()
{
return "hi world";
}
int main()
{
std::string s = "hello world";
perfect_forwarding(s);
perfect_forwarding(get_string());
}
当实参是一个左值时,T被推导为 std::string&,于是static_cast<T&&> 被推导为 static_cast <std::string&>,传递到show_type函数时继续 保持着左值引用的属性;当实参是一个右值时,T被推导为 std::string,于是static_cast<T&&> 被推导为 static_cast<std::string&&>,所以传递到show_type函数时 保持了右值引用的属性
在C++11的标准库中提供了一个 std::forward函数模板,在函数内部也是使用static_cast进行 类型转换
template<class T>
void perfect_forwarding(T &&t)
{
show_type(std::forward<T>(t));
}
参考和后记
现代C++语言核心特性解析 (豆瓣) (douban.com)
宇宙最全面的C++面试题v2.0 - 知乎 (zhihu.com)
有待补充,未完待续
侵权联系删除