不止八股---CPP(右值引用)

问题

❤请回答什么叫左值引用,什么叫右值引用。
什么是将亡值,什么是纯右值。
❤移动语义与完美转发了解吗。
什么是引用折叠?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)

有待补充,未完待续

侵权联系删除

  • 7
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值