在 C++98 中,成员函数通过隐藏的 this 指针访问隐式对象,这就要求使用不同的重载来处理常量和非常量的上下文,而 C++11 引入了引用限定符以区分左值和右值对象。这可能需要每个函数提供四个重载来涵盖所有的 cv-ref 组合,从而导致显著的代码重复和对通用库的维护负担。
核心问题在于,当成员函数必须返回具有与调用者相同值类别和 cv-限定的对象时,以实现高效的移动语义或防止悬空引用。如果不能推断出对象的类型,开发者就会编写冗长的重载集或妥协于复制语义,导致对右值的处理效率低下或在通用代码中引发潜在的生命周期错误,进而传播对象引用。
C++23 引入了显式对象参数,允许语法 void foo(this auto&& self)。在这里,self 成为一个推断参数,捕获对象的值类别和 cv-限定,从而消除了单独的 & 和 && 重载的需要,因为 std::forward<decltype(self)>(self) 传播正确的类别。然而,静态成员函数完全缺乏隐式对象,因此将该语法应用于它们违反了将对象绑定到 self 的基本要求,导致程序根据标准不合格。
// C++23 之前:需要四个重载 class Builder { public: Builder& setName(...) & { /* ... */ return *this; } Builder const& setName(...) const& { /* ... */ return *this; } Builder&& setName(...) && { /* ... */ return std::move(*this); } Builder const&& setName(...) const&& { /* ... */ return std::move(*this); } }; // C++23:单个重载 class Builder { public: template<typename Self> auto setName(this Self&& self, ...) -> Self&& { // ... return std::forward<Self>(self); } };
我们的团队开发了一个高性能的 JSON 库,其中 DOM 节点支持方法链式调用以构建树,这要求 Node 类提供具有不同返回语义的 addChild() 方法。这些方法在父节点是左值时需要通过引用返回父节点,以允许进一步的变更,但在父节点是右值临时对象时,则通过值返回,以实现移动消除并防止意外修改正在过期的对象。
初始实现使用了传统的引用限定符重载。我们维护了四个版本的 addChild:一个返回 Node& 对于左值,一个返回 Node const& 对于常量左值,一个返回 Node&& 对于右值,以及一个返回 Node const&& 对于常量右值。这种方法满足了性能要求,但使我们的测试范围扩大了四倍,且出现了一个关键错误,其中 const&& 重载由于从 & 重载的复制粘贴错误错误地返回了悬空引用。
我们考虑完全放弃引用限定符,并始终通过值返回,依靠 RVO 来优化拷贝,但这对命名对象造成了不必要的移动,并破坏了与存储返回节点引用的现有代码的 API 兼容性。我们还评估了 CRTP,使用基类模板推断派生类型,但这向用户暴露了实现细节,并使继承层次结构复杂化,同时并未完全解决值类别传播的问题。
采用 C++23 显式对象参数使我们能将重载集合并为单个模板方法:template<typename Self> auto addChild(this Self&& self, ...) -> Self。这精确捕获了所需的值类别,实现了完美转发,而不需要在实现中冗余使用 std::move 或 std::forward,并将方法的圈复杂性减少到一条路径。最终,样板代码减少了 75%,并消除了与重载差异相关的错误类型。
为什么使用显式对象参数语法会阻止函数在参数列表之后有传统的 cv-限定符或引用限定符?
传统的成员函数在参数列表之后放置 cv-限定符和引用限定符,以修改隐式的 this 指针类型。使用显式对象参数时,this Self&& self 已在 Self 的类型推断中编码了 cv-限定符和引用类别。在参数列表之后附加 const 或 & 等额外限定符会试图限定一个不存在的隐式对象,从而产生类型系统矛盾。标准明文禁止这种组合,因为显式参数承担了参数和限定符的角色,允许两者同时出现将导致其语义明确性的歧义。
使用显式对象参数与传统成员函数时,函数体内的名称查找有何不同?
在传统的成员函数中,无限定名称查找会自动在类作用域中搜索,就好像在前面添加了 this->。在显式对象参数中,没有隐式的 this 指针;必须显式使用参数 self 来访问成员。候选人常常假设在 void foo(this auto& self) 中的 member 会自动解析为 this->member,但实际上需要 self. 限定或显式的类限定,如 ClassName::member。这改变了基本的查找规则,并在迁移代码时需要适应,尤其是在访问从派生类保护的成员时,self. 明确触发了针对推断类型的访问检查,而不是静态类类型。
显式对象参数是否可以参与虚函数重写,并且重写关系适用什么限制?
显式对象参数可以出现在虚函数中,但它们本质上改变了重写匹配规则。声明 virtual void bar(this Base& self) 的基类不能被声明 void bar(this Derived& self) 的派生类重写,即使传统重写允许协变返回类型。显式对象参数成为重写匹配目的的函数签名的一部分。由于 Base& 和 Derived& 是不同的类型,因此这不构成有效的重写。这防止了使用显式对象参数实现“sfinae友好”虚函数或在多态层次中保持类型的常见模式。要进行重写,派生函数必须准确匹配基类的显式参数类型,否定了重写上下文中该参数的推断好处。