现代C ++中的初始化


众所周知,初始化语义是C ++最复杂的部分之一。 初始化的类型很多,用不同的语法描述,它们都以复杂而具有挑战性的方式进行交互。 C ++ 11带来了“通用初始化”的概念。 不幸的是,她引入了甚至更复杂的规则,反过来,它们在C ++ 14,C ++ 17中被阻塞,而在C ++ 20中又被更改。


C + +俄罗斯会议上,Timur Doumler报告的视频和翻译被削减了。 Timur首先总结了C ++中初始化演变的历史结果,对初始化规则的当前版本,典型问题和意外之处进行了系统的概述,解释了如何有效使用所有这些规则,最后讨论了可以使初始化语义成为标准的新建议。 C ++ 20更加方便。 这个故事进一步代表了他。



目录




您现在看到的gif很好地传达了报告的主要信息。 大约六个月前,我在互联网上找到了它,并将其发布在我的Twitter上。 在给她的评论中,有人说缺少三种初始化类型。 开始进行讨论,在此期间我被邀请报告。 这样就开始了。


关于初始化Nikolay Yossutis已经告诉过 。 他的报告包括一张幻灯片,该幻灯片列出了初始化int的19种不同方式:


int i1; //undefined value int i2 = 42; //note: inits with 42 int i3(42); //inits with 42 int i4 = int(); //inits with 42 int i5{42}; //inits with 42 int i6 = {42}; //inits with 42 int i7{}; //inits with 0 int i8 = {}; //inits with 0 auto i9 = 42; //inits with 42 auto i10{42}; //C++11: std::initializer_list<int>, C++14: int auto i11 = {42}; //inits std::initializer_list<int> with 42 auto i12 = int{42}; //inits int with 42 int i13(); //declares a function int i14(7, 9); //compile-time error int i15 = (7, 9); //OK, inits int with 9 (comma operator) int i16 = int(7, 9); //compile-time error int i17(7, 9); //compile-time error auto i18 = (7, 9); //OK, inits int with 9 (comma operator) auto i19 = int(7, 9); //compile-time error 

在我看来,这是编程语言的一种独特情况。 初始化变量是最简单的操作之一,但是在C ++中,这并非易事。 这种语言不太可能具有其他领域,近年来在该领域中将有尽可能多的关于偏离标准,更正和变更的报告。 初始化规则在标准之间变化,并且Internet上有无数关于如何在C ++中进行初始化的文章。 因此,对其进行系统的审查不是一件容易的事。


我将按时间顺序介绍材料:首先,我们将讨论从C继承的内容,然后是C ++ 98,然后是C ++ 03,C ++ 11,C ++ 14和C ++ 17。 我们将讨论常见的错误,并就正确的初始化提出建议。 我还将讨论C ++ 20中的创新。 报告末尾将显示一个概述表。



默认初始化(C)


