C++ 中转换运算符到值和常量引用之间的重载解析

发布于 2025-01-12 06:02:35 字数 744 浏览 1 评论 0原文

在下面的程序中,struct B 定义了两个转换运算符:to A 和 to const A&。然后从B-对象创建A-对象:

struct A {};

struct B {
  A a;
  B() = default;
  operator const A&() { return a; }
  operator A() { return a; }
};

int main() {
  (void) A(B{});
}

该程序

  • 被所有语言版本的MSVC接受。
  • 所有语言版本均被 GCC 拒绝。
  • 在 C++14 模式下被 Clang 拒绝,在 C++17 模式下被接受。 演示:https://gcc.godbolt.org/z/rKv589EG3

GCC错误消息是

error: call of overloaded 'A(B)' is ambiguous
note: candidate: 'constexpr A::A(const A&)'
note: candidate: 'constexpr A::A(A&&)'

哪个编译器就在这里吗?

In the following program struct B defines two conversion operators: to A and to const A&. Then A-object is created from B-object:

struct A {};

struct B {
  A a;
  B() = default;
  operator const A&() { return a; }
  operator A() { return a; }
};

int main() {
  (void) A(B{});
}

The program is

  • accepted by MSVC in all language versions.
  • rejected by GCC in all language versions.
  • rejected by Clang in C++14 mode and accepted in C++17 mode.
    Demo: https://gcc.godbolt.org/z/rKv589EG3

GCC error message is

error: call of overloaded 'A(B)' is ambiguous
note: candidate: 'constexpr A::A(const A&)'
note: candidate: 'constexpr A::A(A&&)'

Which compiler is right here?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

以歌曲疗慰 2025-01-19 06:02:35

实现差异可能与 CWG 2327 有关。

如果严格看C++20的措辞,那么GCC是正确的,并且重载决议是不明确的。我将首先详细讨论措辞,然后在答案的末尾我将再次讨论 CWG 2327。

初始化有两个候选:

A::A(const A&);
A::A(A&&);

第一步是确定调用每个候选所需的隐式转换序列:从“B 的右值”到 const A& 的 ICS ,以及从“B 的右值”到 A&& 的 ICS。不过,B 的值类别实际上并不相关,因为 B 中的转换函数都没有引用限定符

要从 B 转换为 const A&A&&,我们转到 [dcl.init.ref]。对于到 const A& 的转换,p5.1.2 适用:

对类型“cv1 T1”的引用由类型“cv2 T2”的表达式初始化如下:

  • 如果引用是左值引用和初始化表达式

    • [...]
    • 具有类类型(即,T2 是类类型),其中 T1T2 没有引用相关,并且可以转换为“cv3 T3”类型的左值,其中“cv1 T1”与引用兼容“cv3 T3”(此转换是通过枚举适用的转换函数 (12.4.2.7) 并通过重载解析 (12.4) 选择最佳函数来选择的),

    然后,在第一种情况下,该引用绑定到初始化表达式左值,在第二种情况下,该引用绑定到转换的左值结果(或者在任何一种情况下,绑定到该对象的相应基类子对象)。

这是因为 B 可以转换为 const A 类型的左值(这是 T3),并且 const A code>(如 T1)与 const A(如 T3)引用兼容。

要从 B 转换为 A&&,适用的规则是 p5.3.2,它非常相似,只是这次我们只寻找产生某种类型 T3 的右值。

12.4.2.7,又名 [over.match.ref] 解释了如何查找候选转换函数:

[...] 那些未隐藏在 S 中并产生类型“对 cv2 的左值引用 T2 的非显式转换函数”(初始化左值引用或对函数的右值引用时)或“cv2 T2”或“对 cv2 T2 的右值引用”(初始化时右值引用或对函数的左值引用),其中“cv1 T”与“cv2 ”引用兼容 (9.4.4) >T2”,是候选函数。 [...]

当初始化const A&时,显然operator const A&()是候选者之一。 运算符 A() 不是候选者,因为它不产生左值。 (const A& 可以从 A 右值初始化这一事实是无关紧要的;正如您从上面的措辞中看到的,没有特殊的大小写[over.match.ref] 中的 const 引用。)初始化 A&& 时,operator A() 是候选者,而 operator常量A&() 不是,因为它不产生右值。

