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;
}

image

在情况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;
}

image

这使我们能够在隐式对象是左值时执行高效操作,而在隐式对象是右值时执行安全操作。

对于高级读者:
当隐式对象是非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++开发者并不了解此特性(可能导致使用错误或效率低下)。
  • 标准库通常不采用此特性。

基于上述原因,我们不推荐将引用限定符作为最佳实践。相反,建议始终立即使用访问函数的结果,避免将返回的引用保存以备后用。

posted @ 2026-01-03 16:38  游翔  阅读(17)  评论(0)    收藏  举报