Skip to main content

Command Palette

Search for a command to run...

围绕std::move()谈谈通用引用,类型推导和引用折叠

Published
围绕std::move()谈谈通用引用,类型推导和引用折叠

从std::move()说起

对于std::move(),既可以接受一个左值,也可以接受一个右值。它的函数定义是这个样子的

template <typename T>
typename remove_reference<T>::type&& move(T&& t) noexcept {
    return static_cast<typename remove_reference<T>::type&&>(t);
}

但形参中的T&&并不是是一个右值引用, 如果他是一个右值引用, 那么下面代码是无法编译通过的

Foo f;
std::move(f); // error

其实这里的T&&是一个通用引用(universal reference)

通用引用

T&& Doesn’t Always Mean “Rvalue Reference” by Scott Meyers

通用引用有一条性质,当匹配一个左值表达式时, 会表现的像一个左值引用; 当匹配一个右值表达式的时候, 会表现的像一个右值引用. (后面会谈到是如何实现的)

构成通用引用的条件

正如 Scott Meyers

If a variable or parameter is declared to have type T&& for some deduced type T, that variable or parameter is a universal reference.

所说的, 构成通用引用必须具有两个条件

  1. 形如T&&
  2. T的类型需要推导(deduced type)

举例

  • 必须形如T&& ```cpp int lvalue = 3;

const auto&& r = lvalue; //错误:r不是通用引用,而是右值引用,故不能通过左值初始化!

template void func(const T&& p);

func(lvalue); //错误:p不是通用引用,而是右值引用,故不能通过左值初始化!

必须是`T&&`, 哪怕是`const T &&`都不行。

* 必须存在类型推导
```cpp
template<typename T>
class Bar {
private:
    T member;
public:
    Bar(Bar&& other); // 不是通用引用,而是右值引用
};

other的类型是Bar&&, 而非T&&. 因此,是右值引用

类型推导和引用折叠

那么通用引用是如何实现 当匹配一个左值表达式时, 会表现的像一个左值引用; 当匹配一个右值表达式的时候, 会表现的像一个右值引用 的呢?

  1. 编译器允许它匹配左值和右值
  2. 类型推导和引用折叠最终让它变成左值引用右值引用

类型推导规则

通用引用auto&&T&&的类型推导规则是: 对于

template<typename T>
void f(T&& param);

f(expr);

// auto 和 模版 通过一个非常规范非常系统化的转换流程来转换彼此
  1. 若匹配的expr是左值(lvalue)autoT被推导成lvalue引用类型;例如匹配i时(int i=3;),推导结果是int&;这非常不寻常,这是模板类型推导中唯一一种T被推导为引用的情况

  2. 若匹配的expr是右值rvalueautoT被正常推导成rvalue的类型;例如匹配3时,推导结果是int

类型推导带来的问题

在C++中,引用的引用是非法的,如果在编写代码是写出引用的引用,编译器会报错。

int f = 3;

int & & r1 = f;   //error: cannot declare reference to ‘int&’, which is not a typedef or a template type argument
int & && r2 = f;  //error: cannot declare reference to ‘int&’, which is not a typedef or a template type argument
int && & r3 = f;  //error: cannot declare reference to ‘int&&’, which is not a typedef or a template type argument
int && && r4 = f; //error: cannot declare reference to ‘int&&’, which is not a typedef or a template type argument

但是在编译期,根据类型推导的规则,就会产生引用的引用

template <typename T>
void func(T&& x) {} // 等价于 void func(int& && x);
int main() {
    int a = 0;
    func(a);
}

由于T&& 接受一个左值a, 因此T被类型推导为int&, 此时函数定义等价于 void func(int& && x);

由于引用的引用是非法的。那我们这些例子中都存在引用的引用怎么办呢?好在,这些引用的引用不是存在于源代码中(否则编译失败),而是在编译过程中临时产生的。编译器会立即消除它们,手段就是引用折叠(reference collapsing)

引用折叠

引用折叠规则: “如果任一引用为左值引用,则结果为左值引用。否则(即,如果引用都是右值引用),结果为右值引用。”

也就是说:

  1. 右值引用的右值引用折叠成(collapses into)右值引用;
  2. 其他情况折叠成(collapses into)左值引用;

这样上面的void func(int& && x); 被折叠成为 void func(int& x); 顺利通过编译。

类型推导引用折叠在通用引用机制中起着重要作用:编译器允许T&&匹配左值和右值,然后经过类型推导和(或)引用折叠,T&&才最终变成左值引用或者右值引用。

回到std::move()

那么std::move()是怎样的工作的呢?

template <typename T>
typename remove_reference<T>::type&& move(T&& t) noexcept {
    return static_cast<typename remove_reference<T>::type&&>(t);
}

typename的作用是在remove_reference<T>::type前使用了typename关键字来修饰,编译器才会将该名称当成是类型, 具体可参考知无涯之C++ typename的起源与用法

remove_reference的作用是去掉类型的引用(左值引用和右值引用), 直接返回类型的基本内容。

// 对于 move1(Foo())
// Foo()是右值, T的类型被推断为Foo, move函数被推断为
Foo&& move1(Foo&& item) noexcept {
    return static_cast<Foo&&>(item);
}

// 对于 f=Foo(); move1(f);
// f是左值, T的类型被推断为 Foo&
//Foo&& move1(Foo& && item) noexcept {
//    return static_cast<Foo&&>(item);
//}
// 又因为引用折叠, 被推断为
Foo&& move1(Foo& item) noexcept {
    return static_cast<Foo&&>(item);
}

因此std::move()只是一个语法糖, 并不会带来运行时开销。

59cdb14e158f9b48b22b0f5daf3b5e4d.jpg

Ref

  1. https://www.yuanguohuo.com/2018/05/25/cpp11-universal-ref/

More from this blog

<Programming with Types>随想: Chapter 2. Basic types

类型限制了一个变量可以接受的有效值的集合,对数据可以进行的操作,数据的意义。 空类型(The empty type) 根据类型的定义,类型定义了可以接受的有效值集合,那么这个集合有没有可能为空?答案是有可能的,TypeScirpt 的 never 就是这种类型。 需要注意的是,空类型不同与 void, 后者是有效值集合当中只有一个值,但这个值没有任何意义。而空类型的有效值集合本身是空的。 使用场景 控制流分析 在函数调用时,标志一个函数不会返回任何值: 在调用过程中抛出异常、死循环或者程序崩溃...

Mar 5, 2023
<Programming with Types>随想: Chapter 2. Basic types

精读《设计机器学习系统》-ch04: 训练数据

不同于 Chapter03 从系统的角度来处理数据,这一章从数据科学的视角来处理数据。这章的标题是“training Data”,而非“training dataset”,因为 数据集(dataset) 意味着有限(finite)和固定(stationary), 而现实生产环境中的 数据(data) 通常是 无限 并且 不固定 的。 抽样 抽样方法在 ML 项目的生命周期中无处不在,在这一节中,我们使用生成训练数据作为例子。 那为什么需要抽样?直接使用全部数据不可以吗? 首先,在现实世界中,并不...

Dec 10, 2022
精读《设计机器学习系统》-ch04: 训练数据
7

70 Talk

19 posts