因此,我们有以下隐式转换序列:

  • 对于候选 A::A(const A&),ICS 是 B{} 上的临时物化转换,然后是用户定义的转换 B::operator const A& 以及最后的恒等转换。
  • 对于候选 A::A(A&&),ICS 是 B{} 上的临时具体化转换,后跟用户定义的转换 B ::operator A 最后是身份转换。

对用户定义转换序列进行排序的基本规则[over.ics.rank]/3.3是,如果两个ICS使用相同的用户定义转换函数,则认为第二个标准转换序列更好的一个为整体更好的工业控制系统。此规则在这里不适用,因为两个转换函数不同。本节第 4 页中的决胜规则并不偏向其中之一。所以最后我们必须讨论[over.match.best.general]中的全局决胜规则。 p2.2 似乎可能相关:

  • 根据这些定义,如果对于所有参数,可行函数 F1 被定义为比另一个可行函数 F2 更好的函数 >i,ICSi(F1) 的转换序列并不比 ICSi(F2) 更差>),然后
  • 上下文是通过用户定义的转换(参见 9.4、12.4.2.6 和 12.4.2.7)以及从 F1 的返回类型到目标类型的标准转换序列进行的初始化(即,正在初始化的实体的类型)是比从 F2 返回类型到目标类型的标准转换序列更好的转换序列,或者,如果不是这样的话,[...]

但是,对措辞的正确理解表明,这些规则不会选择任何一个构造函数作为也比另一个好。尽管我们的重载决议涉及“子任务”重载决议来选择用户定义的转换函数,但这些子任务已经完成,我们不再处于用户定义的转换初始化的“上下文”中;我们正在选择最好的建造者。

因此,没有任何规则可用于选择任一构造函数而不是另一个构造函数;重载决策失败。

早期标准版本中似乎没有任何允许编译的措辞。

但如果您查看链接页面上对 CWG 2327 的讨论,您会发现 Richard Smith 建议对初始化规则进行更改。在当前规则下,由于 A 是类类型,因此重载决策始终涉及枚举 A 的构造函数并选择最佳候选者,我们上面讨论过,这可能涉及: “子任务”考虑从 BA 构造函数所需类型的转换函数。 Smith 非正式地提议将 B 的转换函数与 A 的构造函数一起考虑在顶层。然而,目前还没有建议的措辞来解释如何根据构造函数对此类转换函数进行排名。

如果存在三种可能的初始化候选,即

  • 调用 A::A(const A&) (其中 B{} 必须隐式转换为 const A& ;)
  • 调用 A::A(A&&) (其中 B{} 必须隐式转换为 A&&代码>)
  • 调用B::operator A 直接

那么第三个选项被认为比其他两个更好是合理的,我怀疑 Smith 已经提出了一些规则并在 Clang 中实现了它,但是我不确定它是什么。我相信,一旦他解决了措辞的所有情况,他就会为该问题添加措辞。如果确实如此,那么 Clang 仅在适用保证复制省略的 C++17 模式及更高版本中接受代码(通过调用运算符 A 而不是构造函数)是有意义的。至于 MSVC,也许他们有一个提议的解决方案,并决定将其应用到 C++14(也可能是 C++11)。

The implementation divergence is probably related to CWG 2327.

If look strictly at the wording of C++20, then GCC is right and the overload resolution is ambiguous. I'll go into the wording in detail first, and then at the end of the answer I'll discuss CWG 2327 again.

There are two candidates for the initialization:

A::A(const A&);
A::A(A&&);

The first step is to determine the implicit conversion sequence required to call each candidate: the ICS from "rvalue of B" to const A&, and the ICS from "rvalue of B" to A&&. The value category of the B is not actually relevant, though, because neither conversion function in B has a ref-qualifier.

To convert from B to const A& or to A&&, we go to [dcl.init.ref]. For the conversion to const A&, p5.1.2 applies:

A reference to type “cv1 T1” is initialized by an expression of type “cv2 T2” as follows:

  • If the reference is an lvalue reference and the initializer expression

    • [...]
    • has a class type (i.e., T2 is a class type), where T1 is not reference-related to T2, and can be converted to an lvalue of type “cv3 T3”, where “cv1 T1” is reference-compatible with “cv3 T3” (this conversion is selected by enumerating the applicable conversion functions (12.4.2.7) and choosing the best one through overload resolution (12.4)),

    then the reference is bound to the initializer expression lvalue in the first case and to the lvalue result of the conversion in the second case (or, in either case, to the appropriate base class subobject of the object).

