14-7 返回数据成员引用的成员函数

在第12.12节——引用返回与地址返回中,我们探讨了引用返回机制。特别指出:“通过引用返回的对象在函数返回后必须存在”。这意味着不应通过引用返回局部变量,因为局部变量销毁后引用将悬空。但通常可以安全地通过引用返回函数参数(若参数本身是通过引用传递的)或具有静态作用域的变量(包括静态局部变量和全局变量),因为这些变量在函数返回后通常不会被销毁。

例如:

// Takes two std::string objects, returns the one that comes first alphabetically
const std::string& firstAlphabetical(const std::string& a, const std::string& b)
{
	return (a < b) ? a : b; // We can use operator< on std::string to determine which comes first alphabetically
}

int main()
{
	std::string hello { "Hello" };
	std::string world { "World" };

	std::cout << firstAlphabetical(hello, world); // either hello or world will be returned by reference

	return 0;
}

image

成员函数同样可以按引用返回,其安全返回规则与非成员函数一致。但成员函数存在一种特殊情况需要特别说明:即按引用返回数据成员的成员函数。

这种情况最常见于获取访问函数,因此我们将通过获取成员函数来阐释该主题。但需注意,此规则适用于任何返回数据成员引用的成员函数。


按值返回数据成员可能代价高昂

请看以下示例:

#include <iostream>
#include <string>

class Employee
{
	std::string m_name{};

public:
	void setName(std::string_view name) { m_name = name; }
	std::string getName() const { return m_name; } //  getter returns by value
};

int main()
{
	Employee joe{};
	joe.setName("Joe");
	std::cout << joe.getName();

	return 0;
}

image

在此示例中,访问函数 getName() 以值的方式返回 std::string m_name。

虽然这是最安全的做法,但也意味着每次调用 getName() 时都会创建 m_name 的昂贵副本。由于访问函数通常会被频繁调用,因此这通常并非最佳选择。


通过左值引用返回数据成员

成员函数也可通过(const)左值引用返回数据成员。

数据成员的生命周期与包含它们的对象相同。由于成员函数始终在对象上调用,且该对象必须存在于调用者的作用域内,因此成员函数通过(const)左值引用返回数据成员通常是安全的(因为通过引用返回的成员在函数返回时仍存在于调用者的作用域中)。

让我们更新上例,使getName()通过const左值引用返回m_name:

#include <iostream>
#include <string>

class Employee
{
	std::string m_name{};

public:
	void setName(std::string_view name) { m_name = name; }
	const std::string& getName() const { return m_name; } //  getter returns by const reference
};

int main()
{
	Employee joe{}; // joe exists until end of function
	joe.setName("Joe");

	std::cout << joe.getName(); // returns joe.m_name by reference

	return 0;
}

image

现在当调用 joe.getName() 时,会通过引用将 joe.m_name 返回给调用方,从而避免创建副本。调用方随后使用该引用将 joe.m_name 打印到控制台。

由于 joe 在调用方作用域内存在直至 main() 函数结束,因此对 joe.m_name 的引用在相同时间段内同样有效。

关键要点:
返回数据成员的(const)左值引用是可行的。函数返回后,隐含对象(包含该数据成员)仍存在于调用者的作用域内,因此任何返回的引用都将保持有效。


返回数据成员引用的成员函数的返回类型应与该数据成员的类型一致

通常,通过引用返回的成员函数的返回类型应与被返回的数据成员的类型一致。在上例中,m_name 的类型为 std::string,因此 getName() 返回 const std::string&。

若返回 std::string_view,每次函数调用都需创建临时 std::string_view 对象并返回,这会造成不必要的效率损失。若调用方需要 std::string_view,可自行进行类型转换。

最佳实践
返回引用类型的成员函数应返回与被返回数据成员相同类型的引用,以避免不必要的转换。

对于获取器,使用 auto 让编译器根据被返回成员推导返回类型,是确保不发生转换的有效方法:

#include <iostream>
#include <string>

class Employee
{
	std::string m_name{};

public:
	void setName(std::string_view name) { m_name = name; }
	const auto& getName() const { return m_name; } // uses `auto` to deduce return type from m_name
};

int main()
{
	Employee joe{}; // joe exists until end of function
	joe.setName("Joe");

	std::cout << joe.getName(); // returns joe.m_name by reference

	return 0;
}

image

相关内容:
我们在第10.9节——函数的类型推导中介绍了auto返回类型。

然而,从文档角度来看,使用auto返回类型会模糊获取器的返回类型。例如:

const auto& getName() const { return m_name; } // uses `auto` to deduce return type from m_name

该函数实际返回何种字符串类型尚不明确(可能是 std::string、std::string_view、C 风格字符串,或是完全不同的类型!)。

