c++11总结03——右值引用

左值和右值

c++11中所有的值必属于左值、将亡值、纯右值三者之一。将亡值和纯右值都属于右值。

区分左右值属性的方法: 若可对表达式用&符取址,则为左值,否则为右值。 例如int func() { return 0; }   函数func()的返回值为右值,则不能使用&func()。

将亡值

将亡值是C++11新增的跟右值引用相关的表达式,这样表达式通常是将要被移动的对象,比如返回右值引用T&&的函数返回值、std::move的返回值,或者转换为T&&的类型转换函数的返回值。

将亡值可以理解为通过“盗取”其他变量内存空间的方式获取到的值。在确保其他变量不再被使用、或即将被销毁时,通过“盗取”的方式可以避免内存空间的释放和分配,能够延长变量值的生命期。

右值引用

类的右值是一个临时对象,如果没有被绑定到引用,在表达式结束时就会被废弃。于是我们可以在右值被废弃之前,移走它的资源进行废物利用,从而避免无意义的复制。被移走资源的右值在废弃时已经成为空壳,

析构的开销也会降低。

右值中的数据可以被安全移走这一特性使得右值被用来表达移动语义。以同类型的右值构造对象时,需要以引用形式传入参数。右值引用顾名思义专门用来引用右值。

由于右值引用只能绑定在右值上,而右值要么是字面常量,要么是临时对象,所以:

  • 右值引用的对象,是临时的,即将被销毁;
  • 右值引用的对象,不会在其它地方使用。

这两个特性意味着:接受和使用右值引用的代码,可以自由地接管所引用的对象的资源,而无需担心对其他代码逻辑造成数据破坏

引用叠加

我们先来看一段代码。

typedef int& intR;
typedef intR& intRR;

int main() {
    int foo = 42;
    intR bar = foo;
    intRR baz = bar;
    return 0;
}

在这里,intR 实际上是 int&。因此 intRR 就变成了 int& &,注意两个 & 之间有一个空格,表示这是对 int 类型引用的引用,也就是引用的叠加。在 C++11 之前,编译这份代码是会报错的:

 错误:无法声明对‘intR {aka int&}’的引用

这是因为在 C++11 之前,C++ 标准没有写明引用叠加。在 C++11 中,引用叠加有如下规则:

Type&  &  -> Type&
Type&  && -> Type&
Type&& &  -> Type&
Type&& && -> Type&&

这有点类似布尔代数中的与运算:左值引用是 0,右值引用是 1。因此,在 C++11 中,上述代码中的 intRR 实际就是 int& 类型。这样一来,代码就合法了。

同样的引用叠加规则,也可以应用到模板参数推导中。看这个例子

template  void func(T&& foo);
auto fp = func;

在这里,func 是一个模板函数,fp 是函数指针。要确定 fp 的实际类型,就要先确定模板函数参数的类型。

  • 在模板中,T 被 int&& 替换,因此 T 是 int 的右值引用;
  • 在函数参数列表声明中,foo 是 T&& 类型,因此是 int&& && 类型,根据叠加规则,实际 foo 是 int&& 类型。

这样一来,fp 就是 void (*)(int&&) 类型的指针了。

move 语义

假设 class Container 有这样的定义

class Container {
private:
	typedef std::string Resource;

public:
	Container() {
		resource_ = new Resource;
		std::cout << "default constructor." << std::endl;
	}
	explicit Container(const Resource& resource) {
		resource_ = new Resource(resource);
		std::cout << "explicit constructor." << std::endl;
	}
	~Container() {
		delete resource_;
		std::cout << "destructor" << std::endl;
	}
	Container(const Container& rhs) {
		resource_ = new Resource(*(rhs.resource_));
		std::cout << "copy constructor." << std::endl;
	}
	Container& operator=(const Container& rhs) {
		delete resource_;
		resource_ = new Resource(*(rhs.resource_));
		std::cout << "copy assignment." << std::endl;
		return *this;
	}

private:
	Resource* resource_ = nullptr;
};

于是当你执行类似这样的代码的时候,你会很郁闷地发现,效率很低:

Container get() {
  Container ret("tag");
  return ret;
}

int main() {
  Container foo;
  // ...
  foo = get();
  return 0;
}

在执行 bar = foo(); 的时候,会进行这样的操作:

  • 从函数返回值中得到临时对象 rhs
  • 销毁 bar 中的资源(delete resource_;);
  • 将 rhs 中的资源拷贝一份,赋值给 bar 中的资源(resource_ = new Resource(*(rhs.resource_)););
  • 销毁 rhs 这一临时对象。

仔细想想你会发现,销毁 bar 中的资源,再从临时对象中复制相应的资源,这件事情完全没有必要。我们最好能直接抛弃 bar 中的资源而后直接接管 foo 返回的临时对象。这就是 move 语义。

这样一来,就意味着我们需要重载 Container 类的赋值操作符,它应该有这样的函数声明:

Container& Container::operator=( rhs)

为了与拷贝版本的赋值运算符区分,我们希望,当 Container::operator= 的右操作数是右值引用时,调用这个版本的赋值运算符,那么毫无疑问, 应该是 Container&&。于是我们定义它(称为移动赋值运算符,以及同时定义移动构造函数):