This applies because B can be converted to an lvalue of type const A (this is the T3), and const A (as T1) is reference-compatible with const A (as T3).

To convert from B to A&&, the applicable rule is p5.3.2, which is very similar except that this time we are only looking for conversion functions that yield an rvalue of some type T3.

12.4.2.7, a.k.a. [over.match.ref] explains how to find the candidate conversion functions:

[...] Those non-explicit conversion functions that are not hidden within S and yield type “lvalue reference to cv2 T2” (when initializing an lvalue reference or an rvalue reference to function) or “cv2 T2” or “rvalue reference to cv2 T2” (when initializing an rvalue reference or an lvalue reference to function), where “cv1 T” is reference-compatible (9.4.4) with “cv2 T2”, are candidate functions. [...]

When initializing const A&, obviously operator const A&() is one of the candidates. operator A() is not a candidate since it doesn't yield an lvalue. (The fact that a const A& can be initialized from an A rvalue is irrelevant; as you can see from the wording above, there is no special casing for const references in [over.match.ref].) When initializing A&&, operator A() is a candidate, and operator const A&() is not, because it doesn't yield an rvalue.

Thus, we have the following implicit conversion sequences:

  • For the candidate A::A(const A&), the ICS is a temporary materialization conversion on B{} followed by the user-defined conversion B::operator const A& and finally an identity conversion.
  • For the candidate A::A(A&&), the ICS is a temporary materialization conversion on B{} followed by the user-defined conversion B::operator A and finally an identity conversion.

The basic rule for ranking user-defined conversion sequences, [over.ics.rank]/3.3, is that if two ICSes use the same user-defined conversion function, the one whose second standard conversion sequence is better is considered to be the overall better ICS. This rule doesn't apply here because the two conversion functions are different. The tie-breaker rules in p4 of this section do not prefer one over the other. So finally we have to go to the global tie-breaker rules in [over.match.best.general]. p2.2 seems like it might be relevant:

  • Given these definitions, a viable function F1 is defined to be a better function than another viable function F2 if for all arguments i, ICSi(F1) is not a worse conversion sequence than ICSi(F2), and then
  • the context is an initialization by user-defined conversion (see 9.4, 12.4.2.6, and 12.4.2.7) and the standard conversion sequence from the return type of F1 to the destination type (i.e., the type of the entity being initialized) is a better conversion sequence than the standard conversion sequence from the return type of F2 to the destination type or, if not that, [...]

However, a proper understanding of the wording reveals that these rules don't pick out either constructor as being better than the other, either. Although our overload resolution involved a "subtask" overload resolution to select user-defined conversion functions, those subtasks have already been completed and we are no longer in the "context" of an initialization by user-defined conversion; we are in the context of selecting the best constructor.

So there is no rule that can be used to select either constructor over the other; the overload resolution fails.

There does not seem to be any wording from earlier standard editions that would allow this to compile.

But if you look at the discussion of CWG 2327 on the linked page, you'll see that Richard Smith suggests that a change be made to the initialization rules. Under the current rules, since A is a class type, the overload resolution always involves enumerating constructors of A and picking the best candidate, which we discussed above, which might involve as "subtasks" the consideration of conversion functions from B to types required by A's constructors. Smith has informally proposed that the conversion functions of B be considered at the top level alongside the constructors of A. However, there is currently no proposed wording explaining how to rank such conversion functions against constructors.

If there are three possible candidates for the initialization, namely

  • call A::A(const A&) (where B{} must be implicitly converted to const A&)
  • call A::A(A&&) (where B{} must be implicitly converted to A&&)
  • call B::operator A directly

then it would be reasonable for the third option to be considered better than the other two, and I suspect that Smith has already come up with some rule and implemented it in Clang, but I'm not sure what it is. I'm sure he'll add wording to the issue once he's worked out all the cases for the wording. If this is indeed the case, then it makes sense that Clang accepts the code (by calling operator A instead of a constructor) only in C++17 mode and later, where guarantee copy elision applies. As for MSVC, perhaps they have a proposed resolution that they've decided to apply all the way back to C++14 (and probably C++11 as well).

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文