何时使用花括号初始化?

114

在C++11中,我们有了初始化类的新语法,它为我们提供了许多初始化变量的可能性。

{ // Example 1
  int b(1);
  int a{1};
  int c = 1;
  int d = {1};
}
{ // Example 2
  std::complex<double> b(3,4);
  std::complex<double> a{3,4};
  std::complex<double> c = {3,4};
  auto d = std::complex<double>(3,4);
  auto e = std::complex<double>{3,4};
}
{ // Example 3
  std::string a(3,'x');
  std::string b{3,'x'}; // oops
}
{ // Example 4
  std::function<int(int,int)> a(std::plus<int>());
  std::function<int(int,int)> b{std::plus<int>()};
}
{ // Example 5
  std::unique_ptr<int> a(new int(5));
  std::unique_ptr<int> b{new int(5)};
}
{ // Example 6
  std::locale::global(std::locale("")); // copied from 22.4.8.3
  std::locale::global(std::locale{""});
}
{ // Example 7
  std::default_random_engine a {}; // Stroustrup's FAQ
  std::default_random_engine b;
}
{ // Example 8
  duration<long> a = 5; // Stroustrup's FAQ too
  duration<long> b(5);
  duration<long> c {5};
}

每次我声明一个变量,都要考虑使用哪种初始化语法,这会减慢我的编码速度。我相信引入花括号的初衷不是这样。

对于模板代码,更改语法可能会导致不同的含义,因此选择正确的方式是至关重要的。

我想知道是否有一种通用指南可以指导我应该选择哪种语法。


1
一个来自{}初始化的意外行为的例子:string(50, 'x') vs string{50, 'x'} 在这里 - P i
3个回答

72

我认为以下内容可能是一个好的指导方针:

  • 如果你要初始化的(单个)值预期是对象的确切值,使用复制(=)初始化(因为这样,在出现错误时,你永远不会意外地调用显式构造函数,通常会以不同方式解释提供的值)。在复制初始化不可用的地方,看看花括号初始化是否具有正确的语义,如果是,就使用它;否则使用圆括号初始化(如果这也不可用,你将没有办法)。

  • 如果你要初始化的值是一组要存储在对象中的值(例如向量/数组的元素或复数的实部/虚部),如果可用,则使用花括号初始化。

  • 如果你要初始化的值不是要存储的值,而是描述对象的预期值/状态,请使用圆括号。例如,vector的大小参数或fstream的文件名参数。


4
“locale”不是一个字符串,所以你不能使用复制初始化。同时,“locale”也不包含字符串(它可能将该字符串作为实现细节存储,但这不是其目的),因此不应使用花括号初始化。因此,指南建议使用圆括号初始化。 - celtschk
2
我个人最喜欢这个指南,它也适用于通用代码。有一些例外情况(如T {}或语法原因,如最难懂的解析),但总体而言,我认为这是一个好建议。请注意,这是我的主观意见,因此应该查看其他答案。 - helami
2
@celtschk:对于不可复制、不可移动的类型,那样做行不通;type var{};可以。 - ildjarn
2
@celtschk:我不是说这是一个经常发生的事情,但这样做可以减少输入量并在更多的情况下起作用,那么有什么缺点呢? - ildjarn
2
我的指南从来不需要复制初始化。;-] - ildjarn
显示剩余10条评论

29

我相信永远不会有一个通用的指南。我的方法是始终使用花括号记住以下几点:

  1. 初始化器列表构造函数优先于其他构造函数
  2. 所有标准库容器和std::basic_string都有初始化器列表构造函数。
  3. 花括号初始化不允许缩小转换。

因此,圆括号和花括号不能互换使用。但知道它们之间的区别让我在大多数情况下使用花括号初始化(其中一些我无法使用的情况目前是编译器错误)。


