C++中使用花括号初始化的隐式转换

11

我最近在某个地方(无法记起来)读到了使用大括号允许多个用户定义的转换,但是构造函数转换和转换方法转换之间似乎存在一些我不理解的区别。

考虑:

#include <string>

using ::std::string;

struct C {
  C() {}
};

struct A {
  A(const string& s) {}  // Make std::string convertible to A.
  operator C() const { return C(); }  // Makes A convertible to C.
};

struct B {
  B() {}
  B(const A& a) {}  // Makes A convertible to B.
};

int main() {
  B b;
  C c;

  // This works.
  // Conversion chain (all thru ctors): char* -> string -> A -> B
  b = {{"char *"}};

  // These two attempts to make the final conversion through A's
  // conversion method yield compiler errors.
  c = {{"char *"}};
  c = {{{"char *"}}};
  // On the other hand, this does work (not surprisingly).
  c = A{"char *"};
}

现在,我可能误解了编译器的工作,但根据上述和额外的实验,我认为它没有考虑通过转换方法进行的转换。然而,在阅读标准的第4部分和13.3.3.1节时,我找不到原因的线索。这是什么解释呢?

更新

这里有另一个我想要解释的有趣现象。如果我添加

struct D {
  void operator<<(const B& b) {}
};

并且在main函数中:

  D d;
  d << {{ "char *" }};

我遇到一个错误,但如果我写成d.operator<<({{ "char *" }}); 就可以正常工作。

更新2

看起来标准中的第8.5.4节可能会提供一些答案。我会报告我的发现。


一个初始化使用构造函数,不会使用中间类型的转换运算符。两个不起作用的示例失败,因为隐式构造A以使用其operator C违反了这一点。 - Peter
相关问题:https://dev59.com/DFsV5IYBdhLWcg3wyxZ_#35790867 - user2249683
彼得,我想要理解的是规则到底是什么。如果我写 c = A{... 或者 c = {A{...,通过转换方法都可以正常工作。为什么如果我去掉 A,它就决定只使用构造函数呢? - Ari
我希望你知道,在任何不会在一个月内被放弃的代码中使用这种东西都是一个相当糟糕的主意。最好始终知道编译器正在做什么,而不是让它胡乱操作。 - Francesco Dondi
1
嗯,为什么你期望C会隐式转换成一个无关的类A呢? - uh oh somebody needs a pupper
2个回答

7

有一个用户转换可能。

b = {{"char *"}};中,我们实际上执行了

b = B{{"char*"}}; // B has constructor with A (and a copy constructor not viable here)

所以
b = B{A{"char*"}}; // One implicit conversion const char* -> std::string

c = {{"const char*"}}中,我们尝试
c = C{{"char *"}}; // but nothing to construct here.

1
@Ari:它将执行 c = C{A{std::string{"char*"}}}。这只是AC的转换。 - Jarod42
但这就是问题,不是吗?当删除A时,为什么不会发生这种情况?我以为你是说C{{不起作用,因为C没有一个以A为参数的构造函数。显然,C{A或者只有{A并没有构造,而是通过A的方法进行转换。为什么不在没有提到A的情况下发生这种情况呢? - Ari
1
没有接受 A 作为参数的 C++ 构造函数,所以 {{"char*"}} 不能被推导为类型 A - Jarod42
1
@Jarod42 Ari问为什么只限于ctor,而有一个转换运算符可用。你的答案只是重申了他在问题中陈述的内容。 - kfsone

1

通过查找标准的第8.5.4节并跟踪其中的各种交叉引用,我认为我知道正在发生什么。当然,我不是律师,所以我可能错了;这是我最好的努力。

更新:先前版本的答案实际上使用了多个转换。我已更新以反映我的当前理解。

解开混乱的关键是一个花括号初始化列表不是表达式(这也解释了为什么d << {{"char *"}}不能编译)。它是特殊语法,受特殊规则支配,在许多特定上下文中允许使用。其中,我们讨论的相关上下文是:赋值的右侧,在函数调用中作为参数,在构造函数调用中作为参数。

那么,当编译器看到b = {{"char *"}}时会发生什么?这是一种赋值的情况。适用的规则是:

在用户定义的赋值运算符中定义的赋值右侧可以出现花括号初始化列表,在这种情况下,初始化列表作为操作函数的参数传递。

(默认的复制赋值运算符被认为是一个用户定义的赋值运算符。我无法在任何地方找到该术语的定义,并且似乎没有任何语言专门允许使用大括号语法来进行默认复制赋值。)

因此,我们只能通过参数传递给默认的复制赋值运算符B::operator=(const B&),其中传递的参数是{{"char *"}}。由于大括号初始化列表不是表达式,因此这里不存在转换问题,而是一种特定类型的临时变量初始化,即所谓的列表初始化

如果找不到可行的初始化器列表构造函数,则再次执行重载解析,其中候选函数是类T的所有构造函数,参数列表由初始化器列表的元素组成。

所以编译器剥去外层的花括号,使用`{"char *"}`作为参数执行重载解析。这成功匹配构造函数`B::B(const A&)`,因为在该临时类型的列表初始化中,重载解析再次成功匹配参数`"char *"`的`A::A(const string&)`构造函数,这是通过一种指定的用户定义转换完成的,即从`char*`到`string`。
现在,在`c = {{"char *"}}`的情况下,过程类似,但是当我们尝试使用`{{"char *"}}`列表初始化类型为`C`的临时变量时,重载解析无法找到匹配的构造函数。关键是按定义,只有参数列表可以与列表内容相匹配的构造函数才能通过列表初始化工作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接