使用现代C ++和标准的每个修订版,我们都可以采用更舒适的方式来初始化类的字段:静态和非静态:存在非静态数据成员初始化(来自C ++ 11)和内联变量(用于静态) C ++ 17以来的成员)。
在此博客文章中,您将学习如何使用语法以及从C ++ 11到C ++ 14,C ++ 17到C ++ 20多年来的语法变化。
数据成员的初始化
在C ++ 11之前,如果您有一个类成员,则只能通过构造函数中的初始化列表将其初始化为默认值。
// pre C++11 class:struct SimpleType { int field; std::string name; SimpleType() : field(0), name("Hello World") { }}
从C ++ 11开始,语法得到了改进,您可以进行初始化field并name代替声明:
// since C++11:struct SimpleType { int field = 0; // works now! std::string name { "Hello World "} // alternate way with { } SimpleType() { }}
如您所见,变量将在声明位置获得其默认值。无需在构造函数内设置值。
该功能称为*非静态数据成员初始化*或简称NSDMI。
更重要的是,自C ++ 17起,我们可以使用内联变量来初始化静态数据成员:
struct OtherType { static const int value = 10; static inline std::string className = "Hello Class"; OtherType() { }}
现在,无需className在相应的cpp文件中进行定义。编译器保证所有编译单元只能看到静态成员的一个定义。以前,在C ++ 17之前,您必须将定义放入cpp文件之一。
请注意,对于常量整数静态字段(value),即使在C ++ 98中,我们也可以“就地”初始化它们。
让我们探索这些有用的功能:NSDMI和内联变量。我们将看到示例以及这些年来这些功能如何改进。
NSDMI-非静态数据成员初始化
简而言之,编译器将对字段进行初始化,就像您将其写入构造函数初始化程序列表中一样。
SimpleType() : field(0) { }
让我们详细了解一下:
怎么运行的
只需一点“机器”,我们就可以看到编译器何时执行初始化。
让我们考虑以下类型:
struct SimpleType{ int a { initA() }; int b { initB() }; // ...};
initA()和initB()函数的实现 有副作用,它们会记录其他消息:
int initA() { std::cout << "initA() called"; return 1;}std::string initB() { std::cout << "initB() called"; return "Hello";}
这使我们可以看到何时调用代码。
例如:
struct SimpleType{ int a { initA() }; std::string b { initB() }; SimpleType() { } SimpleType(int x) : a(x) { }};
并使用:
std::cout << "SimpleType t10"; SimpleType t0;std::cout << "SimpleType t1(10)"; SimpleType t1(10);
输出:
SimpleType t0:initA() calledinitB() calledSimpleType t1(10):initB() called
t0 是默认初始化的,因此两个字段都使用其默认值初始化。
在第二种情况下,对于t1,只有一个值是默认初始化的,而另一个则来自构造函数参数。
您可能已经猜到了,编译器将对字段进行初始化,就像在“成员初始化列表”中初始化字段一样。因此,它们在调用构造函数的主体之前会获取默认值。
换句话说,编译器将扩展代码:
int a { initA() }; std::string b { initB() }; SimpleType() { }SimpleType(int x) : a(x) { }
进入
int a; std::string b; SimpleType() : a(initA()), b(initB()) { }SimpleType(int x) : a(x), b(initB()) { }
其他构造函数呢?
复制和移动构造函数
编译器将在所有构造函数中执行字段的初始化,包括复制和移动构造函数。但是,当复制或移动构造函数为默认值时,则无需执行该额外的初始化。
请参阅示例:
struct SimpleType{ int a { initA() }; std::string b { initB() }; SimpleType() { } SimpleType(const SimpleType& other) { std::cout << "copy ctor"; a = other.a; b = other.b; };};
和用例:
SimpleType t1;std::cout << "SimpleType t2 = t1:";SimpleType t2 = t1;
输出:
SimpleType t1:initA() calledinitB() calledSimpleType t2 = t1:initA() calledinitB() calledcopy ctor
在这里查看代码@Wandbox。
在上面的示例中,编译器使用其默认值初始化了这些字段。这就是为什么最好在复制构造函数中使用初始化程序列表的原因:
SimpleType(const SimpleType& other) : a(other.a), b(other.b) { std::cout << "copy ctor"; };
我们得到:
SimpleType t1:initA() calledinitB() calledSimpleType t2 = t1:copy ctor
如果您依赖于编译器生成的默认副本构造函数,则会发生相同的情况:
SimpleType(const SimpleType& other) = default;
移动构造函数也会发生同样的事情。
NSDMI的优势
- 容易写
- 您确定每个成员都正确初始化。
- 声明和默认值在同一位置
- 当我们有几个构造函数时特别有用。
- 以前,我们将不得不为成员复制初始化代码,或者编写一个自定义方法InitMembers(),该方法将在构造函数中调用。
- 现在,您可以执行默认初始化,构造函数将仅执行其特定工作…
NSDMI是否有负面影响?
很难提出缺点,但让我们尝试:
- 性能:当您具有性能关键型数据结构(例如Vector3D类)时,可能需要使用“空”初始化代码。您可能会有未初始化的数据成员的风险,但是您将保存一些说明。
- 使类在C ++ 11中不聚合,但在C ++ 14中不聚合。请参阅有关C ++ 14更改的部分。
- 由于默认值位于头文件中,因此任何更改都可能导致需要重新编译依赖的编译单元。如果仅在实现文件中设置值,则不是这种情况。感谢Yehezkel在评论中提到它!这个缺点也适用于静态变量,我们将在后面讨论。
您还有其他问题吗?
聚合,NSDMI的C ++ 14更新
最初,在C ++ 11中,如果您使用默认成员初始化,则您的类不能是聚合类型:
struct Point { float x = 0.0f; float y = 0.0f; };// won't compile in C++11Point myPt { 10.0f, 11.0f};
我不知道这个问题,但是Shafik Yaghmour在文章下面的评论中指出了这一点。
在C ++ 11规范中,不允许聚合类型进行此类初始化,但是在C ++ 14中,此要求已删除。链接到带有详细信息的StackOverflow问题
幸运的是,它已在C ++ 14中修复,因此
Point myPt { 10.0f, 11.0f};
如预期编译,请参阅@Wandbox
位域的C ++ 20更新
从C ++ 11开始,代码仅考虑“常规”字段……但是类中的位字段又如何呢?
class Type { unsigned int value : 4;};
这只是C ++ 20中的最新更改,使您可以编写:
class Type { unsigned int value : 4 = 0; unsigned int second : 4 { 10 };};
C ++ 20接受的建议是C ++ 20 P0683的默认位字段初始化程序。
内联变量C ++ 17
到目前为止,我们讨论了非静态数据成员。对于在类中声明和初始化静态变量,我们是否有任何改进?
在C ++ 11/14中,您必须在相应的cpp文件中定义一个变量:
// a header file:struct OtherType { static int classCounter; // ...};// implementation, cpp fileint OtherType::classCounter = 0;
幸运的是,对于C ++ 17,我们还获得了内联变量,这意味着您可以static inline在类内定义变量,而无需在cpp文件中定义它们。
// a header file, C++17:struct OtherType { static inline int classCounter = 0; // ...};
编译器保证为所有包含类声明的翻译单元精确定义一个静态变量。内联变量仍然是静态类变量,因此它们将在main()调用函数之前进行初始化(您可以在我的独立文章中阅读更多内容,在程序开始时静态变量会发生什么?)。
该功能使开发仅标头的库变得容易得多,因为无需为静态变量创建cpp文件或使用一些技巧将它们保存在标头文件中。
与案 auto
由于我们可以在类中声明和初始化变量,因此存在一个有趣的问题auto。我们可以使用吗?这似乎是很自然的方法,并且会遵循AAA(几乎始终为自动)规则。
您可以使用auto静态变量:
class Type { static inline auto theMeaningOfLife = 42; // int deduced};
但不是作为类的非静态成员:
class Type { auto myField { 0 }; // error auto param { 10.5f }; // error };
不幸的是,auto不支持。例如在海湾合作委员会,我得到
error: non-static data member declared with placeholder 'auto'
虽然静态成员只是静态变量,这就是为什么编译器推断类型相对容易,但对于常规成员而言却不那么容易。主要是因为类型和类布局可能会循环依赖。如果您对全文感兴趣,可以在cor3ntin博客上阅读以下出色的解释:自动非静态数据成员初始化程序的案例| cor3ntin。
CTAD案例-类模板参数推导
同样,auto对于非静态成员变量和CTAD ,我们也有一些限制:
它适用于静态变量:
class Type { static inline std::vector ints { 1, 2, 3, 4, 5, 6, 7}; // deduced vector};
但不是作为非静态成员:
class Type { std::vector ints { 1, 2, 3, 4, 5, 6, 7}; // error!};
在GCC 10.0上我得到
error: 'vector' does not name a type
编译器支持
摘要
在本文中,我们回顾了现代C ++如何改变类内成员的初始化。
在C ++ 11中,我们获得了NSDMI-非静态数据成员初始化。现在,您可以声明一个成员变量,并使用默认值对其进行初始化。初始化将在构造函数初始化列表中的每个构造函数主体被调用之前进行。
NSDMI在C ++ 14(聚合)和C ++ 20(现在支持位字段)中得到了改进。
更重要的是,在C ++ 17中,我们获得了内联变量,这意味着您可以声明并初始化静态成员,而无需在相应的cpp文件中执行此操作。
这是一个结合了这些功能的“摘要”示例:
struct Window{ inline static unsigned int default_width = 1028; inline static unsigned int default_height = 768; unsigned int _width { default_width }; unsigned int _height { default_height }; unsigned int _flags : 4 { 0 }; std::string _title { "Default Window" }; Window() { } Window(std::string title) : _title(std::move(title)) { } // ...};
为了简单起见default_width,default_height它们是静态变量,例如可以从配置文件中加载这些静态变量,然后使用它们来初始化默认的Window状态。
轮到你了
您在项目中使用NSDMI吗?您是否将静态Inline变量用作类成员?
您是否在代码中使用它?
越是优化代码,越是演练,越是思考,就越能发现C/C++的优势所在。