15-10 引用限定符
作者注:
本课为可选内容。建议您快速浏览以熟悉材料,但无需完全理解即可继续后续课程。
在第14.7节——返回数据成员引用的成员函数中,我们讨论了当隐式对象是右值时,调用返回数据成员引用的访问函数可能带来的危险。以下是简要回顾:
#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; } // getter returns by const reference
};
// createEmployee() returns an Employee by value (which means the returned value is an rvalue)
Employee createEmployee(std::string_view name)
{
Employee e { name };
return e;
}
int main()
{
// Case 1: okay: use returned reference to member of rvalue class object in same expression
std::cout << createEmployee("Frank").getName() << '\n';
// Case 2: bad: save returned reference to member of rvalue class object for use later
const std::string& ref { createEmployee("Garbo").getName() }; // reference becomes dangling when return value of createEmployee() is destroyed
std::cout << ref << '\n'; // undefined behavior
return 0;
}

在情况2中,createEmployee(“Garbo”)返回的右值对象在初始化ref后被销毁,导致ref指向刚被销毁的数据成员。后续对ref的操作将表现为未定义行为。
这形成了一种微妙的困境。
若 getName() 函数按值返回,当隐式对象为右值时是安全的,但当隐式对象为左值(最常见情况)时会产生昂贵且不必要的复制。
若 getName() 函数以 const 引用形式返回,虽然高效(无需复制 std::string),但当隐式对象为右值时可能被误用(导致未定义行为)。
由于成员函数通常在左值隐式对象上调用,惯用做法是采用 const 引用返回,并在隐式对象为右值时避免误用返回的引用。
引用限定符Ref qualifiers
上述挑战的根源在于,我们希望仅用一个函数处理两种不同情况(一种是隐式对象为左值,另一种是隐式对象为右值)。对一种情况最优的方案,对另一种情况未必理想。
为解决此类问题,C++11引入了鲜为人知的引用限定符特性,它允许我们根据隐式对象是左值还是右值来重载成员函数。借助该特性,我们可以创建两个版本的getName()函数——一个用于隐式对象为左值的情况,另一个用于隐式对象为右值的情况。
首先,我们从未添加引用限定符的 getName() 版本开始:
const std::string& getName() const { return m_name; } // callable with both lvalue and rvalue implicit objects
要重新限定此函数的重载,我们为仅匹配左值隐式对象的重载添加 & 修饰符,为仅匹配右值隐式对象的重载添加 && 修饰符:
const std::string& getName() const & { return m_name; } // & qualifier overloads function to match only lvalue implicit objects, returns by reference
std::string getName() const && { return m_name; } // && qualifier overloads function to match only rvalue implicit objects, returns by value
由于这些函数是不同的重载,它们可以具有不同的返回类型!我们的左值限定重载通过 const 引用返回,而右值限定重载则按值返回。
以下是上述内容的完整示例:
#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; } // & qualifier overloads function to match only lvalue implicit objects
std::string getName() const && { return m_name; } // && qualifier overloads function to match only rvalue implicit objects
};
// createEmployee() returns an Employee by value (which means the returned value is an rvalue)
Employee createEmployee(std::string_view name)
{
Employee e { name };
return e;
}
int main()
{
Employee joe { "Joe" };
std::cout << joe.getName() << '\n'; // Joe is an lvalue, so this calls std::string& getName() & (returns a reference)
std::cout << createEmployee("Frank").getName() << '\n'; // Frank is an rvalue, so this calls std::string getName() && (makes a copy)
return 0;
}

这使我们能够在隐式对象是左值时执行高效操作,而在隐式对象是右值时执行安全操作。
对于高级读者:
当隐式对象是非const临时对象时,上述getName()的右值重载在性能方面可能不够理想。这种情况下,隐式对象终将在表达式结束时销毁。因此,与其让右值获取器返回成员的(可能代价高昂的)副本,不如尝试使用std::move移动该成员。可通过为非const右值添加以下重载获取器实现:
// If the implicit object is a non-const rvalue, use std::move to try to move m_name
std::string getName() && { return std::move(m_name); }
这既可以与 const rvalue 获取器共存,也可以直接用它替代(因为 const rvalue 相当罕见)。
我们在第 22.4 课中介绍了 std::move —— std::move。
关于带引用修饰符的成员函数的几点说明
首先,对于给定函数,非引用修饰符重载与引用修饰符重载不能共存。请选择其中一种使用。
其次,类似于const左值引用可绑定右值的机制,若仅存在const左值限定函数,则该函数可接受左值或右值隐式对象。
第三,任何限定重载均可通过显式删除(使用=delete)禁止调用。例如删除右值限定版本将阻止该函数处理右值隐式对象。
那么为什么我们不建议使用引用修饰符呢?
虽然引用修饰符很简洁,但在此处使用存在一些弊端。
- 为每个返回引用的获取器添加右值重载会增加类结构的冗余,而这种情况并不常见,且通过良好编程习惯即可轻松规避。
- 采用右值重载按值返回意味着,即使在可安全使用引用的场景(如本节开头示例中的情况1),我们仍需承担复制(或移动)的开销。
此外:
- 多数C++开发者并不了解此特性(可能导致使用错误或效率低下)。
- 标准库通常不采用此特性。
基于上述原因,我们不推荐将引用限定符作为最佳实践。相反,建议始终立即使用访问函数的结果,避免将返回的引用保存以备后用。

浙公网安备 33010602011771号