使用初始化列表构造函数的不同调用方式

9
考虑以下关于初始化列表构造函数用法的示例:
std::vector<std::string> v = { "xyzzy", "plugh", "abracadabra" };
std::vector<std::string> v({ "xyzzy", "plugh", "abracadabra" });
std::vector<std::string> v{ "xyzzy", "plugh", "abracadabra" }; 

它们之间有任何差异(即使是稍微的)吗?

在一个大项目中,你需要定义一个标准,你会选择哪种样式?
我更喜欢第一种样式,第三种样式很容易与使用参数的构造函数混淆。而且第一种样式看起来更像其他编程语言。


1
好问题,虽然大多数是基于个人观点的。我投票支持(1),因为它强调了使用std::initializer_list的方法。 - leemes
2
我会投票给#3,从#1中可能会误导人认为使用了operator=和copy_constructor。 - Exceptyon
1
如果你认为初始化中的 = 意味着 operator=,那么你需要学习 C++ 类的入门课程。编码标准不应旨在确保非程序员能理解代码。 - Yakk - Adam Nevraumont
1
@Yakk 你应该总是像下一个接手你代码的人是个白痴一样编程。 - ahruss
@ahruss,那你就不能编程了。嗯,你可以将所有代码标记为只读。 - Yakk - Adam Nevraumont
1
第二种形式让我有点反感。 - Jonathan Wakely
1个回答

7
在涉及到一个字符串向量的情况下,这三种形式没有区别。然而,如果使用了接受initializer_list的构造函数并且是explicit的话,第一种形式(即复制列表初始化)是不被允许的,而后两种形式(即直接列表初始化)是被允许的。因此,我更倾向于使用第三种形式,避免使用第二种形式因为括号是多余的。

正如Yakk在评论中指出的那样,当正在构造的类型没有接受initializer_list参数的构造函数时,会出现其他差异。

例如,如果正在构造的类型具有一个接受三个参数(全部为char const *类型)的构造函数,而不是initializer_list构造函数,则情况1和3有效,但情况2是不合法的,因为花括号初始化列表在括号内时无法匹配三个参数的构造函数。

如果该类型具有initializer_list构造函数,但括号初始化列表的元素不能隐式转换为initializer_list ,则将考虑其他构造函数。假设存在另一个匹配的构造函数,则形式2会导致中间副本被构建,而其他两种方法则不会。这可以通过使用以下示例编译 -fno-elide-constructors进行演示。

struct foo
{
    foo(int, int) { std::cout << __PRETTY_FUNCTION__ << std::endl; }
    foo(foo const&) { std::cout << __PRETTY_FUNCTION__ << std::endl; }
    foo(std::initializer_list<std::string>) { std::cout << __PRETTY_FUNCTION__ << std::endl; }
};

int main()
{
    foo f1 = {1,2};
    std::cout << "----\n";
    foo f2({1,2});
    std::cout << "----\n";
    foo f3{1,2};
}

输出:

foo::foo(int, int)
----
foo::foo(int, int)
foo::foo(const foo&)
----
foo::foo(int, int)


以下案例不是问题的一部分,但仍然值得注意。在某些情况下,使用嵌套的大括号可能会导致难以理解的行为。请考虑。
std::vector<std::string> v1{{ "xyzzy", "plugh", "abracadabra" }};
std::vector<std::string> v2{{ "xyzzy", "plugh"}};

v1 按预期工作,将包含 3 个字符串的 vector,而 v2 将导致未定义的行为。有关详细解释,请参见 this answer


除了隐式/显式之外,另外两个的区别在于正在构建的对象没有初始化列表构造函数,并且如果传递的类型无法转换为对象具有的列表构造函数的类型。比较std::vector<std::string> ({1,3}){1,3} - Yakk - Adam Nevraumont
@Yakk 很好的观点,我更新了有关第一部分的答案。我明白你在第二部分中想表达的意思,即缺乏隐式转换,但是你评论中的这两个片段都无法编译,我很难想出一个示例来展示这两种形式的不同之处。 - Praetorian
但它们无法编译的原因不同!基本上,如果列表失败,T{a,b} 将尝试查找 T 的 2 个参数构造函数,而 T({1,3}) 将尝试使用 {1,3} 构造 T 的 1 个参数构造函数(包括复制构造函数,如果存在的话)。我想。struct foo { foo(int,int); foo(foo const&)=delete; }; - Yakk - Adam Nevraumont
@Yakk 哦,好的,现在我明白你的意思了。是的,它们会以不同的方式失败,原因就是你所描述的。后一种情况确实考虑了复制构造函数。我稍后会更新答案。 - Praetorian
@dynamic 好的,我已经很清楚地表明了嵌套大括号不是原始问题的一部分。我觉得这与手头的话题相关,因为您正在询问初始化中不同组合的大括号和括号的微妙差别。在map的情况下需要嵌套大括号,因为它的initializer_list构造函数采用pair<const Key, Value>,而pair 2个参数构造函数由内部大括号匹配。 - Praetorian

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