指向成员的指针访问运算符的行为差异

ngm*_*r80 5 c++ pointer-to-member language-lawyer implicit-conversion

在 C++ 中,我正在寻找标准的关键部分\n解释我在该语言的两个指向成员的指针访问运算符之间观察到的行为的细微差别,.*以及->*

\n

根据下面显示的我的测试程序,虽然->*似乎允许其\n右手表达式是任何类型隐式转换为 \n pointer to member of S.*但事实并非如此。当使用 ngcc 和 clang 进行编译时,两个编译器都会对标记为 \'(2)\' 的行产生错误,表明我的类Offset不能用作成员指针。

\n

测试程序\n https://godbolt.org/z/46nMPvKxE

\n
#include <iostream>\n\nstruct S { int m; };\n\ntemplate<typename C, typename M>\nstruct Offset\n{\n    M C::* value;\n    operator M C::* () { return value; }  // implicit conversion function\n};\n\nint main()\n{\n    S s{42};\n    S* ps = &s;\n    Offset<S, int> offset{&S::m};\n\n    std::cout << ps->*offset << \'\\n\';  // (1) ok\n    std::cout << s.*offset << \'\\n\';    // (2) error\n    std::cout.flush();\n}\n
Run Code Online (Sandbox Code Playgroud)\n

编译器输出

\n
GCC 12.2:\n    \'offset\' cannot be used as a member pointer, since it is of type \'Offset<S, int>\'\nclang 15.0:\n    right hand operand to .* has non-pointer-to-member type \'Offset<S, int>\'\n
Run Code Online (Sandbox Code Playgroud)\n

程序变化

\n

为了证明上面显示的测试程序中->*确实使用\'s\nconversion 函数执行了隐式转换,我将其声明为显式用于测试目的,Offset

\n
    explicit operator M C::* () { return value; }  // no longer implicit conversion function\n
Run Code Online (Sandbox Code Playgroud)\n

导致编译器也会对标记为“(1)”的行产生错误:

\n
GCC 12.2:\n    error: no match for \'operator->*\' (operand types are \'S*\' and \'Offset<S, int>\')\n    note: candidate: \'operator->*(S*, int S::*)\' (built-in)\n    note:   no known conversion for argument 2 from \'Offset<S, int>\' to \'int S::*\'\nclang 15.0:\n    error: right hand operand to ->* has non-pointer-to-member type \'Offset<S, int>\'\n
Run Code Online (Sandbox Code Playgroud)\n

研究

\n

虽然这两个运算符之间存在有据可查的差异,\n->*是可重载的.*,\n 是不可重载的,但我的代码显然没有使用此选项\n,而是依赖于为原始operator ->*指针类型定义的内置选项S*

\n

除了重载性方面的差异之外,我只找到了说明表达式相似性的文档。引自标准(https://open-std.org/jtc1/sc22/wg21/docs/papers/2020/n4868.pdf):

\n
\n

[7.6.4.2] 二元运算符 .* 将其第二个操作数(该操作数的类型为 \xe2\x80\x9c 指向 T\xe2\x80\x9d 成员的指针)绑定到其第一个操作数(该操作数应为 T 类的左值)或 T 是明确且可访问的基类的类。结果是第二个操作数指定类型的对象或函数。

\n
\n
\n

[7.6.4.3] [...] 表达式 E1-> E2 被转换为等效形式 ( (E1)).*E2。

\n
\n

并引用自 cppreference.com ( https://en.cppreference.com/w/cpp/language/operator_member_access#Built-in_pointer-to-member_access_operators ):

\n
\n

两个运算符的第二个操作数都是指向 T 的成员(数据或函数)的指针类型的表达式,或指向 T 的明确且可访问的基类 B 的成员的指针。\n表达式 E1->*E2 完全等价于 (* E1).*E2 为内置型;这就是为什么以下规则仅针对 E1.*E2。

\n
\n

我在任何地方都没有找到右手操作数转换的概念。

\n

问题

\n

我忽略了什么?有人可以指出我对这种行为差异的解释吗?

\n

T.C*_*.C. 4

当使用可重载运算符并且至少一个操作数属于类或枚举类型时,重载决策将使用包含内置候选者 ( [over.match.oper]/3 ) 的候选集来执行 -->*具体请参见[over .build]/9 .

在本例中,选择了内置候选者,因此将隐式转换应用于第二个操作数,然后->*解释为内置运算符([over.match.oper]/11)。

对于.*,根本没有重载解析,因此也没有隐式转换。