初始化列表中的空括号魔法

14

考虑以下最简单的例子:

#include <iostream>

struct X {
  X() { std::cout << "Default-ctor" << std::endl; }
  X(std::initializer_list<int> l) { 
      std::cout << "Ilist-ctor: " << l.size() << std::endl; 
  }
};

int main() {
    X a{};
    X b({}); // reads as construct from {}
    X c{{}}; // reads as construct from {0}
    X d{{{}}}; // reads as construct from what?
    // X e{{{{}}}}; // fails as expected
}

Godbolt示例

关于a、b和c,我没有问题,一切都相当清楚

但我不明白为什么d会起作用

这个额外的大括号在d中代表什么?我查阅了C++20标准,但很难找到答案。clang和gcc都同意这段代码,所以是我自己错过了什么。


我不确定,但它可能是从另一个通过内部大括号初始化的临时对象复制/移动而来(当然,由于C++17的规则,实际上不会是复制/移动)。 - chris
@chris 我使用了C++14和-fno-elide-constructors运行它,没有调用任何构造函数。 - Kostas
2个回答

19

了解编译器的工作情况的一个好方法是使用所有错误编译:-Weverything。让我们看一下这里的输出(仅针对d):

9.cpp:16:6: warning: constructor call from initializer list is incompatible with C++98                                                                                            
      [-Wc++98-compat]                                                                                                                                                            
  X d{{{}}}; // reads as construct from what?                                                                                                                                     
     ^~~~~~                           

X::X(std::initializer_list) 被调用。

9.cpp:16:8: warning: scalar initialized from empty initializer list is incompatible with                                                                                          
      C++98 [-Wc++98-compat]                                                                                                                                                      
  X d{{{}}}; // reads as construct from what?                                                                                                                                     
       ^~                               

在内部 {} 初始化的标量 (int)。 因此我们有X d{{0}}

9.cpp:16:7: warning: initialization of initializer_list object is incompatible with                                                                                               
      C++98 [-Wc++98-compat]                                                                                                                                                      
  X d{{{}}}; // reads as construct from what?                                                                                                                                     
      ^~~~                                                                                                                                                                        
5 warnings generated.                                                                                                                                                             

std::initializer_list{0}初始化。 因此我们有X d{std::initializer_list<int>{0}};

这显示了我们需要的一切。 额外的括号用于构造初始化列表。

注意:如果您想添加额外的括号,可以通过调用复制/移动构造函数(或省略它)来实现,但是C++编译器不会隐式地为您执行此操作,以防止出错:

X d{X{{{}}}}; // OK
X e{{{{}}}}; // ERROR

12

我想简单地解释一下:

X d{               {                       {}        }};
   |               |                       |
   construct an    |                       |
   `X` from ...    an initializer_list     |
                   containing...           int{}

列表初始化的规则是尽可能地找到一个initializer_list<T> 构造函数并使用它,否则...枚举构造函数并执行正常操作。

对于X{{}},这是列表初始化:最外层的{}initializer_list,其中包含一个元素:{},它是0。 这很简单直接(虽然密秘)。

但是,对于X{{{}}},使用最外层的{}作为initializer_list不再起作用,因为您无法从{{}}初始化int。 因此,我们回退到使用构造函数。 现在,其中一个构造函数需要一个initializer_list,所以有点像重新开始,除了我们已经剥离了一层大括号。


这就是为什么例如vector<int>{{1, 2, 3}}也可以工作,而不仅仅是vector<int>{1, 2, 3}。 但是,像这样...不要这样做。


谢谢,我已经接受了第一个答案,但这个解释也很好。顺便说一下,我发现了一个更有趣的花括号疯狂示例:std::vectorstd::string vs = {{{{{}}}}};根据您的解释,这里使用统一初始化 {v},从 {{}} 构造字符串和从 {{s}} 构造向量,哇... - Konstantin Vladimirov

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