C ++ 11统一初始化是否可以替代旧式语法?


172

我知道C ++ 11的统一初始化解决了该语言在语法上的歧义,但是在许多Bjarne Stroustrup的演示文稿中(尤其是在GoingNative 2012演讲期间的那些演示文稿中),他的示例现在在构造对象时主要使用此语法。

现在是否建议在所有情况下使用统一初始化?就编码风格和一般用法而言,此新功能应采用的一般方法是什么?什么原因使用它?

请注意,在我的脑海中,我主要以对象构造作为用例,但是如果要考虑其他场景,请告诉我。


这可能是在Programmers.se上讨论得更好的主题。它似乎倾向于良好的主观方面。
Nicol Bolas'2

6
@NicolBolas:另一方面,您的出色答案可能是c ++-faq标记的很好候选者。我认为我们之前没有对此进行解释。
Matthieu M.

Answers:


233

编码风格最终是主观的,从中获得实质性性能收益的可能性很小。但是,我要说的是,您从统一初始化的自由使用中受益:

最小化冗余类型名

考虑以下:

vec3 GetValue()
{
  return vec3(x, y, z);
}

为什么需要输入vec3两次?有什么意义吗?编译器非常清楚函数返回的内容。我为什么不能只说“用这些值调用返回的构造函数并返回它?” 通过统一初始化,我可以:

vec3 GetValue()
{
  return {x, y, z};
}

一切正常。

函数参数甚至更好。考虑一下:

void DoSomething(const std::string &str);

DoSomething("A string.");

无需键入类型名即可工作,因为它std::string知道如何从const char*隐式构建自身。那很棒。但是,如果该字符串来自RapidXML,那该怎么办。或Lua字符串。也就是说,假设我实际上知道字符串的长度。如果我只是传递a std::string,则采用a 的构造函数const char*将必须采用字符串的长度const char*

但是,有一个过载需要明确地指定长度。但是要使用它,我必须这样做:DoSomething(std::string(strValue, strLen))。为什么在那里有多余的类型名?编译器知道类型是什么。与一样auto,我们可以避免使用额外的类型名:

DoSomething({strValue, strLen});

它只是工作。没有类型名称,没有大惊小怪,什么也没有。编译器完成工作,代码更短,每个人都很高兴。

当然,有一个论点是第一个版本(DoSomething(std::string(strValue, strLen)))更清晰易读。也就是说,很明显正在发生什么,谁在做什么。在一定程度上说是对的。了解统一的基于初始化的代码需要查看函数原型。这就是为什么有人说您永远不要通过非const引用传递参数的相同原因:这样您就可以在调用站点看到是否正在修改值。

但是可以说同样的话auto; 知道从中得到什么auto v = GetSomething();需要了解的定义GetSomething。但是,auto一旦您能够使用它,它就不会被鲁near地弃用。就个人而言,我认为一旦习惯了就可以了。特别是具有良好的IDE。

永远不会得到最令人烦恼的解析

这是一些代码。

class Bar;

void Func()
{
  int foo(Bar());
}

流行测验:什么是foo?如果回答“变量”,那是错误的。它实际上是一个函数的原型,该函数将返回a的函数作为其参数Bar,并且该foo函数的返回值是int。

这被称为C ++的“最烦人的解析”,因为它对人类绝对没有意义。但是C ++的规则黯然需要这样的:如果它可能被解释为一个函数原型,那么它是。问题是Bar(); 那可能是两件事之一。它可能是名为的类型Bar,这意味着它正在创建一个临时文件。或者它可以是不带任何参数并返回的函数Bar

统一初始化不能解释为函数原型:

class Bar;

void Func()
{
  int foo{Bar{}};
}

Bar{}总是创建一个临时的。int foo{...}总是创建一个变量。

在很多情况下,您都想使用,Typename()但由于C ++的解析规则而无法使用。使用Typename{},没有歧义。

不这样做的原因

您放弃的唯一真正力量正在缩小。您不能通过统一初始化用较大的值来初始化较小的值。

int val{5.2};

那不会编译。您可以使用老式的初始化来执行此操作,但不能使用统一初始化。

这样做的部分目的是使初始化程序列表实际起作用。否则,关于初始值设定项列表的类型将存在很多模棱两可的情况。

当然,有些人可能认为这样的代码不值得编译。我个人碰巧同意;缩小范围非常危险,并且可能导致不愉快的行为。最好在编译器阶段尽早发现这些问题。至少,缩小范围表明某人对代码的思考不是太认真。

请注意,如果警告级别很高,则编译器通常会警告您这种情况。因此,实际上,所有要做的就是使警告变为强制错误。有人可能会说您还是应该这样做;)

还有一个原因不这样做:

std::vector<int> v{100};

这是做什么的?它可以创建一个vector<int>包含一百个默认构造项目的对象。或者它可以创建一个vector<int>值为1的项目100。两者在理论上都是可能的。

实际上,是后者。

为什么?初始化程序列表使用与统一初始化相同的语法。因此,必须有一些规则来解释在模棱两可的情况下该怎么做。规则很简单:如果编译器可以使用初始化列表构造带括号初始化列表中,那么它。由于vector<int>具有一个采用的初始值设定项列表构造函数initializer_list<int>,并且{100}可能是有效的initializer_list<int>,因此必须为

为了获得调整大小的构造函数,您必须使用()代替{}

请注意,如果这vector是不能转换为整数的值,则不会发生。initializer_list不适合该vector类型的初始化列表构造函数,因此编译器可以从其他构造函数中自由选择。


11
+1钉了它。我正在删除我的答案,因为您的答案将更详细地说明所有相同的观点。
R. Martinho Fernandes