7
花括号的缺点在于我可能会错误地调用列表构造器,而圆括号则没有这个问题。这难道不是默认使用圆括号的理由吗? - helami
4
关于 int i = 0; 这行代码,我认为没有人会使用 int i{0},这可能会让人感到困惑(此外,0int 类型,因此不会发生 窄化)。对于其它的情况,我会遵循 Juancho 的建议:尽量使用 {},但要注意少数情况下不需要使用。需要注意的是,并不是所有类型都能够接受初始化列表作为构造函数参数,你可以期望容器和类似容器的类型(例如元组)会有这种构造函数,但大多数代码都会调用适当的构造函数。 - David Rodríguez - dribeas
3
这取决于你是否在意精度缩小问题。我很在意,因此我更喜欢编译器告诉我 int i{some floating point} 是错误的,而不是默默地截断。 - juanchopanza
3
关于“prefer {},但要注意少数情况下不适用”的问题:假设两个类具有语义上等效的构造函数,但其中一个类还有初始化列表。这两个等效的构造函数应该以不同的方式被调用吗? - helami
3
如果两个类有语义上等价的构造函数,但其中一个类还有初始化列表,那么这两个等价的构造函数应该被称为不同的吗?如果我遇到了最棘手的解析问题,这可能发生在任何实例的任何构造函数上。如果您只使用 {} 来表示“初始化”,除非您绝对不能这样做,否则避免这种情况会更容易些。 - Nicol Bolas
显示剩余6条评论

17
除了通用代码(例如模板)之外,您可以在任何地方使用大括号(我也是这样做的)。一个优点是它可以在任何地方使用,例如在类内初始化时也可以使用。
struct foo {
    // Ok
    std::string a = { "foo" };

    // Also ok
    std::string b { "bar" };

    // Not possible
    std::string c("qux");

    // For completeness this is possible
    std::string d = "baz";
};

或用于函数参数:
void foo(std::pair<int, double*>);
foo({ 42, nullptr });
// Not possible with parentheses without spelling out the type:
foo(std::pair<int, double*>(42, nullptr));

对于变量,我并不太关注使用T t = { init };还是T t { init };的形式,我认为它们之间的区别微乎其微,最坏的情况只会导致编译器提示有关错误使用explicit构造函数的信息。
对于接受std::initializer_list的类型,非std::initializer_list构造函数显然有时是必需的(经典示例是std::vector<int> twenty_answers(20, 42);)。那么就可以不使用大括号。
当涉及到通用代码(即模板)时,上面的最后一段应该引起一些警惕。考虑以下内容:
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{ return std::unique_ptr<T> { new T { std::forward<Args>(args)... } }; }

然后,auto p = make_unique<std::vector<T>>(20, T {});如果Tint,则创建大小为2的向量;如果Tstd::string,则创建大小为20的向量。这里非常明显存在问题,而且没有任何特征可以帮你解决(例如SFINAE):std::is_constructible 是基于直接初始化,而我们使用大括号初始化,它只有在没有接受std::initializer_list的构造函数干扰时才会转换为直接初始化。同样,std::is_convertible也无法帮助。

我已经调查过是否可能手动编写一个特征来解决这个问题,但我对此并不是非常乐观。无论如何,我认为我们不会错过太多,因为make_unique<T>(foo, bar)的结果相当于T(foo, bar)的构造,这非常直观;尤其是考虑到make_unique<T>({ foo, bar })非常不同,只有在foobar具有相同类型时才有意义。

因此,对于通用代码,我仅使用大括号进行值初始化(例如T t {};T t = {};),这非常方便,我认为优于C++03的方式T t = T();。否则,就是直接初始化语法(即T t(a0, a1, a2);),或者有时是默认构造(我认为唯一使用它的情况是T t; stream >> t;)。

这并不意味着所有大括号都是不好的,考虑前面的例子:

template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{ return std::unique_ptr<T> { new T(std::forward<Args>(args)...) }; }

即使实际类型取决于模板参数T,仍然使用大括号来构建std::unique_ptr<T>


@interjay 我的一些示例确实需要使用无符号类型,例如 make_unique<T>(20u, T {}),其中 T 可以是 unsignedstd::string。对于细节不太确定。(请注意,我还就直接初始化与花括号初始化在完美转发函数方面的期望进行了评论。)std::string c("qux"); 没有被指定为类内初始化,以避免语法中成员函数声明的歧义。 - Luc Danton
@interjay,我不同意你的第一点观点,可以查看8.5.4列表初始化和13.3.1.7通过列表初始化进行初始化。至于第二个问题,您需要仔细查看我写的内容(涉及类内初始化)和/或C++语法(例如member-declarator,它引用brace-or-equal-initializer)。 - Luc Danton
嗯,你说得对 - 我之前正在使用GCC 4.5 进行测试,那似乎证实了我的说法,但是GCC 4.6 确实支持你的观点。而且我确实错过了你谈论的是类内初始化这一事实。抱歉。 - interjay

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