第八章 引用、万能引用等
# 第八章 引用、万能引用等
接下来的几个问题将涉及引用、万能引用(universal references),甚至还会提到noexcept
。
# 问题58:std::move
移动了什么?
std::move
什么都没移动。在运行时,它根本不会执行任何操作,甚至不会生成一个字节的可执行代码。
实际上,std::move
只是一个工具,用于将其输入的任何内容转换为右值引用(rvalue reference)。
因此,std::move
这个名字不太恰当,也许叫rvalue_cast
会更好,但它就叫这个名字,我们只要记住它并不会移动任何东西就行。它返回一个右值引用,而右值引用是移动操作的候选对象。对一个对象使用std::move
,是在告诉编译器这个对象可以被移动。这就是std::move
名字的由来:便于指定哪些对象可以被移动。
值得注意的是,从常量变量进行移动是不可能的,因为移动构造函数(move constructor)和移动赋值(move assignment)会改变执行移动操作的对象。然而,如果你尝试从常量对象进行移动,编译器不会报错,甚至不会给出警告。对常量对象的移动请求会被悄悄地转换为复制操作。
# 问题59:std::forward
转发了什么?
就像std::move
什么都没移动一样,std::forward
也什么都没转发。同样地,它在运行时也不会执行任何操作,甚至不会生成一个字节的可执行代码。
std::forward
和std::move
一样,也是一种类型转换。但它是如何使用的呢?
std::forward
最常见的使用场景是在函数模板中,该函数模板接受一个万能引用参数,并将其传递给另一个函数:
void process(MyClass&& rvalueArgument);
template<typename T>
void logAndProcess(T&& param) {
auto now = std::chrono::system_clock::now();
makeLogEntry("Calling 'process'", now);
process(std::forward(param));
}
2
3
4
5
6
7
8
这也被称为完美转发(perfect forwarding)。它有两个重载版本。
一个版本将左值(lvalues)作为左值转发,将右值作为右值转发;另一个版本是条件类型转换,它将右值作为右值转发,禁止将右值作为左值转发。试图将右值作为左值转发会导致编译时错误。
# 问题60:万能引用和右值引用有什么区别?
如果函数模板参数的类型为T&&
(T
为推导类型),或者对象使用auto&&
声明,那么这个参数或对象就是一个万能引用。
template<typename T>
void f(T&& param);
// 万能引用
auto&& v2 = v;
2
3
4
5
你可能会问,什么是万能引用呢?当万能引用用右值初始化时,它就相当于右值引用;当用左值初始化时,它就相当于左值引用。它到底是什么取决于传入的内容。
如果类型声明的形式不是精确的type&&
,或者没有发生类型推导(即没有使用auto
),那么就是右值引用。
void f(MyClass&& param);
MyClass&& var1 = MyClass();
template<typename T>
void f(std::vector<T>&& param);
2
3
4
需要记住的是,了解右值引用和万能引用的区别,有助于你更准确地阅读源代码。这是一个只能绑定到右值的右值类型,还是一个既可以绑定到右值也可以绑定到左值的万能引用呢?理解这一点也能避免与同事交流时产生歧义(“我这里用的是万能引用,不是右值引用……”)。
# 问题61:什么是引用折叠(reference collapsing)?
引用折叠可能发生在以下四种不同的场景中:
- 模板实例化(template instantiation)
auto
类型生成- 类型定义(typedefs)和别名声明(alias declarations)的创建与使用
- 使用
decltype
不允许声明引用的引用,但编译器可能会在上述场景中生成引用的引用。当编译器生成引用的引用时,引用折叠规则决定接下来会发生什么。
int x;
auto& & rx = x;
typedef int& T;
// a具有int&类型
T&& a;
template <typename T> void func(T&& a);
auto fp = func<int&&>;
2
3
4
5
6
7
引用分为两种(左值引用和右值引用),所以引用与引用的组合可能有四种:
- 左值引用到左值引用
- 左值引用到右值引用
- 右值引用到左值引用
- 右值引用到右值引用
如果在上述四种场景中出现了引用的引用,根据以下规则,这些引用会折叠为单个引用:
如果其中一个引用是左值引用,结果就是左值引用。否则(即两个都是右值引用),结果就是右值引用。
在类型推导区分左值和右值且发生引用折叠的场景中,万能引用被视为右值引用。
# 问题62:constexpr
函数在何时求值?
constexpr
函数可能在编译时求值,但这不是绝对的。它们既可以在运行时执行,也可以在编译时执行。这通常取决于编译器版本和优化级别。
如果在编译时使用constexpr
变量请求constexpr
函数的值,那么它将在编译时执行,例如:constexpr auto foo = bar(42)
,这里的bar
是一个constexpr
函数。
此外,如果constexpr
函数在C数组初始化或静态断言(static assertion)的上下文中执行,它也会在编译时求值。
如果需要一个常量,但你提供的只是一个运行时函数,编译器会提示你。
把所有函数都声明为constexpr
并不是一个好主意,因为大多数计算最好在运行时进行。同时,值得注意的是,constexpr
函数总是线程安全的,并且会被内联(inlined)。
# 问题63:何时应该将函数声明为noexcept
?
对于完全用C语言或其他不支持异常的语言编写的函数,绝对应该加上noexcept
。C++标准库对C标准库中的所有函数都隐式地做了这样的处理。
除此之外,对于不会抛出异常的函数,或者即使抛出异常你也不介意程序崩溃的函数,应该使用noexcept
。
下面是一段小代码示例,展示如何使用它:
void func1() noexcept ;
void func2() noexcept(true);
void func3() throw();
void func4() noexcept(false);
2
3
4
但函数不会抛出异常是什么意思呢?这意味着它不能使用任何会抛出异常的其他函数,它自身被声明为noexcept
,并且不使用dynamic_cast
转换为引用类型。
六个生成的特殊函数是隐式的noexcept
函数。
如果在函数声明了noexcept
的情况下仍然抛出异常,就会调用std::terminate
。
正如C++核心准则所指出的,当程序崩溃比实际处理异常更好时,你可以使用noexcept
。
使用noexcept
可以给编译器提供执行某些优化的提示,也能让开发者知道无需处理可能的异常。