章节目录

引用限定符

本节阅读量:

前面学习返回数据成员引用的成员函数时,我们讨论过:当隐式对象是右值时,返回数据成员的引用是危险的。下面简要回顾一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <string>
#include <string_view>

class Employee
{
private:
	std::string m_name{};

public:
	Employee(std::string_view name): m_name { name } {}
	const std::string& getName() const { return m_name; } //  返回 const 引用
};

// createEmployee() 按值返回一个 Employee (意味着返回的是一个右值)
Employee createEmployee(std::string_view name)
{
	Employee e { name };
	return e;
}

int main()
{
	// Case 1: okay: 在同一个表达式中使用右值的成员的引用
	std::cout << createEmployee("Frank").getName() << '\n';

	// Case 2: 有问题: 保存右值返回的成员的引用,稍后使用
	const std::string& ref { createEmployee("Garbo").getName() }; // 悬空引用,createEmployee() 创建的临时对象已经被销毁
	std::cout << ref << '\n'; // 未定义的行为

	return 0;
}

在案例2中,从createEmployee(“Garbo”) 返回的右值对象会在初始化ref后被销毁,使ref引用到刚刚被销毁的数据成员。之后继续使用ref会造成未定义行为。

这个问题有些棘手。

  1. 如果getName()函数按值返回,会生成昂贵且不必要的副本。
  2. 如果getName()函数通过常量引用返回,则效率很高(因为不会生成std::string的副本),但当调用对象是右值时,可能会被误用(导致未定义行为)。

由于成员函数通常在左值对象上调用,因此传统做法是通过常量引用返回,并在隐式对象是右值时避免误用返回的引用。


引用限定符

上述挑战的根源在于,我们希望一个函数服务于两种不同情况:一种是隐式对象为左值,另一种是隐式对象为右值。适合一种情况的最佳方案,对另一种情况可能并不理想。

为了解决这类问题,C++11引入了一个鲜为人知的特性,称为引用限定符。它允许根据成员函数是在左值对象还是右值对象上调用来进行重载。使用这个特性,可以创建getName()的两个版本:一个用于对象是左值的情况,另一个用于对象是右值的情况。

首先,从getName() 的非引用限定版本开始:

1
std::string& getName() const { return m_name; } // 在左值和右值对象上均可调用

为了给此函数添加引用限定,可以将「&」限定符加到只匹配左值对象的重载中,并将「&&」限定符加到只匹配右值对象的重载中:

1
2
const std::string& getName() const &  { return m_name; } //  & 限定只匹配左值隐式对象, 按引用返回
std::string        getName() const && { return m_name; } // && 限定只匹配右值隐式对象, 按值返回

因为这些函数是不同的重载,所以它们可以拥有不同的返回类型!左值限定重载通过常量引用返回,而右值限定重载通过值返回。

下面是完整示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <string>
#include <string_view>

class Employee
{
private:
	std::string m_name{};

public:
	Employee(std::string_view name): m_name { name } {}

	const std::string& getName() const &  { return m_name; } //  & 限定只匹配左值隐式对象, 按引用返回
	std::string        getName() const && { return m_name; } // && 限定只匹配右值隐式对象, 按值返回
};

// createEmployee() 按值返回一个 Employee (意味着返回的是一个右值)
Employee createEmployee(std::string_view name)
{
	Employee e { name };
	return e;
}

int main()
{
	Employee joe { "Joe" };
	std::cout << joe.getName() << '\n'; // Joe 是 左值, 调用的是 std::string& getName() & (返回引用)
    
	std::cout << createEmployee("Frank").getName() << '\n'; // Frank 是 右值, 调用的是 std::string getName() && (返回拷贝)

	return 0;
}

这允许我们在隐式对象是左值时保持高效率,而在隐式对象是右值时保证安全。


关于引用限定成员函数的一些注释

首先,对于给定函数,非引用限定重载和引用限定重载不能共存。只能使用其中一种。

其次,如果仅提供左值限定重载(即未定义右值限定版本),则任何使用右值隐式对象调用该函数的代码都会导致编译错误。这提供了一种有用的方法,可以完全防止函数与右值隐式对象一起使用。


那么,为什么不建议使用引用限定符呢?

虽然引用限定符有用,但以这种方式使用它们也有一些缺点。

  1. 向每个返回引用的getter添加右值重载会增加类的混乱度,而它只是为了解决不常见的情况;通过良好的使用习惯通常很容易避免这类问题。
  2. 右值重载按值返回意味着必须支付复制(或移动)的成本,即使在可以安全使用引用的情况下也是如此(例如本课开头示例中的情况1)。

此外:

  1. 大多数C++开发人员都不知道该功能(这可能会导致错误或使用效率低下)。
  2. 标准库通常不使用此功能。

基于以上所有原因,不建议将引用限定符作为最佳实践。相反,建议始终立即使用访问函数的结果,而不要保存返回的引用供以后使用。


15.8 友元类和友元成员函数

上一节

15.10 第15章总结

下一节