【问题标题】:C++ implicit conversions with brace initializers带有大括号初始值设定项的 C++ 隐式转换
【发布时间】:2016-05-27 11:40:01
【问题描述】:

我最近在某处(不记得在哪里)读到关于使用大括号来允许多个用户定义的转换,但是我不明白的构造函数转换和转换方法转换之间似乎存在差异。

考虑:

#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&lt;&lt;({{ "char *" }}); 它工作正常。

更新 2

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

【问题讨论】:

  • 初始化使用构造函数,并且不会使用中间类型的转换运算符。这两个非工作示例失败了,因为隐式构造 A 以使用其 operator C 违背了这一点。
  • 彼得,我想了解的是规则到底是什么。如果我写 c = A{...c = {A{... 它可以通过转换方法正常工作。如果我放弃A,为什么它决定只使用ctors?
  • 我希望你知道在任何代码中包含任何一个月内不会被放弃的东西是一个相当糟糕的主意。知道编译器在做什么总是比让它发狂要好。
  • 嗯什么。为什么您希望 C 隐式转换为不相关的类 A?

标签: c++ c++11 implicit-conversion list-initialization


【解决方案1】:

可能有一个用户转化。

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.

【讨论】:

  • @Ari:它会做c = C{A{std::string{"char*"}}}。只有A->C 转换。
  • 但这就是问题所在,不是吗?为什么在删除 A 时不会发生这种情况?我以为您是在说 C{{ 不起作用,因为 C 没有将 A 作为参数的 ctor。显然,C{A 或只是{A 不是在构造,而是通过A 的方法进行转换。为什么不说A就不会发生这种情况?
  • 没有采用AC构造函数,所以{{"char*"}}不能推导出为A类型。
  • @Jarod42 Ari 询问 为什么 当有可用的转换运算符时,它仅限于 ctor。您的回答只是重复了他在问题中所说的内容。
【解决方案2】:

深入研究标准的第 8.5.4 节并遵循其中的各种交叉引用,我想我明白发生了什么。当然,IANAL,所以我可能错了;这是我最大的努力。

更新:以前版本的答案实际上使用了多次转换。我已经更新了它以反映我目前的理解。

解开混乱的关键是花括号初始化列表不是表达式(这也解释了为什么d &lt;&lt; {{"char *"}} 无法编译)。它是特殊的语法,受特殊规则的约束,在许多特定的上下文中是允许的。在这些上下文中,与我们讨论相关的是:赋值的 rhs、函数调用中的参数和构造函数调用中的参数。

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

braced-init-list 可能出现在...的右侧,由用户定义的赋值运算符定义的赋值,在这种情况下传递初始化列表 作为运算符函数的参数。

(大概,默认的复制赋值运算符被认为是用户定义的赋值运算符。我在任何地方都找不到该术语的定义,而且似乎没有任何语言允许专门用于默认复制分配的大括号语法。)

所以我们被简化为将参数传递给默认的复制赋值运算符B::operator=(const B&amp;),其中传递的参数是{{"char *"}}。因为花括号初始化列表不是表达式,所以这里不存在转换问题,而是B 类型的临时初始化的一种形式,具体而言,所谓的列表初始化

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

因此,编译器会去掉外面的大括号对,并使用{"char *"} 作为参数执行重载解析。这成功了,匹配构造函数B::B(const A&amp;),因为再次有一个临时类型A 的列表初始化,其中重载解析成功匹配A::A(const string&amp;) 的参数"char *",这可以通过一个分配的用户定义转换,即从char*string

现在,在c = {{"char *"}} 的情况下,过程类似,但是当我们尝试使用{{"char *"}}C 类型的临时对象进行列表初始化时,重载解析无法找到匹配的构造函数。关键是,根据定义,列表初始化只能通过一个构造函数起作用,该构造函数的参数列表可以匹配列表的内容。

【讨论】:

    猜你喜欢
    • 2021-11-21
    • 2014-05-13
    • 1970-01-01
    • 1970-01-01
    • 2011-05-31
    • 2016-07-25
    • 1970-01-01
    • 1970-01-01
    • 2018-09-06
    相关资源
    最近更新 更多