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 的路径修改它会产生未定义行为。即使对象本来可写,这样做也会破坏访问器传达的接口约定。原始成员只读、逻辑上只读、外部对象只读和线程安全,需要分别设计。