为啥列表初始化(使用花括号)比其他方法更好?

Posted

技术标签:

【中文标题】为啥列表初始化(使用花括号)比其他方法更好?【英文标题】:Why is list initialization (using curly braces) better than the alternatives?为什么列表初始化(使用花括号)比其他方法更好? 【发布时间】:2022-01-07 16:37:02 【问题描述】:
MyClass a1 a;     // clearer and less error-prone than the other three
MyClass a2 = a;
MyClass a3 = a;
MyClass a4(a);

为什么?

【问题讨论】:

没错,这很方便,但在我看来它降低了可读性——我喜欢在阅读代码时查看对象是什么类型。如果你 100% 确定对象是什么类型,为什么要使用 auto?如果你使用列表初始化(阅读我的答案),你可以确定它总是正确的。 @Oleksiy: std::map<std::string, std::vector<std::string>>::const_iterator 想和你谈谈。 @Oleksiy 我推荐阅读this GotW。 @doc 我会说using MyContainer = std::map<std::string, std::vector<std::string>>; 更好(尤其是因为你可以模板化!) 天哪,只有在 C++ 中才存在这样的问题。非常感谢您提出这个问题,答案真的很有帮助 【参考方案1】:

您需要阅读Herb Sutter's (updated) GotW #1。 这详细解释了这些选项之间的区别,以及更多选项,以及与区分不同选项的行为相关的几个问题。

第 4 节的要点/复制:

什么时候应该使用 ( ) 与 语法来初始化对象?为什么? 这是简单的指南:

指南:优先使用带 的初始化,例如向量 v = 1, 2, 3, 4 ;或 auto v = vector 1, 2, 3, 4 ;,因为 它更一致,更正确,并且无需了解 完全是老式的陷阱。在您喜欢的单参数情况下 只看到 = 符号,例如 int i = 42;和自动 x = 任何东西; 省略括号很好。 …

这涵盖了绝大多数情况。只有一个主 例外:

… 在极少数情况下,例如向量 v(10,20);或自动 v = vector(10,20);,使用 ( ) 初始化显式调用 否则被 initializer_list 隐藏的构造函数 构造函数。

但是,这通常应该是“罕见”的原因是因为默认 和复制构造已经很特殊并且可以与 一起正常工作,并且 好的类设计现在大多避免使用 ( ) 的情况 由于这个最终设计指南,用户定义的构造函数:

准则:当你设计一个类时,避免提供一个构造函数 用 initializer_list 构造函数模棱两可地重载,因此 用户不需要使用 () 来访问这样一个隐藏的构造函数。

【讨论】:

【参考方案2】:

基本上是从 Bjarne Stroustrup 的“The C++ Programming Language 4th Edition”中复制和粘贴

列表初始化不允许缩小 (§iso.8.5.4)。那就是:

一个整数不能转换为另一个不能保存其值的整数。例如,字符 允许转换为 int,但不允许转换为 char。 无法将浮点值转换为另一种无法容纳其的浮点类型 价值。例如,允许双精度浮点数,但不允许双精度浮点数。 浮点值无法转换为整数类型。 整数值不能转换为浮点类型。

例子:

void fun(double val, int val2) 

    int x2 = val;    // if val == 7.9, x2 becomes 7 (bad)

    char c2 = val2;  // if val2 == 1025, c2 becomes 1 (bad)

    int x3 val;    // error: possible truncation (good)

    char c3 val2;  // error: possible narrowing (good)

    char c4 24;    // OK: 24 can be represented exactly as a char (good)

    char c5 264;   // error (assuming 8-bit chars): 264 cannot be 
                     // represented as a char (good)

    int x4 2.0;    // error: no double to int value conversion (good)



唯一=优先于 的情况是使用 auto 关键字来获取由初始化程序确定的类型。

例子:

auto z1 99;   // z1 is an int
auto z2 = 99; // z2 is std::initializer_list<int>
auto z3 = 99;   // z3 is an int

结论

除非您有充分的理由不这样做,否则更喜欢 初始化。

【讨论】:

还有一个事实是使用()可以被解析为函数声明。你可以说T t(x,y,z); 但不能说T t(),这是令人困惑和不一致的。有时,你确定x,你甚至不能说T t(x); 我非常不同意这个答案;当您的类型具有接受std::initializer_list 的ctor 时,支撑初始化变得一团糟。 RedXIII 提到了这个问题(并且只是忽略了它),而您完全忽略了它。 A(5,4)A5,4 可以调用完全不同的函数,这点很重要。它甚至可能导致看起来不直观的调用。说默认情况下你应该更喜欢 会导致人们误解发生了什么。不过,这不是你的错。我个人认为这是一个经过深思熟虑的功能。 @user1520427 这就是为什么会有“除非你有充分的理由不这样做”部分。 虽然这个问题很老,但它有很多成功,因此我在这里添加它只是为了参考(我在页面的其他任何地方都没有看到它)。从带有新 Rules for auto deduction from braced-init-list 的 C++14 开始,现在可以编写 auto var 5 并将其推导出为 int 而不是 std::initializer_list&lt;int&gt; 哈哈,从所有的cmets来看,仍然不清楚该怎么做。很明显,C++ 规范是一团糟!【参考方案3】:

只要您不像 Google 在 Chromium 中那样使用 -Wno-narrowing 进行构建,它只会更安全。如果你这样做了,那就不太安全了。如果没有这个标志,唯一的不安全情况将由 C++20 修复。

注意: A)大括号更安全,因为它们不允许变窄。 B) 花括号不太安全,因为它们可以绕过私有或删除的构造函数,并隐式调用显式标记的构造函数。

