C++0x 统一初始化的“怪异”现象

17

像很多人一样,我对C++0x非常兴奋。我尝试学习和使用新特性,以便在新项目中编写最好的、最易于维护的代码。

不用说,我喜欢新初始化器背后的思想。所以我正在研究它们,这些对我来说是有意义的:

T x = { 1, 2, 3 }; // like a struct or native array
T x({1, 2, 3});    // copy construct something like an "object literal" in other languages... cool!
return {1, 2, 3};  // similar to above, but returning it, even cooler!

我不理解的是:

T x{1, 2, 3};

这感觉有点奇怪。我不确定人们想要使用什么语法,而这仿佛并不“正确”。这种语法的设计思路是什么?
唯一一个看起来有所区别的例子是这样的:
std::vector<int> the_vec{4};

这将调用初始化列表构造函数,但为什么不直接写成这样:

std::vector<int> the_vec = {4};

那就做每个人都已经熟悉的事情吗?

4个回答

26

这个语法的设计思想是什么?

首先,花括号语法使得避免棘手的解析成为可能:

T x(); // function declaration
T x{}; // value-initialized object of type 'T' named 'x'

在C++03中,最接近这个的方法是T x((T()));T x = T();,两者都要求T有一个可访问的复制构造函数。

虽然这是一个不错的答案,请告诉我他们没有仅仅为了“修复”最讨厌的解析而添加了全新的初始化语法 :-/. - Evan Teran
很高兴有一个统一的初始化语法(或者说它真的很酷;我还没有机会使用它 :-P)。由于各种令人烦恼的解析(包括 T x() 和更令人烦恼的 T x(T())),括号不能用作统一初始化语法的标点符号。花括号没有这个问题。 - James McNellis
6
@Evan:它被称为“统一初始化”是有原因的,因为它在任何地方都可以使用相同的方式。现在, {} 表示“初始化”,这就是它的设计思想。 - Nicol Bolas

19

首先,您实际上有两个变量:

T x = { 1, 2, 3 };
T x{1, 2, 3};

这两种初始化方式本质相同,唯一的区别在于如果选择了explicit构造函数,则第一种方法无效。否则,它们是完全相同的。第一种称为“复制列表初始化”,第二种称为“直接列表初始化”。
概念是带有=的形式赋予一个“复合值”——由3个整数组成的值,并用该值初始化x。对于这样的初始化,只允许使用非explicit构造函数。对于x{1, 2, 3}(没有等号),概念是使用3个值来初始化变量——概念上不是一个复合值,而是你同时提供的3个单独的值。你可以说这是“构造函数调用”的最通用的意义。
你展示的另一种初始化实际上与上述两种完全不同:
T x({1, 2, 3});

它只调用T的构造函数,并使用{1,2,3}作为参数。它不会执行任何花哨的操作,例如:如果T是数组,则初始化数组;如果T是聚合结构/类,则初始化结构体成员。如果T不是类,则该声明无效。但如果T恰好具有复制或移动构造函数,则它可以使用该构造函数通过复制列表初始化来构建临时的T,并将复制/移动构造函数的引用参数绑定到该临时变量。我相信您在实际编码中不需要经常使用这种形式。
所有这些都记录在初始化程序列表的委员会提案文件中。在本例中,您需要查看Initializer Lists — Alternative Mechanism and Rationale,第"A programmer's view of initialization kinds"部分:

我们观察到,了解复制初始化和直接初始化之间差异的专业程序员通常会错误地认为前者比后者效率低。(实际上,在两种初始化都有意义时,它们同样有效。)

我们相反地发现,按照不同的术语思考这些问题更加有用:

  • 通过调用构造函数来构造(“构造函数调用”)
  • 通过传递值来构造(“转换”)

(恰好,前者对应于"直接初始化",后者对应于"复制初始化",但标准术语并不帮助程序员。)

后来,他们发现

Note that since we treat the { ... } in

X x = { ... };

as a single value, it is not equivalent to

X x{ ... };

where the { ... } is an argument list for the constructor call (we emphasize it because it is unlike N2531).

在C++0x FDIS中所规定的规则与那篇论文中所述有些许不同,但是该论文所提出的理念被保留并在C++0x FDIS中得到了实现。


1
问题...您提到第一种形式如果选择了explicit构造函数是无效的,然后在第二段中提到只有explicit构造函数应该允许第一种形式。这两个陈述似乎相互矛盾。如果它们不矛盾,您能否进一步解释为什么会这样? - Jason

4

从理论角度来看,给出的答案非常好,但也许还需要一些实际的例子。使用统一初始化方式,现在可以编写以前根本不可能实现的构造方法。例如:

  • 初始化成员数组。

  • 全局常量容器(例如映射)。

举个例子:

class Foo
{
  int data[3];
public:
  Foo() : data{1,2,3} { }
};

在这里,我们可以直接初始化成员数组,而无需进行赋值(考虑默认构造不可用的情况)。

const std::map<int, std::string> labels {
  { 1 , "Open" },
  { 2 , "Close" },
  { 3 , "Reboot" } };

有时候,只读的全局查找对象非常有用,但是如果没有统一初始化,您无法填充数据。

是的,但在这两个例子中,我不能写成:data({1,2,3})const std::map<int, std::string> labels({....});并保持与人们(至少是我)所期望的一致吗? - Evan Teran
@Evan:对于数组(或一般聚合类型),没有构造函数语法,所以你不能写出 data({1,2,3}) -- 我猜这会导致向后不兼容。对于容器来说,你也可以在括号中写入初始化列表,那是一个替代方法。 - Kerrek SB
是的,我更喜欢他们在数组中添加构造函数语法,这样对我来说更易读。 - Evan Teran

1

我认为在泛型编程中,语法的一致性非常重要。例如考虑以下代码:

#include <utility>
#include <tuple>
#include <vector>

template <class T>
struct Uniform {
  T t;
  Uniform() : t{10, 12} {}
};

int main(void)
{
  Uniform<std::pair<int, int>> p;
  Uniform<std::tuple<int, int>> t;
  Uniform<int [2]> a;
  Uniform<std::vector<int>> v; // Uses initializer_list

  return 0;
}

这是一个很好的答案,+1,但我认为它可以更好地解决。个人而言,我更喜欢(我知道我的意见并不重要)。如果所有类型都可以使用构造函数/复制构造函数语法进行初始化(包括数组),并且用户传递其他语言称为“对象字面量”的内容。在您的示例中,t({10, 12}) {}。我认为这将更符合该语言的一致性。 - Evan Teran
遗憾的是,uniform 无法初始化 std::array<int, 2>。std::array 没有 std::initializer_list 构造函数。 - Sumant

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