第八章 引用、万能引用等

# 第八章 引用、万能引用等

接下来的几个问题将涉及引用、万能引用(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::forwardstd::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));
}
1
2
3
4
5
6
7
8

这也被称为完美转发(perfect forwarding)。它有两个重载版本。

一个版本将左值(lvalues)作为左值转发,将右值作为右值转发;另一个版本是条件类型转换,它将右值作为右值转发,禁止将右值作为左值转发。试图将右值作为左值转发会导致编译时错误。

# 问题60:万能引用和右值引用有什么区别?

如果函数模板参数的类型为T&&T为推导类型),或者对象使用auto&&声明,那么这个参数或对象就是一个万能引用。

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

// 万能引用
auto&& v2 = v; 
1
2
3
4
5

你可能会问,什么是万能引用呢?当万能引用用右值初始化时,它就相当于右值引用;当用左值初始化时,它就相当于左值引用。它到底是什么取决于传入的内容。

如果类型声明的形式不是精确的type&&,或者没有发生类型推导(即没有使用auto),那么就是右值引用。

void  f(MyClass&& param); 
MyClass&& var1 = MyClass(); 
template<typename  T>
void  f(std::vector<T>&& param); 
1
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&&>; 
1
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); 
1
2
3
4

但函数不会抛出异常是什么意思呢?这意味着它不能使用任何会抛出异常的其他函数,它自身被声明为noexcept,并且不使用dynamic_cast转换为引用类型。

六个生成的特殊函数是隐式的noexcept函数。

如果在函数声明了noexcept的情况下仍然抛出异常,就会调用std::terminate

正如C++核心准则所指出的,当程序崩溃比实际处理异常更好时,你可以使用noexcept

使用noexcept可以给编译器提供执行某些优化的提示,也能让开发者知道无需处理可能的异常。

上次更新: 2025/03/27, 20:29:48

社区交流

讨论与留言

前往 GitHub Issues →

本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。

最近更新
第 1 课:专栏导论与安全边界
07-01
第 3 课:安装编译与调试环境
07-01
第 4 课:第三方库下载、编译与依赖管理
07-01
更多文章>