在C ++中,很多东西都是从C继承的,这就是为什么我们将从它开始。 有几种方法可以在C中初始化变量。 它们可能根本不被初始化,这称为默认初始化 。 我认为这是一个不幸的名字。 事实是,没有为变量分配默认值,只是没有初始化。 如果在C ++和C中使用未初始化的变量,则会得到未定义的行为:


 int main() { int i; return i; // undefined behaviour } 

自定义类型也是如此:如果在某些struct存在未初始化的字段,则在访问它们时,也会发生未定义的行为:


 struct Widget { int i; int j; }; int main() { Widget widget; return widget.i; //   } 

C ++中已添加了许多新的构造:类,构造函数,公共,私有,方法,但这些都不会影响上述行为。 如果某些元素未在类中初始化,则在访问它时会发生未定义的行为:


 class Widget { public: Widget() {} int get_i() const noexcept { return i; } int get_j() const noexcept { return j; } private: int i; int j; }; int main() { Widget widget; return widget.get_i(); // Undefined behaviour! } 

默认情况下,没有神奇的方法可以在C ++中初始化类元素。 这是一个有趣的观点,在我从事C ++的职业生涯的最初几年中,我并不知道这一点。 我当时使用的编译器和IDE都没有以任何方式提醒我这一点。 我的同事在检查代码时没有注意此功能。 我非常确定,由于她的原因,这些年来我编写的代码中存在一些非常奇怪的错误。 在我看来,类应该初始化其变量似乎很明显。


在C ++ 98中,可以使用成员初始化器列表初始化变量。 但是,这种问题的解决方案并不是最佳的,因为它必须在每个构造函数中完成,而且这很容易忘记。 另外,初始化以声明变量的顺序进行,而不是以成员初始化列表的顺序进行:


 // C++98: member initialiser list class Widget { public: Widget() : i(0), j(0) {} // member initialiser list int get_i() const noexcept { return i; } int get_j() const noexcept { return j; } private: int i; int j; }; int main() { Widget widget; return widget.get_i(); } 

在C ++ 11中,添加了直接成员初始化程序,使用起来更加方便。 它们允许您同时初始化所有变量,这使您可以确信所有元素都已初始化:


 // C++11: default member initialisers class Widget { public: Widget() {} int get_i() const noexcept { return i; } int get_j() const noexcept { return j; } private: int i = 0; // default member initialisers int j = 0; }; int main() { Widget widget; return widget.get_i(); } 

我的第一个建议:尽可能使用DMI(直接成员初始化器)。 它们既可以与内置类型( floatint )一起使用,也可以与对象一起使用。 初始化元素的习惯使我们更自觉地处理此问题。



复制初始化(C)


因此,默认情况下,从C继承的第一个初始化方法是初始化,因此不应使用它。 第二种方法是复制初始化 。 在这种情况下,我们通过等号表示变量-其值:


 // copy initialization int main() { int i = 2; } 

当参数按值传递给函数时,或按值从函数返回对象时,也使用复制初始化:


 // copy initialization int square(int i) { return i * i; } 

等号可能会给人以分配值的印象,但事实并非如此。 复制初始化不是值分配。 本报告中不会涉及拨款。


复制初始化的另一个重要属性:如果值的类型不匹配,则执行转换序列。 转换序列具有某些规则,例如,它不调用显式构造函数,因为它们不转换构造函数。 因此,如果对构造函数标记为显式的对象执行复制初始化,则会发生编译错误:


 struct Widget { explicit Widget(int) {} }; Widget w1 = 1; // ERROR 

而且,如果存在另一个非显式的构造函数,但类型更糟,则复制初始化将调用它,而忽略该显式的构造函数:


 struct Widget { explicit Widget(int) {} Widget(double) {} }; Widget w1 = 1; //  Widget(double) 


聚合初始化(C)


我想谈的第三种初始化类型是聚合初始化 。 当使用括号中的一系列值初始化数组时,将执行该命令:


 int i[4] = {0, 1, 2, 3}; 

如果您未指定数组的大小,则从括号中包含的值的数量派生它:


 int j[] = {0, 1, 2, 3}; // array size deduction 

聚合类使用相同的初始化,即,这些类只是公共元素的集合(聚合类的定义中还有其他一些规则,但现在我们不再赘述):


 struct Widget { int i; float j; }; Widget widget = {1, 3.14159}; 

此语法甚至在C和C ++ 98中都有效,并且从C ++ 11开始,您可以跳过其中的等号:


 Widget widget{1, 3.14159}; 

聚合初始化实际上对每个元素使用副本初始化。 因此,如果您尝试对具有显式构造函数的多个对象使用聚合初始化(均等号和不带等号),则将对每个对象执行复制初始化,并且会发生编译错误:


 struct Widget { explicit Widget(int) {} }; struct Thingy { Widget w1, w2; }; int main() { Thingy thingy = {3, 4}; // ERROR Thingy thingy {3, 4}; // ERROR } 

如果这些对象还有另一个非显式的构造函数,则将其调用,即使更不适合键入:


 struct Widget { explicit Widget(int) {} Widget(double) {} }; struct Thingy { Widget w1, w2; }; int main() { Thingy thingy = {3, 4}; //  Widget(double) Thingy thingy {3, 4}; //  Widget(double) } 

让我们考虑聚合初始化的另一个属性。 问题:该程序返回什么值?


 struct Widget { int i; int j; }; int main() { Widget widget = {1}; return widget.j; } 

隐藏文字

是的,零。 如果在聚合初始化期间跳过值数组中的某些元素,则相应的变量将设置为零。 这是一个非常有用的属性,因为有了它,永远不会有未初始化的元素。 它适用于聚合类和数组:


 //     int[100] = {}; 

聚合初始化的另一个重要属性是省略了方括号(括号省略)。 您认为该程序返回什么价值? 它有一个Widget ,它是两个int值的集合;还有Thingy ,它是Widgetint的集合。 如果将两个初始化值传递给{1, 2}什么?


 struct Widget { int i; int j; }; struct Thingy { Widget w; int k; }; int main() { Thingy t = {1, 2}; return tk; //   ? } 

隐藏文字

答案是零。 在这里,我们正在处理子聚合,即嵌套的聚合类。 可以使用嵌套括号来初始化此类,但是您可以跳过这些括号对之一。 在这种情况下,执行子聚合的递归遍历,结果{1, 2}等效于{{1, 2}, 0} 。 诚然,此属性并不完全明显。



静态初始化(C)


最后, 静态初始化也从C继承:静态变量总是被初始化。 这可以通过几种方式来完成。 可以使用常量表达式初始化静态变量。 在这种情况下,初始化发生在编译时。 如果您没有为变量分配任何值,则将其初始化为零:


 static int i = 3; //   statit int j; //   int main() { return i + j; } 

即使j未初始化,该程序也会返回3。 如果变量不是由常量而是由对象初始化的,则可能会出现问题。


这是我正在处理的真实库中的示例:


 static Colour red = {255, 0, 0}; 

其中有一个Color类,并且将原色(红色,绿色,蓝色)定义为静态对象。 这是有效的操作,但是一旦在使用red的初始化器中出现另一个静态对象,就会出现不确定性,因为没有初始化变量的严格顺序。 您的应用程序可以访问未初始化的变量,然后崩溃。 幸运的是,在C ++ 11中,可以使用constexpr构造函数,然后我们要处理常量初始化。 在这种情况下,初始化顺序没有问题。


因此,从C语言继承了四种初始化类型:默认初始化,复制,聚合和静态初始化。



直接初始化(C ++ 98)


让我们继续到C ++ 98。 区别C ++和C的最重要的功能也许是构造函数。 这是构造函数调用的示例:


 Widget widget(1, 2); int(3); 

使用相同的语法,您可以初始化诸如intfloat内置类型。 这种语法称为直接初始化 。 当我们在括号中有一个参数时,它总是被执行。


对于内置类型( intboolfloat ),此处与复制初始化没有区别。 如果我们在谈论用户类型,那么与复制初始化不同,直接初始化可以传递多个参数。 实际上,为此,发明了直接初始化。


另外,直接初始化不执行转换序列。 而是使用重载分辨率调用构造函数。 直接初始化的语法与函数调用相同,并且使用与其他C ++函数相同的逻辑。


因此,在使用显式构造函数的情况下,尽管复制初始化会引发错误,但直接初始化可以正常工作:


 struct Widget { explicit Widget(int) {} }; Widget w1 = 1; //  Widget w2(1); //    

在具有两个构造函数的情况下,其中一个构造函数是显式的,而第二个构造函数在类型上不太合适,第一个构造函数是使用直接初始化调用的,第二个构造函数是使用副本调用的。 在这种情况下,更改语法将导致调用另一个构造函数-这通常被忘记:


 struct Widget { explicit Widget(int) {} Widget(double) {} }; Widget w1 = 1; //  Widget(double) Widget w2(1); //  Widget(int) 

当使用括号时,总是使用直接初始化,包括使用构造函数调用表示法初始化临时对象时,以及在括号和转换表达式中带有初始化器的new表达式中,都使用直接初始化:


 useWidget(Widget(1, 2)); //   auto* widget_ptr = new Widget(2, 3); // new-expression with (args) static_cast<Widget>(thingy); // cast 

只要C ++本身存在,该语法就存在,并且它有一个重要的缺陷,Nikolai在主题演讲中提到了: 最令人烦恼的解析 。 这意味着编译器可以将所有内容读为声明(声明),而将其读为声明。


考虑一个示例,其中有一个Widget类和一个Thingy类,以及一个接收WidgetThingy构造函数:


 struct Widget {}; struct Thingy { Thingy(Widget) {} }; int main () { Thingy thingy(Widget()); } 

乍一看,似乎在Thingy初始化时,将创建的默认Widget传递给它,但实际上,该函数在此处声明。 此代码声明一个函数,该函数接收另一个函数作为输入,该函数不接收任何输入作为输入,并返回Widget ,而第一个函数返回Thingy 。 该代码编译没有错误,但是我们不太可能寻求这种行为。



值初始化(C ++ 03)


让我们继续下一个版本-C ++ 03。 通常认为此版本没有重大更改,但事实并非如此。 在C ++ 03中,出现了值初始化,其中写入了空括号:


 int main() { return int(); // UB  C++98, 0   C++03 } 

在C ++ 98中,这里会发生未定义的行为,因为默认情况下会进行初始化,并且从C ++ 03开始,此程序将返回零。


规则是这样的:如果存在用户定义的默认构造函数,则使用值进行初始化会调用此构造函数,否则返回零。


使用自定义构造函数更详细地考虑这种情况:


 struct Widget { int i; }; Widget get_widget() { return Widget(); // value initialization } int main() { return get_widget().i; } 

在此程序中,该函数初始化新Widget的值并返回它。 我们调用此函数并访问Widget对象的元素i 。 从C ++ 03开始,由于没有用户定义的默认构造函数,因此此处的返回值为零。 如果存在这样的构造函数,但没有初始化i ,那么我们将得到未定义的行为:


 struct Widget { Widget() {} //   int i; }; Widget get_widget() { return Widget(); // value initialization } int main() { return get_widget().i; //   ,  UB } 

值得注意的是,“用户定义”并不意味着“用户定义”。 这意味着用户必须提供构造函数的主体,即花括号。 如果在上面的示例中,将构造函数主体替换为= default (此功能已在C ++ 11中添加),则程序的含义将更改。 现在我们有了一个由用户定义的构造函数(用户定义),但是没有由用户提供的构造函数(用户提供),因此程序返回零:


 struct Widget { Widget() = default; // user-defined,   user-provided int i; }; Widget get_widget() { return Widget(); // value initialization } int main() { return get_widget().i; //  0 } 

现在,让我们尝试Widget() = default移出类。 程序的含义再次更改: Widget() = default如果在类之外,则认为它是用户提供的构造函数。 程序再次返回未定义的行为。


 struct Widget { Widget(); int i; }; Widget::Widget() = default; //  ,  user-provided Widget get_widget() { return Widget(); // value initialization } int main() { return get_widget().i; //    , UB } 

有一定的逻辑:在类外部定义的构造函数可以在另一个翻译单元内部。 编译器可能看不到此构造函数,因为它可能在另一个.cpp文件中。 因此,编译器无法得出关于此类构造函数的任何结论,也无法将带有主体的构造函数与= default的构造函数区分开。



通用初始化(C ++ 11)


C ++ 11中有许多非常重要的更改。 特别是,引入了通用(统一)初始化,我更喜欢将其称为“独角兽初始化”,因为它很神奇。 让我们看看她为什么出现。


正如您已经注意到的那样,在C ++中,有许多具有不同行为的不同初始化语法。 带有括号的烦人的语法分析带来了许多不便。 开发人员也不喜欢聚合初始化只能用于数组,而不能用于std::vector类的容器。 相反,您必须执行.reserve.push_back ,或使用各种令人毛骨悚然的库:


 //    ,  : std::vector<int> vec = {0, 1, 2, 3, 4}; //   : std::vector<int> vec; vec.reserve(5); vec.push_back(0); vec.push_back(1); vec.push_back(2); vec.push_back(3); vec.push_back(4); 

该语言的创建者试图通过使用大括号但不带等号的语法来解决所有这些问题。 假定这对于所有类型都是单一语法,其中使用花括号,并且不存在令人烦恼的解析问题。 在大多数情况下,此语法会起作用。


这种新的初始化称为列表初始化 ,它有两种类型:直接和复制。 在第一种情况下,仅使用花括号,在第二种情况下,使用等号的花括号:


 // direct-list-initialization Widget widget{1, 2}; // copy-list-initialization Widget widget = {1, 2}; 

用于初始化的列表称为braced-init-list 。 重要的是此列表不是对象;它没有类型。 从早期版本切换到C ++ 11不会对聚合类型造成任何问题,因此此更改并不重要。 但是现在括号中的列表具有新功能。 尽管没有类型,但可以将其隐藏转换为std::initializer_list ,它是一种特殊的新类型。 并且如果有一个接受std::initializer_list作为输入的构造函数,则此构造函数称为:


 template <typename T> class vector { //... vector(std::initializer_list<T> init); //   initializer_list }; std::vector<int> vec{0, 1, 2, 3, 4}; //  ^  

在我看来,从C ++委员会的角度来看, std::initializer_list不是最成功的解决方案。 对他来说弊大于利。


首先, std::initializer_list是带有const元素的固定大小的向量。 也就是说,它是一种类型,它具有迭代器返回的beginend函数,它具有自己的迭代器类型,要使用它,您需要包括一个特殊的标头。 由于std::initializer_list元素是const ,因此无法移动,因此,如果上面代码中的T为仅移动类型,则该代码将不会执行。


接下来, std::initializer_list是一个对象。 实际上,我们使用它来创建和传输对象。 通常,编译器可以对此进行优化,但是从语义的角度来看,我们仍然处理不必要的对象。


几个月前,Twitter上进行了一项民意调查:如果您可以回到过去并从C ++中删除某些内容,那么您将删除哪些内容? 所有投票中的大多数都恰好收到了initializer_list


https://twitter.com/shafikyaghmour/status/1058031143935561728


, initializer_list . , .


, . , initializer_list , . :


 std::vector<int> v(3, 0); //   0, 0, 0 std::vector<int> v{3, 0}; //   3, 0 

vector int , , , — . . , initializer_list , 3 0.


:


 std::string s(48, 'a'); // "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" std::string s{48, 'a'}; // "0a" 

48 «», «0». , string initializer_list . 48 , . ASCII 48 — «0». , , , int char . . , , .


. , ? ?


 template <typename T, size_t N> auto test() { return std::vector<T>{N}; } int main () { return test<std::string, 3>().size(); } 

, — 3. string int , 1, std::vector<std::int> initializer_list . initializer_list , . string int float , , . , . , emplace , . , {} .


, .



.
— ( {a} )
( = {a} );
:


  1. «» , std::initializer_list .
    — .
  2. ,
    () .

.


1: = {a} , a ,
.


2: , {} .
, initializer_list .
Widget<int> widget{}\ ?


 template Typename<T> struct Widget { Widget(); Widget(std::initializer_list<T>); }; int main() { Widget<int> widget{}; //    ? } 

, , initializer_list , initializer_list . . , , initializer_list . , . , .


{} . , -, , Widget() = default Widget() {} — .


Widget() = default :


 struct Widget { Widget() = default; int i; }; int main() { Widget widget{}; //   (),   vexing parse return widget.i; //  0 } 

Widget() {} :


 struct Widget { Widget() {}; // user-provided  int i; }; int main() { Widget widget{}; //  ,    return widget.i; //  ,  UB } 

: , (narrowing conversions). int double , , :


 int main() { int i{2.0}; // ! } 

, double . C++11, , . :


 struct Widget { int i; int j; }; int main() { Widget widget = {1.0, 0.0}; //   ++11    C++98/03 } 

, , , , (brace elision). , , . , map . map , — :


 std::map<std::string, std::int> my_map {{"abc", 0}, {"def", 1}}; 

, . :


 std::vector<std::string> v1 {"abc", "def"}; // OK std::vector<std::string> v2 {{"abc", "def"}}; // ?? 

, , initializer_list . initializer_list , , , . , . , .


initializer_listinitializer_list , . , const char* . , string , char . . , , .


:


  • ;
  • .

. braced-init-list . :


 Widget<int> f1() { return {3, 0}; // copy-list    } void f2(Widget); f2({3, 0}); // copy-list   

, , braced-init-list . braced-init-list , .


, . StackOverflow , . , . , , :


 #include <iostream> struct A { A() {} A(const A&) {} }; struct B { B(const A&) {} }; void f(const A&) { std::cout << "A" << std::endl; } void f(const B&) { std::cout << "B" << std::endl; } int main() { A a; f( {a} ); // A f( {{a}} ); // ambiguous f( {{{a}}} ); // B f({{{{a}}}}); // no matching function } 


++14


, C++11 . , , . C++14. , .


, ++11 direct member initializers, . , direct member initializers . ++14, direct member initializers:


 struct Widget { int i = 0; int j = 0; }; Widget widget{1, 2}; //    C++14 

, auto . ++11 auto braced-init-list, std::initializer_list :


 int i = 3; // int int i(3); // int int i{3}; // int int i = {3}; // int auto i = 3; // int auto i(3); // int auto i{3}; //  ++11 — std::initializer_list<int> auto i = {3}; //  ++11 — std::initializer_list<int> 

: auto i{3} , int , std::initializer_list<int> . ++14 , auto i{3} int . , . , auto i = {3} std::initializer_list<int> . , : int , — initializer_list .


 auto i = 3; // int auto i(3); // int auto i{3}; //  ++14 — int,         auto i = {3}; //    std::initializer_list<int> 

, C++14 , , , , . , .


, ++14 :


  • , , std::initializer_list .


  • std::initializer_list move-only .


  • c , emplace make_unique .


  • , :


    • , -;
    • ;
    • auto .

  • , , .



: assert(Widget(2,3)) , assert(Widget{2,3}) . , , , . , . .



C++


, ++.


int , . . — , .


: , , std::initializer_list , direct member initializers. , .


, é . .


 struct Point { int x = 0; int y = 0; }; setPosition(Point{2, 3}); takeWidget(Widget{}); 

braced-init-list — .


 setPosition({2, 3}); takeWidget({}); 

, , . , — , . , , , , , . , , initializer_list . : , , .


:


  • = value


  • = {args} = {} :


    • std::initializer_list
    • direct member initialisation ( (args) )

  • {args} {} é


  • (args)



, (args) vexing parse. . 2013 , , auto . , : auto i; — . , :


 auto widget = Widget(2, 3); 

, . , , vexing parse:


 auto thingy = Thingy(); 

« auto» («almost always auto», AAA), ++11 ++14 , , , std::atomic<int> :


 auto count = std::atomic<int>(0); // C++11/14:  // std::atomic is neither copyable nor movable 

, atomic . , , , , . ++17 , , (guaranteed copy elision):


 auto count = std::atomic<int>(0); // C++17: OK, guaranteed copy elision 

auto . — direct member initializers. auto .


++17 CTAD (class template argument deduction). , . . , CppCon, CTAD , . , ++17 , ++11 ++14, , . , , , , .



(++20)


++20, . , , : (designated initialization):


 struct Widget { int a; int b; int c; }; int main() { Widget widget{.a = 3, .c = 7}; }; 

, . , , . , . , b .


, , , . , .


, , 99, :


  • , , . ++ , , . :


     Widget widget{.c = 7, .a = 3}; //  

    , .


  • ++ , {.ce = 7}; , {.c{.e = 7}} :


     Widget widget{.ce = 7}; //  

  • ++ , , :


     Widget widget{.a = 3, 7}; //  

  • ++ . , -, , .


     int arr[3]{.[1] = 7}; //  



C++20


++20 , . ( wg21.link/p1008 ).


++17 , , . , , , :


 struct Widget { Widget() = delete; int i; int j; }; Widget widget1; //  Widget widget2{}; //   C++17,     C++20 

, , . ++20 . , . , . , , , .


( wg21.link/p1009 ). Braced-init-list new , : , ? — , : braced-init-list new :


 double a[]{1, 2, 3}; // OK double* p = new double[]{1, 2, 3}; //   C++17,   C++20 

, ++11 braced-init-list. ++ . , .



(C++20)


, ++20 . , . ++20 : ( wg21.link/p0960 ).


 struct Widget { int i; int j; }; Widget widget(1, 2); //   C++20 

. , emplace make_unique . . : auto , : 58.11 .


 struct Widget { int i; int j; }; auto widget = Widget(1, 2); 

, :


 int arr[3](0, 1, 2); 

, : uniform 2.0. . , , , , . — initializer_list : , , — . , . , - , — . .


, . direct member initializers. auto . direct member initializers — , . , . — , .


, , . — , — . , .



, , C++ Russia 2019 Piter «Type punning in modern C++» . , ++20, , , «» ++ , .

Source: https://habr.com/ru/post/zh-CN469465/


All Articles