class Container {
 private:
  typedef std::string Resource;

 public:
  Container() {
    resource_ = new Resource;
    std::cout << "default constructor." << std::endl;
  }
  explicit Container(const Resource& resource) {
    resource_ = new Resource(resource);
    std::cout << "explicit constructor." << std::endl;
  }
  ~Container() {
    delete resource_;
    std::cout << "destructor" << std::endl;
  }
  Container(const Container& rhs) {
    resource_ = new Resource(*(rhs.resource_));
    std::cout << "copy constructor." << std::endl;
  }
  Container& operator=(const Container& rhs) {
    delete resource_;
    resource_ = new Resource(*(rhs.resource_));
    std::cout << "copy assignment." << std::endl;
    return *this;
  }
  Container(Container&& rhs) : resource_(rhs.resource_) {
    rhs.resource_ = nullptr;
    std::cout << "move constructor." << std::endl;
  }
  Container& operator=(Container&& rhs) {
    Resource* tmp = resource_; resource_ = rhs.resource_; rhs.resource_ = tmp;
    std::cout << "move assignment." << std::endl;
    return *this;
  }

 private:
  Resource* resource_ = nullptr;
};

亦即,我们只需要对两个指针的值进行操作就可以了。这样一来,相同代码的执行过程会变成:

Container get() {
  Container ret("tag");
  return ret;
}

int main() {
  Container foo;
  // ...
  foo = get();
  return 0;
}

move-demo-return-Container

  • 从函数返回值中得到临时对象 rhs
  • 交换 foo.resource_ 和 rhs.resource_ 两个指针的值;
  • 销毁 rhs 这一临时对象。

这相当于我们将临时对象 rhs 中的资源「移动」到了 foo 当中,避免了销毁资源再拷贝赋值的开销。

完美转发(perfect forwarding)

首先我们来看一个工厂函数

template
std::shared_ptr factory(const ArgT& arg) {
    return shapred_ptr(new T(arg));
}

factory 函数有两个模板参数 T 与 ArgT,并假定类型 T 有一个构造函数,可以接受 const ArgT& 类型的参数,进行 T 类型对象的构造,然后返回一个 T 类型的智能指针,指向构造出来的对象。

毫无疑问,在这个例子里,factory 函数的 arg 变量既可以接受左值,也可以接受右值(允许将右值绑定在常量左值引用上)。但这里还有一个问题,按照之前的分析,不论 arg 接受的是什么类型,到了 factory 函数内部,arg 本身都将是一个左值。这样一来,假设类型 T 的构造函数支持对 ArgT 类型的右值引用,也将永远不会被调用。也就是说,factory 函数无法实现 move 语义,也就无法不能算是完美转发。

这里我们引入一个函数,它是标准库的一部分:

template
T&& forward(typename std::remove_reference::type& a) noexcept
{
  return static_cast(a);
}

当 a 的类型是 S& 的时候,函数将返回 S&;当 a 的类型是 S&& 的时候,函数将返回 S&&。因此,在这种情况下,我们只需要稍微改动工厂函数的定义就可以了:

template
std::shared_ptr factory(ArgT&& arg) {
    return std::shared_ptr(new T(std::forward(arg)));
}

于是:

  • 当 arg 是接受的参数是 Type& 时,ArgT 是 Type&arg 的类型是 Type&T::T(Type&) 被调用;
  • 当 arg 是接受的参数是 Type&& 时,ArgT 是 Type&&arg 的类型是 Type&&T::T(Type&&) 被调用。

这样一来,就保留了 move 语义,实现了完美转发。

std::move

标准库还定义了 std::move 函数,它的作用就是将传入的参数以右值引用的方式返回。

template
typename std::remove_reference::type&&
std::move(T&& a) noexcept
{
  typedef typename std::remove_reference::type&& RvalRef;
  return static_cast(a);
}

首先,出现了两次 std::remove_reference::type&&,它确保不论 T 传入的是什么,都将返回一个真实类型的右值引用。static_cast(a) 则将 a 强制转换成右值引用并返回。有了 std::move,我们就可以调用 std::unique_ptr 的移动赋值运算符了(当然,单独这样调用可能没有什么意义):

std::unique_ptr new_ptr = std::move(old_ptr);
// old_ptr 应当立即被销毁,或者赋予别的值
// 不应对 old_ptr 当前的状态做任何假设

在这里,因为使用了 std::move 窃取了 old_ptr 中的资源,然后将他们移动到了 new_ptr 中去。这就隐含了一层意思:接下来我们不会在用 old_ptr 做任何事情,除非我们显式地对 old_ptr 赋予新值。事实上,我们不应对 old_ptr 当前的状态做任何假设,它就和已定义但未初始化的状态一样。因为,old_ptr 当前的状态,完全取决于 std::unique_ptr::operator=(unique_ptr&&) 的行为。

 

部分参考: https://liam.page/2016/12/11/rvalue-reference-in-Cpp/

你可能感兴趣的:(c++11/17,右值引用)