【问题标题】:List-initialization and failed overload resolution of initializer_list constructorinitializer_list 构造函数的列表初始化和重载解析失败
【发布时间】:2015-02-07 12:24:19
【问题描述】:

下面使用clang35 -std=c++11编译失败:

#include <iostream>
#include <string>
#include <initializer_list>

class A
{
 public:
  A(int, bool) { std::cout << __PRETTY_FUNCTION__ << std::endl; }
  A(int, double) { std::cout << __PRETTY_FUNCTION__ << std::endl; }
  A(std::initializer_list<int>) { std::cout << __PRETTY_FUNCTION__ << std::endl; }
};

int main()
{
  A a1 = {1, 1.0};
  return 0;
}

有错误

init.cpp:15:14: error: type 'double' cannot be narrowed to 'int' in initializer list [-Wc++11-narrowing]
  A a1 = {1, 1.0};
             ^~~
init.cpp:15:14: note: insert an explicit cast to silence this issue
  A a1 = {1, 1.0};
             ^~~
             static_cast<int>( )

OTOH,它会警告缩小并在 g++48 -std=c++11 上编译

init.cpp: In function ‘int main()’:
init.cpp:15:17: warning: narrowing conversion of ‘1.0e+0’ from ‘double’ to ‘int’ inside { } [-Wnarrowing]
   A a1 = {1, 1.0};
                 ^
init.cpp:15:17: warning: narrowing conversion of ‘1.0e+0’ from ‘double’ to ‘int’ inside { } [-Wnarrowing]

并产生结果

A::A(std::initializer_list<int>)

这两种行为都有意义吗?引用自cppreference

所有以 std::initializer_list 作为唯一参数的构造函数, 或者如果其余参数具有默认值,则作为第一个参数 值,被检查,并通过重载决议与 std::initializer_list 类型的单个参数

如果前一个阶段没有产生匹配,T的所有构造函数 参与针对一组参数的重载决议 由支撑初始化列表的元素组成,具有限制 只允许非缩小转换。如果这个阶段 生成一个显式构造函数作为 a 的最佳匹配 复制列表初始化,编译失败(注意,简单 复制初始化,完全不考虑显式构造函数)

由于不允许缩小转换,我希望重载解决步骤与A(std::initializer_list&lt;int&gt;) 构造函数不匹配,而是与A(int, double) 匹配。例如,将A(std::initializer_list&lt;int&gt;) 更改为A(std::initializer_list&lt;std::string&gt;) 会同时使用clang35g++48 进行编译并打印

A::A(int, double)

正如预期的那样。

【问题讨论】:

  • 大概你的意思是 Clang 3.5。你命名的二进制文件并没有真正的帮助:) mv clang25 clang35 "oops"
  • 你是对的 :) 这是团队在工作中维护构建系统时使用的版本控制约定,我从未考虑过。

标签: c++ c++11 overload-resolution list-initialization


【解决方案1】:

这种行为是有道理的。 Scott Meyers 在 Effective Modern C++ 中有一个几乎完全一样的例子(强调原文):

但是,如果一个或多个构造函数声明了std::initializer_list 类型的参数,则使用花括号初始化语法的调用强烈倾向于采用std;:initializer_lists 的重载。 强烈。如果编译器有任何方式将使用大括号初始化程序的调用解释为采用std::initializer_list 的构造函数,编译器将采用该解释。

使用此类的示例:

class Widget {
public:
    Widget(int, bool);
    Widget(int, double);
    Widget(std::initializer_list<long double>);
};

Widget w1(10, true); // calls first ctor
Widget w2{10, true}; // calls std::initializer_list ctor
Widget w3(10, 5.0); // calls second ctor
Widget w4{10, 5.0}; // calls std::initializer_list ctor

这两个调用调用initializer_list ctor,即使它们涉及转换两个参数 - 即使其他构造函数是完美匹配的。

此外:

编译器将花括号初始化器与采用std::initializer_lists 的构造函数匹配的决心是如此强烈,即使无法调用最佳匹配的std::initializer_list 构造函数,它也会占上风。例如:

class Widget {
public:
    Widget(int, bool); // as before
    Widget(int, double); // as before
    Widget(std::initializer_list<bool> ); // now bool
};

Widget w{10, 5.0}; // error! requires narrowing conversions

两个编译器都选择了正确的重载(initializer_list 之一)——我们可以看到这是标准要求的(第 13.3.1.7 节):

当非聚合类类型T 的对象被列表初始化(8.5.4)时,重载决议选择构造函数 分两个阶段:

(1.1) — 最初,候选函数是类 T 的初始化列表构造函数 (8.5.4) 和 参数列表由作为单个参数的初始值设定项列表组成。
(1.2) — 如果没有找到可行的初始化列表构造函数,则再次执行重载决议,其中 候选函数是类T的所有构造函数,参数列表由元素组成 初始化列表。

但是调用那个特定的构造函数涉及到缩小范围。在 8.5.1 中:

如果 initializer-clause 是一个表达式 并且需要缩小转换(8.5.4)来转换表达式,程序格式错误。

所以程序格式不正确。在这种情况下,clang 选择抛出错误,而 gcc 选择发出警告。两个编译器都符合要求。

【讨论】:

  • 哪种行为有意义? clang 和 gcc 不同意 - clang 3.5 无法编译,而 g++ 4.8.3 选择 initializer_list 重载,即使存在缩小转换。上面的示例没有说明任何困难 - 转换没有缩小,因此选择 initilalizer_list 重载也就不足为奇了。
  • 我们可能会补充一点,这个强烈的偏好很棒,因为这样你就可以例如std::vector&lt;std::string&gt; { "hello", "world" } 因为编译器努力尝试将初始化列表视为std::string 类型对象的初始化。否则我们将永远被困在写std::vector&lt;std::string&gt; { std::string{"hello"}, std::string{"world"} }想象一下当你尝试初始化某些东西时的痛苦less trivial
  • @Pradhan 从书中添加缩小示例
  • @Barry 两个编译器都符合要求。缩小使程序格式错误,这仅意味着编译器必须发出诊断; GCC 的警告满足了这个要求。
  • @T.C.我看到很多例子都明确表示// error: narrowing
猜你喜欢
  • 2015-09-04
  • 1970-01-01
  • 1970-01-01
  • 2012-03-19
  • 2018-06-20
  • 1970-01-01
  • 2012-04-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多