作者注
本课为选读内容。建议通读以了解基本概念,无需深入掌握即可继续后续课程。
问题回顾:右值对象返回引用的风险
在《返回数据成员引用的成员函数》中,我们指出:当通过返回引用的访问函数调用右值对象时,极易产生悬空引用。简例如下:
#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 引用
};
Employee createEmployee(std::string_view name)
{
Employee e{ name };
return e; // 返回值是右值
}
int main()
{
// 情况 1:安全——同一表达式内用完即弃
std::cout << createEmployee("Frank").getName() << '\n';
// 情况 2:危险——保存引用供后续使用
const std::string& ref{ createEmployee("Garbo").getName() }; // 右值对象销毁后 ref 悬空
std::cout << ref << '\n'; // 未定义行为
}
在“情况 2”中,createEmployee("Garbo")
产生的右值对象在初始化 ref
后即被销毁,ref
成为悬空引用,后续使用导致未定义行为。
两难抉择
- 若
getName()
按值返回,右值调用安全,但在常见左值调用时会带来不必要的拷贝开销。 - 若
getName()
返回const
引用,左值调用高效,但右值调用易被误用。
传统做法是返回 const
引用,并告诫使用者“不要在右值上保存引用”。
C++11 提供了更优雅的方案:引用限定符(ref-qualifiers)。
引用限定符
通过给成员函数添加 &
或 &&
限定符,可以根据隐式对象是左值还是右值提供重载:
const std::string& getName() const & { return m_name; } // 仅匹配左值
std::string getName() const && { return m_name; } // 仅匹配右值
&
限定:仅当隐式对象为左值时调用;返回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; } // 左值版本
std::string getName() const && { return m_name; } // 右值版本
};
Employee createEmployee(std::string_view name)
{
return Employee{ name };
}
int main()
{
Employee joe{ "Joe" };
std::cout << joe.getName() << '\n'; // 左值调用 & 版本
std::cout << createEmployee("Frank").getName() << '\n'; // 右值调用 && 版本
}
进阶:右值版本使用 std::move
若右值对象为非 const,其成员即将随对象销毁,可通过 std::move
避免拷贝:
std::string getName() && { return std::move(m_name); }
可与 const &&
版本并存,或完全替代之(因 const
右值较少见)。std::move
用法见第 22.4 课。
注意事项
- 不能共存:同一函数若存在引用限定符重载,则不能再存在无引用限定符的版本。
- 默认回退:若仅提供
const &
版本,右值对象亦可调用该版本(类似const T&
可绑定右值)。 - 显式删除:
= delete
可禁止某限定符版本,例如删除&&
版本即可阻止右值调用。
为何不推荐广泛使用引用限定符?
尽管优雅,但存在以下缺点:
- 代码膨胀:为每个返回引用的访问函数增加右值重载,增加维护负担,而右值误用场景并不常见且易于避免。
- 强制拷贝:右值重载按值返回,可能产生不必要的拷贝/移动开销。
- 认知度低:大多数开发者不熟悉此特性;标准库亦极少使用。
因此,我们不推荐将引用限定符作为最佳实践。
更简洁的做法是:立即使用访问函数返回值,不要将其引用保存供后续使用。