C++中operator->如何保持值类别?是否存在类型系统漏洞?
C++中operator->的值类别转发问题
假设有结构体Bar和Foo,其中Foo重载的operator*可以在转发成员访问时保持值类别:
#include <cstdio> struct Bar { void func() & { printf("func() &\n"); } void func() && { printf("func() &&\n"); } }; struct Foo { Bar bar; Bar &operator*() & { return bar; } Bar &&operator*() && { return static_cast<Bar &&>(bar); } }; int main() { Foo foo; (*foo).func(); // 输出 func() & (*Foo()).func(); // 输出 func() && }
为简化func()调用,给Foo添加operator->:
struct Foo { Bar bar; Bar &operator*() & { return bar; } Bar &&operator*() && { return static_cast<Bar &&>(bar); } Bar *operator->() & { return &bar; } // 本应返回“指向右值Bar的指针”,但C++无此概念 Bar *operator->() && { return &bar; } };
但C++中不存在左值/右值指针的概念,导致以下代码均调用左值版本func() &:
int main() { Foo foo; foo->func(); // 输出 func() & Foo()->func(); // 输出 func() & }
针对这一现象,提出两个技术问题:
- 能否让
operator->像operator*一样保持值类别,同时维持现有调用语法? - 该现象是否属于C++类型系统的漏洞?
问题1解答:可以通过代理对象实现
要让operator->保持值类别转发且不修改调用语法,核心思路是让operator->返回代理对象而非原生指针,利用代理对象的引用限定符(&/&&)来控制最终成员访问的表达式值类别。
具体实现如下:
#include <cstdio> #include <utility> // for std::move struct Bar { void func() & { printf("func() &\n"); } void func() && { printf("func() &&\n"); } }; struct Foo { Bar bar; Bar &operator*() & { return bar; } Bar &&operator*() && { return std::move(bar); } // 代理类:用于转发值类别 struct ArrowProxy { Bar& bar_ref; // 递归返回自身,触发operator*的调用 ArrowProxy* operator->() { return this; } // 根据代理对象的值类别返回对应引用 Bar& operator*() & { return bar_ref; } Bar&& operator*() && { return std::move(bar_ref); } }; // 左值Foo返回左值代理 ArrowProxy operator->() & { return {bar}; } // 右值Foo返回右值代理 ArrowProxy operator->() && { return {bar}; } }; int main() { Foo foo; foo->func(); // 输出 func() & Foo()->func(); // 输出 func() && }
原理说明
C++中E->M的语义是递归调用operator->直到得到指针,最终等价于(*X).M(X是最终的指针或代理对象)。上述实现中:
- 左值
Foo调用operator->返回左值ArrowProxy,最终调用operator*&返回Bar&,触发左值版本func(); - 右值
Foo调用operator->返回右值ArrowProxy,最终调用operator*&&返回Bar&&,触发右值版本func()。
整个过程完全保留foo->func()的调用语法,同时实现了值类别转发。
问题2解答:不属于类型系统漏洞,是设计权衡的结果
该现象并非C++类型系统的漏洞,而是标准委员会的设计取舍:
- 原生指针的语义一致性:C++的指针核心语义是“指向内存中的对象”,通过指针访问成员时,
*指针的结果始终是左值——这是为了与原生指针的行为保持一致,避免引入“左值指针/右值指针”这类复杂概念,降低语言学习和实现成本。 - 明确的替代方案:标准库提供了
operator*作为值类别转发的明确途径,而代理模式也能在需要时扩展operator->的能力,无需修改语言核心类型系统。
类似std::optional的容器也采用相同的operator->行为,这是为了维持与原生指针的行为对齐,保证API的一致性和可预测性。
内容的提问来源于stack exchange,提问作者geza
相关产品推荐
相关产品推荐

