我知道C ++ 11的统一初始化解决了该语言在语法上的歧义,但是在许多Bjarne Stroustrup的演示文稿中(尤其是在GoingNative 2012演讲期间的那些演示文稿中),他的示例现在在构造对象时主要使用此语法。
现在是否建议在所有情况下都使用统一初始化?就编码风格和一般用法而言,此新功能应采用的一般方法是什么?什么原因不使用它?
请注意,在我的脑海中,我主要以对象构造作为用例,但是如果要考虑其他场景,请告诉我。
我知道C ++ 11的统一初始化解决了该语言在语法上的歧义,但是在许多Bjarne Stroustrup的演示文稿中(尤其是在GoingNative 2012演讲期间的那些演示文稿中),他的示例现在在构造对象时主要使用此语法。
现在是否建议在所有情况下都使用统一初始化?就编码风格和一般用法而言,此新功能应采用的一般方法是什么?什么原因不使用它?
请注意,在我的脑海中,我主要以对象构造作为用例,但是如果要考虑其他场景,请告诉我。
Answers:
编码风格最终是主观的,从中获得实质性性能收益的可能性很小。但是,我要说的是,您从统一初始化的自由使用中受益:
考虑以下:
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
类型的初始化列表构造函数,因此编译器可以从其他构造函数中自由选择。
std::vector<int> v{100, std::reserve_tag};
。与相似std::resize_tag
。当前需要两个步骤来保留向量空间。
int foo(10)
,会不会遇到相同的问题?其次,不使用它的另一个原因似乎更多是工程过度的问题,但是如果我们使用构造所有对象{}
,但是一天之后,我为初始化列表添加了构造函数,该怎么办呢?现在,我所有的构造语句都变成了初始化列表语句。在重构方面似乎非常脆弱。对此有何评论?
int foo(10)
,您是否会遇到相同的问题?” No. 10是整数文字,而整数文字绝不能是类型名。烦人的解析来自Bar()
可能是类型名或临时值的事实。这就是为编译器造成歧义的原因。
unpleasant behavior
-要记住一个新的标准术语:>
我不同意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
auto
与显式类型声明之战相同的精神,我会寻求一种平衡:在模板元编程情况下,无论如何类型通常都是显而易见的,统一的初始化程序会花费很多时间。它将避免重复复杂-> decltype(....)
的咒语,例如简单的单行功能模板(让我哭泣)。
如果您的构造函数merely copy their parameters
在类in exactly the same order
中声明的相应类变量中,则使用统一初始化最终比调用构造函数更快(但也可以完全相同)。
显然,这并没有改变您必须始终声明构造函数的事实。
struct X { int i; }; int main() { X x{42}; }
。同样正确的是,统一初始化可能比值初始化更快。