21
最后一点是为什么我真的很想std::vector<int> v{100, std::reserve_tag};。与相似std::resize_tag。当前需要两个步骤来保留向量空间。
Xeo 2012年

6
@NicolBolas-两点:我认为麻烦的解析问题是foo(),而不是Bar()。换句话说,如果您这样做了int foo(10),会不会遇到相同的问题?其次,不使用它的另一个原因似乎更多是工程过度的问题,但是如果我们使用构造所有对象{},但是一天之后,我为初始化列表添加了构造函数,该怎么办呢?现在,我所有的构造语句都变成了初始化列表语句。在重构方面似乎非常脆弱。对此有何评论?
void.pointer 2012年

7
@RobertDailey:“如果这样做int foo(10),您是否会遇到相同的问题?” No. 10是整数文字,而整数文字绝不能是类型名。烦人的解析来自Bar()可能是类型名或临时值的事实。这就是为编译器造成歧义的原因。
Nicol Bolas

8
unpleasant behavior-要记住一个新的标准术语:>
sehe 2012年

64

我不同意Nicol Bolas的答案部分“ 最小化冗余类型名”。由于代码只能编写一次并可以多次读取,因此我们应该尽量减少读取和理解代码所花费的时间,而不是编写代码所花费的时间。试图仅最小化键入就是试图优化错误的东西。

请参见以下代码:

vec3 GetValue()
{
  <lots and lots of code here>
  ...
  return {x, y, z};
}

第一次阅读上面的代码的人可能不会立即理解return语句,因为到他到达那一行时,他将已经忘记了return类型。现在,他必须回滚到函数签名或使用某些IDE功能才能查看返回类型并完全理解return语句。

同样,对于初次阅读代码的人来说,要理解真正构造的内容并不容易:

void DoSomething(const std::string &str);
...
const char* strValue = ...;
size_t strLen = ...;

DoSomething({strValue, strLen});

当有人认为DoSomething也应该支持其他字符串类型并添加以下重载时,以上代码将中断:

void DoSomething(const CoolStringType& str);

如果CoolStringType碰巧有一个采用const char *和size_t的构造函数(就像std :: string一样),则对DoSomething({strValue,strLen})的调用将导致歧义错误。

我对实际问题的回答:
不,不应该将统一初始化视为旧式构造函数语法的替代。

我的推理是这样的:
如果两个语句没有相同的意图,则它们不应看起来相同。对象初始化有两种概念:
1)将所有这些项目放入我要初始化的对象中。
2)使用我作为指导提供的这些参数构造该对象。

使用概念1的示例:

struct Collection
{
    int first;
    char second;
    double third;
};

Collection c {1, '2', 3.0};
std::array<int, 3> a {{ 1, 2, 3 }};
std::map<int, char> m { {1, '1'}, {2, '2'}, {3, '3'} };

使用概念2的示例:

class Stairs
{
    std::vector<float> stepHeights;

public:
    Stairs(float initHeight, int numSteps, float stepHeight)
    {
        float height = initHeight;

        for (int i = 0; i < numSteps; ++i)
        {
            stepHeights.push_back(height);
            height += stepHeight;
        }
    }
};

Stairs s (2.5, 10, 0.5);

我认为新标准允许人们像这样初始化楼梯是一件坏事:

Stairs s {2, 4, 6};

...因为混淆了构造函数的含义。这样的初始化看起来像概念1,但事实并非如此。即使看起来像是,它也没有将三个不同的步长值注入对象s中。而且,更重要的是,如果已经发布了如上所述的Stairs的库实现,并且程序员一直在使用它,然后,如果库实现者后来向Stairs添加了initializer_list构造函数,则所有使用Stairs进行统一初始化的代码语法即将中断。

我认为C ++社区应该就如何使用统一初始化达成一致的约定,即在所有初始化中统一使用,或者像我强烈建议的那样,将这两个初始化概念分开,从而向程序员的读者阐明程序员的意图。编码。


AFTERTHOUGHT:
这又是为什么您不应该将统一初始化视为旧语法的替代,以及为什么不能对所有初始化使用大括号表示法的另一个原因:

说,制作副本的首选语法是:

T var1;
T var2 (var1);

现在,您认为应该用新的大括号语法替换所有初始化,以便您可以(并且代码看起来)更加一致。但是,如果类型T为聚合,则使用大括号的语法将不起作用:

T var2 {var1}; // fails if T is std::array for example

48
如果您有“ <这里有很多代码”,无论语法如何,您的代码都将难以理解。
凯文·克莱恩

8
除了IMO之外,IDE的职责还包括告知您返回的类型(例如,悬停)。当然,如果您不使用IDE,那么您
就要

4
@TommiT我同意您所说的某些内容。但是,出于auto显式类型声明之战相同的精神,我会寻求一种平衡:在模板元编程情况下,无论如何类型通常都是显而易见的,统一的初始化程序会花费很多时间。它将避免重复复杂-> decltype(....)的咒语,例如简单的单行功能模板(让我哭泣)。
sehe 2012年

5
但是,如果类型T是一个聚合,则使用花括号的语法将不起作用: ”请注意,这是标准中报告的缺陷,而不是有意的预期行为。
Nicol Bolas

5
“现在,他必须回滚到函数签名”,如果必须滚动,则函数太大。
Miles Rout 2014年

-3

如果您的构造函数merely copy their parameters在类in exactly the same order中声明的相应类变量中,则使用统一初始化最终比调用构造函数更快(但也可以完全相同)。

显然,这并没有改变您必须始终声明构造函数的事实。


2
您为什么说它可以更快?
jbcoe

这是不正确的。不需要声明构造函数:struct X { int i; }; int main() { X x{42}; }。同样正确的是,统一初始化可能比值初始化更快。
蒂姆
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.