跳转到内容
新建笔记

C++ const 成员函数与返回引用:访问路径和对象寿命

const MachineTree& getMachineTree(); 与 MachineTree& getMachineTree() const; 中,两个 const 限制的是不同的访问路径。前者限制返回的引用,后者限制函数访问调用对象的方式。不能只交换位置,就认为可以继续返回同一个普通数据成员。

本文使用 C++17,延续 WorkingTemplate 持有 MachineTree 的原例。

先分别看返回类型和成员函数资格

跳转到“先分别看返回类型和成员函数资格”
声明谁能调用返回后允许什么
const MachineTree& getMachineTree();非 const 对象通过返回引用只读;函数自身仍可改变普通成员
MachineTree& getMachineTree() const;const 或非 const 对象返回可写引用,但实现必须有合法的可写对象来源
MachineTree& getMachineTree();非 const 对象常用于开放内部对象的读写访问
const MachineTree& getMachineTree() const;const 或非 const 对象常用于开放内部对象的只读访问

这些声明都假设函数可访问,且尚未加入引用限定符。在 WorkingTemplate 的 const 成员函数中,this 的类型是 const WorkingTemplate*;普通的非 mutable 成员 currentMachineTree 也通过 const 路径访问。C++17 的 this 指针规则

因此,原第二个程序的 MachineTree& getMachineTree() const { return currentMachineTree; } 不能编译:它试图把 const 成员访问结果绑定到可写引用。原笔记给出的“调用后成功修改”的输出不成立。

常用的接口是一对重载:非 const 对象得到 MachineTree&,const 对象得到 const MachineTree&。它们按成员函数末尾的 const 区别;仅改变返回类型不能构成重载。

完整程序:成对访问器、只读返回与外部借用

跳转到“完整程序:成对访问器、只读返回与外部借用”

下例额外用 & 限定访问器只能从左值拥有者取得引用,并删除两个右值重载,避免直接写 WorkingTemplate{}.getMachineTree()。这只是防住一种常见悬垂来源;拥有者以后仍可能被销毁,调用者必须遵守引用寿命。

#include <cassert>
#include <ostream>
#include <sstream>
#include <string>
#include <type_traits>
#include <utility>
class MachineTree {
public:
void print(std::ostream& out) const { out << "Machine Tree Data"; }
void modify() { ++revision_; }
int revision() const { return revision_; }
private:
int revision_ = 0;
};
class WorkingTemplate {
public:
MachineTree& getMachineTree() & { return currentMachineTree_; }
const MachineTree& getMachineTree() const & { return currentMachineTree_; }
MachineTree& getMachineTree() && = delete;
const MachineTree& getMachineTree() const && = delete;
const MachineTree& readAndCount() & {
++reads_;
return currentMachineTree_;
}
int reads() const { return reads_; }
private:
MachineTree currentMachineTree_;
int reads_ = 0;
};
class BorrowedTree {
public:
explicit BorrowedTree(MachineTree& tree) : tree_(&tree) {}
MachineTree& getMachineTree() const { return *tree_; }
private:
MachineTree* tree_; // 不拥有对象;被借用的 tree 必须仍然存活
};
class QueryCounter {
public:
int query() const { ++calls_; return 42; }
int calls() const { return calls_; }
private:
mutable int calls_ = 0;
};
int main() {
static_assert(std::is_same_v<
decltype(std::declval<WorkingTemplate&>().getMachineTree()), MachineTree&>);
static_assert(std::is_same_v<
decltype(std::declval<const WorkingTemplate&>().getMachineTree()),
const MachineTree&>);
WorkingTemplate owner;
owner.getMachineTree().modify();
const WorkingTemplate& readOnlyOwner = owner;
const MachineTree& readOnlyTree = readOnlyOwner.getMachineTree();
assert(readOnlyTree.revision() == 1);
assert(&readOnlyTree == &owner.getMachineTree());
assert(&owner.readAndCount() == &readOnlyTree);
assert(owner.reads() == 1); // 返回只读引用不妨碍非 const 函数修改自身
std::ostringstream out;
readOnlyTree.print(out);
assert(out.str() == "Machine Tree Data");
MachineTree external;
const BorrowedTree view{external};
view.getMachineTree().modify();
assert(external.revision() == 1);
const QueryCounter counter;
assert(counter.query() == 42);
assert(counter.calls() == 1);
}

BorrowedTree 展示了第二种声明何时可以合法实现:const 固定的是 view 自身的访问路径,指针指向的外部对象仍是可写的。它既不拥有外部对象,也不保证外部对象不变。引用成员也需要类似地区分被引用对象与拥有引用的对象。

mutable 成员允许在 const 对象中被修改,适合经过设计的缓存或统计等内部状态;它不是绕过所有接口约束的通行证,也不自动提供线程同步。mutable 的规则

返回引用还涉及生命周期

跳转到“返回引用还涉及生命周期”

const 不延长被返回对象的寿命。返回局部变量的引用、保存已经销毁的 WorkingTemplate 的成员引用,都可能悬垂。给拥有者增加引用限定符,可以限制调用来源;若调用者需要独立保存数据,则应考虑按值返回或显式建立副本。引用限定符与隐式对象参数

也不要用 const_cast 让原错误实现“通过”:如果底层实际对象就是 const,通过去掉 const 的路径修改它会产生未定义行为。即使对象本来可写,这样做也会破坏访问器传达的接口约定。原始成员只读、逻辑上只读、外部对象只读和线程安全,需要分别设计。