这两个组合意味着如果里面是原始常量,它们会更安全,但如果它们是对象,则安全性会降低(尽管在 C++20 中已修复)

【讨论】:

我尝试在 goldbolt.org 上闲逛,以使用提供的示例代码绕过“显式​​”或“私有”构造函数,并使其中一个或另一个私有或显式,并以适当的编译器错误作为回报[ s]。想用一些示例代码来支持它吗? 这是针对 C++20 提出的问题的修复:open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1008r1.pdf 如果您编辑答案以显示您正在谈论的 C++ 版本,我很乐意改变我的投票。 clang++ -std=c++14 告诉我main.cpp:22:7: error: calling a private constructor of class 'Foo'。就隐式调用显式构造函数而言,该参数甚至没有意义。这是一个隐式构造函数调用:foo_instance = false;。 false 通过调用匹配的构造函数被隐式转换为 Foo 。如果使用大括号,则显式调用构造函数。关键是你不能在不提及类型名称的情况下使用花括号进行这样的赋值。【参考方案4】:

关于使用列表初始化的优点已经有了很好的答案,但是我个人的经验法则是尽可能不要使用花括号,而是让它依赖于概念含义:

如果我创建的对象在概念上包含我在构造函数中传递的值(例如容器、POD 结构、原子、智能指针等),那么我将使用大括号。 如果构造函数类似于普通函数调用(它执行一些或多或少复杂的操作,这些操作由参数参数化),那么我使用的是普通函数调用语法。 对于默认初始化,我总是使用花括号。 一方面,这样我总是确定对象被初始化,无论它是否例如是具有默认构造函数的“真实”类,无论如何都会被调用或内置/ POD 类型。其次,在大多数情况下,它与第一条规则一致,因为默认初始化对象通常表示“空”对象。

根据我的经验,这个规则集可以比默认使用花括号更一致地应用,但是当它们不能使用或具有与“正常”函数调用不同的含义时,必须明确记住所有异常带括号的语法(调用不同的重载)。

例如非常适合 std::vector 等标准库类型:

vector<int> a10,20;   //Curly braces -> fills the vector with the arguments

vector<int> b(10,20);   //Parentheses -> uses arguments to parametrize some functionality,                          
vector<int> c(it1,it2); //like filling the vector with 10 integers or copying a range.

vector<int> d;      //empty braces -> default constructs vector, which is equivalent
                      //to a vector that is filled with zero elements

【讨论】:

完全同意您的大部分回答。但是,您不认为为向量放置空括号只是多余的吗?我的意思是,当您需要对泛型类型 T 的对象进行值初始化时,这没关系,但是对于非泛型代码这样做的目的是什么? @Mikhail:这当然是多余的,但我的习惯是总是明确地初始化局部变量。正如我所写,这主要是关于一致性,所以我不会忘记它,当它很重要时。当然,我不会在代码审查或风格指南中提及。 非常干净的规则集。 这是迄今为止最好的答案。 就像继承 - 容易被滥用,导致代码难以理解。 @MikeMB 示例:const int &amp;b struct A const int &b; A():b ; () 会做的那样),而是将它绑定到一个临时整数对象,然后让它悬空。 GCC 即使使用-Wall 也不会对第二个示例发出警告。【参考方案5】:

使用大括号初始化的原因有很多,但您应该知道initializer_list&lt;&gt; 构造函数优于其他构造函数,默认构造函数除外。这会导致构造函数和模板出现问题,其中 T 构造函数类型可以是初始化列表或普通的旧 ctor。

struct Foo 
    Foo() 

    Foo(std::initializer_list<Foo>) 
        std::cout << "initializer list" << std::endl;
    

    Foo(const Foo&) 
        std::cout << "copy ctor" << std::endl;
    
;

int main() 
    Foo a;
    Foo b(a); // copy ctor
    Foo ca; // copy ctor (init. list element) + initializer list!!!

假设您没有遇到此类类,则没有理由不使用初始化器列表。

【讨论】:

这是泛型编程中非常重要的一点。当你编写模板时,不要使用括号初始化列表( ... 的标准名称),除非你想要 initializer_list 语义(好吧,也许是为了默认构造一个对象) . 老实说,我不明白为什么 std::initializer_list 规则甚至存在——它只会给语言增加混乱和混乱。如果你想要 std::initializer_list 构造函数,那么做 Fooa 有什么问题?这似乎比让std::initializer_list 优先于所有其他重载更容易理解。 以上评论+1,因为我认为它真的很乱!!这不是逻辑; Fooa 对我来说遵循一些逻辑远远超过 Fooa 这变成初始化列表优先级(用户可能会认为嗯...) 基本上 C++11 用另一种混乱代替了一种混乱。哦,对不起,它并没有取代它——它增加了它。你怎么知道你没有遇到这样的课程?如果您开始没有 std::initializer_list&lt;Foo&gt; 构造函数,但它会在某个时候被添加Foo 类以扩展其接口?然后Foo 类的用户就搞砸了。 .. “使用大括号初始化的许多原因”是什么?这个答案指出了一个原因 (initializer_list&lt;&gt;),它并没有真正限定 说它是首选的,然后继续提到一个 首选的好案例.我错过了什么其他约 30 人(截至 2016 年 4 月 21 日)发现有帮助?

以上是关于为啥列表初始化(使用花括号)比其他方法更好?的主要内容,如果未能解决你的问题,请参考以下文章

我看到花括号、括号和分号彼此相邻,为啥?

switch结构case语句后的多个语句必须放在花括号中。 这句话对吗?为啥?

为啥将 C 代码块用花括号括起来?

C++11 中带或不带花括号的初始化差异

python中,花括号,中括号,小括号的区别

在 Python 中使用花括号初始化 Set