总是使用{}初始化对象是一个好习惯吗?

3
使用new {}语法来初始化对象,像这样:
int a { 123 };

有益处——你不会错误地声明一个函数而不是创建一个变量。我甚至听说,这应该成为一种习惯。但看看可能会发生什么:

// I want to create vector with 5 ones in it:
std::vector<int> vi{ 5, 1 }; // ups we have vector with 5 and 1.

这是一个好习惯吗?有没有办法避免这样的问题?

3
是的,因为你禁用了窄化转换。 - 101010
8
等一下,这个不是有很多重复的吗? - Columbo
4
Scott Meyers在《Effective Modern C++》一书中专门介绍了这个问题。 - The Vivandiere
1
@Columbo:至少有一些近似的问题。例如,https://dev59.com/IF4c5IYBdhLWcg3wD2rC,https://dev59.com/JHrZa4cB1Zd3GeqP0kqz,只是其中的几个。 - Jerry Coffin
这个问题可能更适合Programmers.SE,实际上在那里已经有一个合理的重复问题:http://programmers.stackexchange.com/q/133688/89959 - Kyle Strand
2个回答

4

在适用的情况下,应该优先使用列表初始化来初始化对象,因为:

  1. 除了其他好处外,列表初始化限制了允许的隐式缩小转换。

    具体来说,它禁止:

    • 从浮点类型到整数类型的转换
    • long doubledoublefloat的转换和从doublefloat的转换,除非源是常量表达式并且不发生溢出。
    • 从整数类型到浮点类型的转换,除非源是一个可以精确存储在目标类型中的常量表达式。
    • 将整数或未作用域枚举类型转换为无法表示原始所有值的整数类型,除非源是一个可以精确存储在目标类型中的常量表达式。
  2. 另一个好处是它不会遭受最烦人的解析问题
  3. 此外,初始化列表构造函数优于其他可用的构造函数,除了默认构造函数。
  4. 此外,它们广泛可用,所有STL容器都有初始化列表构造函数。

现在关于你的示例,我会说:有了知识就有了力量。对于创建由5个1组成的向量(std::vector<int> vi( 5, 1);),有一个特定的构造函数。


2
你不觉得 std::vector<int> vi( 5, 1);std::vector<int> vi{ 5, 1}; 看起来几乎一模一样,但实际上却有着截然不同的作用吗?花括号初始化存在足够多的陷阱,以至于仅列出它的优点是没有帮助的。 - Kyle Strand
@KyleStrand 用刀子可以切面包,但在过程中也可能割伤手指,因此要学会正确使用刀子。每个功能都有其优点和缺点。在我看来,使用大括号初始化比缺点更多。 - 101010
1
即使我们直接接受你的比喻(我并不认为任何现代语言应该像C++一样“像刀子一样锋利”,更不用说那么令人困惑了——刀子是简单的!),为了正确使用刀子而不伤害自己,你必须了解风险以及好处。你的回答只解释了好处——但实际上存在真正的风险。 - Kyle Strand
而且,关于这两个初始化的评论“看起来”非常相似,这不仅是美学的问题。正如 Joel Spolsky 所说的那样,错误代码应该看起来就是错误的。如果两段代码“看起来”非常相似,但实际上执行的操作却截然不同,那么从语言设计(和使用)的角度来看,这就有些危险了。 - Kyle Strand
在我看来,最好的经验法则是:除了默认构造函数之外,在计划调用实际构造函数时永远不要使用大括号。大括号适用于基本类型、默认/值初始化和列表初始化。如果您正在调用非平凡类型的非默认构造函数,请使用括号。(编辑:我的粗心“实际构造函数”意味着“不带初始化列表的构造函数”)。 - Nir Friedman
显示剩余3条评论

4
坦白地说,各种初始化技术的微妙之处使得很难说哪种做法是一个“好习惯”。
正如在评论中提到的,Scott Meyers在《现代有效的C++》一书中详细讨论了大括号初始化。他在博客上发表了有关此问题的进一步评论,例如这里这里。在第二篇文章中,他明确表示,他认为C++初始化混乱的困境只是语言设计不良的结果。
正如101010的回答中提到的,大括号初始化有益处。在我看来,防止隐式缩小是主要的好处。当然,“最令人烦恼的解析”问题当然是一个真正的好处,但它微不足道--在我看来,在大多数情况下,错误的int a();而不是int a;可能会在编译时被捕获。
但至少有两个主要缺点:
  • 在C++11和C++14中,auto总是从大括号初始化程序中推导出std::initializer_list。在C++17中,如果初始化列表中只有一个元素,并且没有使用=,则auto会推导出该元素的类型;多个元素的行为未更改(请参见上面链接的第二篇博客文章,其中有更清晰的解释和示例。)(编辑:正如下面T.C.在评论中指出的那样,我对于使用大括号初始化的C++17规则的理解仍然不完全正确。)
  • 101010列出的第三个好处是,初始化列表构造函数优先于所有其他构造函数,这实际上既是缺点也是好处。您已经提到了std::vector<int> vi{ 5, 1 };的行为对熟悉vector旧的两个元素构造函数的人来说是令人惊讶的。Scott Meyers在《现代有效的C++》一书中列出了一些其他例子。就个人而言,我认为这比auto的推断行为更糟糕(我通常只使用复制初始化的auto,这使得第一个问题相当容易避免)。
编辑:事实证明,愚蠢的编译器实现决策有时可以成为使用大括号初始化的另一个原因(尽管真正正确的方法可能是使用#undef)。

auto i{1,2}; 在C++17中是不合法的,而且这个更改是一个DR,所以你也应该在C++11/14模式下看到它。 - T.C.
@T.C. DR的意思是“缺陷报告”吗?我不确定这是什么意思 - 它本质上是对标准先前版本的追溯更改吗?如果是这样,我没有意识到这种事情的存在。您是否知道有关auto行为的良好写作,其中包括该信息?当我的主要观点只是“它很复杂和令人困惑”时,我宁愿链接到其他资源而不是尝试完整/正确的解释。 - Kyle Strand

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