因此,我们通常更倾向于使用显式返回类型。


右值隐式对象与按引用返回

存在一种情况需要特别注意。在上例中,joe 是左值对象,其存在直至函数结束。因此 joe.getName() 返回的引用同样有效至函数结束。

但若隐式对象是右值(例如按值返回函数的返回值)呢?右值对象在其所属完整表达式结束时即被销毁。当右值对象被销毁时,对其成员的所有引用都会失效并悬空,使用此类引用将导致未定义行为。

因此,对右值对象成员的引用只能在其所属完整表达式内部安全使用。

提示:
完整表达式的定义已在第1.10课--表达式导论 中讲解。

警告:
右值对象在其创建的完整表达式结束时被销毁。此时对该右值对象成员的任何引用都将悬空。
对右值对象成员的引用只能在其创建的完整表达式内部安全使用。

让我们探讨几个相关案例:

#include <iostream>
#include <string>
#include <string_view>

class Employee
{
	std::string m_name{};

public:
	void setName(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;
	e.setName(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();

	// 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; // undefined behavior

	// Case 3: okay: copy referenced value to local variable for use later
	std::string val { createEmployee("Hans").getName() }; // makes copy of referenced member
	std::cout << val; // okay: val is independent of referenced member

	return 0;
}

image

当调用 createEmployee() 时,它将按值返回一个 Employee 对象。该返回的 Employee 对象是一个右值,其存在期将持续到包含 createEmployee() 调用的完整表达式结束为止。当该右值对象被销毁时,对其成员的任何引用都将变成悬空引用。

在情况 1 中,我们调用 createEmployee(“Frank”),该函数返回一个右值 Employee 对象。随后我们对该右值对象调用 getName(),该方法返回对 m_name 的引用。此引用随即被用于将姓名打印至控制台。此时包含 createEmployee(“Frank”) 调用的完整表达式结束,右值对象及其成员被销毁。由于右值对象及其成员在此之后均未被使用,此情况无误。

在情况2中,问题开始显现。首先,createEmployee(“Garbo”)返回一个右值对象。接着我们调用getName()获取该右值对象的m_name成员引用,并用此引用初始化ref。此时包含createEmployee(“Garbo”)调用的完整表达式结束,右值对象及其成员被销毁。这导致 ref 成为悬空引用。因此后续语句使用 ref 时,实际上访问的是悬空引用,将引发未定义行为。

关键要点:
完整表达式的求值在该表达式作为初始化器使用后即告终止。这使得对象能够使用同类型右值进行初始化(因右值在初始化完成前不会被销毁)。


安全使用按引用返回的成员函数

尽管右值隐式对象存在潜在风险,但按惯例,获取器应通过常量引用(而非值)返回难以复制的类型。

基于此,我们来探讨如何安全使用此类函数的返回值。上例中的三种情况阐明了三个关键要点:

  • 优先立即使用成员函数的返回值(如情况1所示)。由于此方法同时适用于左值和右值对象,始终遵循此原则可规避风险。
  • 切勿“保存”返回的引用以备后用(如情况2所示),除非能确定隐式对象为左值。若对右值隐含对象操作,使用悬空引用时将导致未定义行为。
  • 若确需保存返回引用供后续使用且无法确定隐含对象为左值,可将返回引用作为非引用局部变量的初始化表达式(如案例3所示),此操作会将被引用的成员复制到局部变量中。

最佳实践:
优先采用立即返回引用类型的成员函数返回值,以避免隐式对象为右值时悬空引用的问题。


不要返回指向私有数据成员的非 const 引用

由于引用与被引用的对象行为完全一致,返回非 const 引用的成员函数将直接暴露该成员(即使该成员为私有)。

例如:

#include <iostream>

class Foo
{
private:
    int m_value{ 4 }; // private member

public:
    int& value() { return m_value; } // returns a non-const reference (don't do this)
};

int main()
{
    Foo f{};                // f.m_value is initialized to default value 4
    f.value() = 5;          // The equivalent of m_value = 5
    std::cout << f.value(); // prints 5

    return 0;
}

image

由于 value() 返回对 m_value 的非 const 引用,调用方能够使用该引用直接访问(并修改)m_value 的值。

这使得调用方能够绕过访问控制系统。

常量成员函数不能返回对数据成员的非 const 引用

常量成员函数不允许返回对成员的非 const 引用。这符合逻辑——常量成员函数既不能修改对象状态,也不能调用会改变对象状态的函数。它不应执行任何可能导致对象被修改的操作。

若允许常量成员函数返回成员的非常量引用,等于向调用方提供了直接修改该成员的途径,这违背了常量成员函数的设计初衷。

posted @ 2025-12-28 13:35  游翔  阅读(28)  评论(0)    收藏  举报