C++20:列表初始化,聚合初始化,强制省略拷贝优化,指定初始化......

何为初始化

变量的类型决定变量所占用的内存大小和布局;变量在创建是获得一个特定的值,我说这个变量被初始化了。

分类及标准

变量的初始化是C++标准的一个重要组成部分,C++初始化按照是否存在拷贝,可将初始化划分为直接初始化和拷贝初始化;其他初始化均可以归类为此两大类。

C++标准中规定,如果变量使用赋值运算符(=)初始化就是拷贝初始化,否则就是直接初始化。

C++标准

按照C++标准引入时间,笔者将变量初始化相关标准汇总如下:

  • C++98标准的直接初始化,拷贝初始化,聚合初始化,圆括号初始化
  • C++11标准的列表初始化和initializer_list;
  • C++17标准的强制省略拷贝优化,列表初始化类型推导,聚合初始化扩展(允许基类)
  • C++20标准的聚合初始化的扩展,指定初始化器初始化。

C++98

C++98标准是C++语言的第一个major版本;此标准引入的初始化包括直接初始化,拷贝初始化,圆括号初始化,花括号以及聚合初始化。

按照前述初始化标准,圆括号初始化属于直接初始化,聚合初始化是一种特殊的拷贝初始化。

直接初始化

未通过拷贝构造实现的初始化就是直接初始化 ,此初始化方式一般通过直接调用构造函数初始化变量。例如:

string s(5,'c'); //  s被初始化为"ccccc"

拷贝初始化

拷贝初始化是相对于直接初始化而言的,拷贝初始化通过调用类型的拷贝构造实现对象初始化。例如:

string name = "liuguang";

在C++98标准中,name赋值基于隐式转换经历了2个步骤:

  • 调用string入参为const char*构造函数,产生临时string变量temp;
  • 执行赋值操作调用入参为temp的string拷贝构造函数,完成name的初始化。

但可惜的是因为编译器优化,我们调试看到却是直接调用构造函数初始化,name初始化会直接调用string入参为const char*构造函数。

所以name初始化本应属于拷贝初始化的,但是优化后会被别人误解为直接初始化。GCC的-fno-elide-constructors和MSVC的/Od可以关闭此编译器优化。

圆括号初始化

圆括号初始化使用()来初始化变量,其是基于使用形式的命名方式,例如:

double d(1.3);    // d被初始化为1.3
string s(5,'c');  // s被初始化为"ccccc"

聚合初始化

聚合初始化一般适用于下述两种初始化场景:

  • 数组初始化
char a[3] = {
    
    'a', 'b', '\0'};
  • 满足下列条件的class,struct或union
    – 所有成员都是public的非静态数据成员
    – 用户没有定义任何构造函数
    – 没有虚成员函数
    – 没有基类
    – 没有类内初始化
struct AggrPerson
{
    
    
	std::string name;
	int age;
};

AggrPerson aggrPerson = {
    
    "liuguang", 20};

花括号初始化

C++98标准中,花括号初始化除了聚合初始化外,仅有一种使用场景。例如

int units{
    
    2};

C++11

C++11标准是C++语言的第二个major版本,在C++历史上具有具足重要的地位。C++98标准存在多种形式的初始化方式,C++11引入了初始化列表使得这一问题得到了根本性改善。

列表初始化

由于C++98标准存在多种形式的初始化方式,从而导致初始化问题变的极为复杂,为了解决C++98存在的棘手问题,C++11中花括号初始化变量得到了全面应用。这种初始化新形式被成为列表初始化。列表初始化,允许我们采用{}方式,统一初始化所有类型。

int a[] = {
    
     1,2,3 };
int aa[]{
    
     1, 2,3 };
std::vector<int> aas{
    
     1,2,3 };
std::unordered_map<std::string, int> az{
    
     {
    
    "1", 1}, {
    
    "2", 2} };

类型收窄

类型收窄一般指可能导致数据变化或者精度丢失的隐式转换。应用于内置变量时,列表初始化有一个重要特点,列表初始化可以防止类型收窄:如果初始化列表初始化变量存在信息丢失风险时,编译器会报错。而且列表初始化是C++11中,唯一可以防止类型收窄的初始化方式,这也是列表初始化区别其他初始化最重要的特点。

// 浮点型到整型的转换
int aVal = 2.0;    // C++98/0x 标准可以通过编译
int aVal2 = {
    
    2.0}; // C++11 标准提示编译错误,类型收窄

// 整型到浮点型的转换
float c = 1e70;    // C++98/0x 标准可以通过编译
float d = {
    
    1e70};  // C++11 标准提示编译错误,类型收窄

可能导致类型收窄的典型场景:

  • 浮点数转化为整数
  • 高精度浮点数转换为低精度浮点数
  • 整型(非强类型枚举)转换为浮点数
  • 整型(非强类型枚举)转换为较低长度的整型

initializer_list

initializer_list由C++11 STL引入,他可以解决任意长度的初始化列表,但要求参数的类型同为T或可隐式转换为T,initializer_list的定义:

template <class T> class initializer_list

在进行函数调用的时候需要使用花括号将所有的参数括起来。例如:

void errorMsg(std::initializer_list<std::string> str)      //可变参数,所有参数类型一致
{
    
    
	for (auto beg = str.begin(); beg != str.end(); ++beg)
		std::cout << *beg << " ";
	std::cout << std::endl;
}

//调用
errorMsg({
    
    "hello","error",error});     // error为string类型
errorMsg({
    
    "hello2",well});             // well为string类型

需要说明的是initializer_list对象中的元素永远是常量值,我们无法改变initializer_list对象中元素的值。

C++17

进一步完善初始化,引入了强制省略拷贝优化,列表初始化类型推导两特性 。列表初始化类型推导是关于auto类型推导,强制省略拷贝优化是关于对象拷贝初始化的优化,聚合初始化扩展(允许基类)。

列表初始化类型推导

C++11的列表初始化规则,在与 auto联合使用时经常无法达到程序员的期望并出错,为此C++17对列表初始化规则进行增强。

C++17对列表初始化的增强,可总结为下面三条规则:

  • auto var {element}; 将 var 推导为 与 element相同的类型
  • auto var {element1, element2, …}; 此形式非法,提示编译错误
  • auto var = {element1, element2, …}; var 推导为 std::initializer_list,其中 T为 element 类型

auto var = {element1, element2, …};
此种类型推导,要求所有element类型必须相同,或者可转换为相同类型。

auto v   {
    
    1};         // 正确:v 推导为 int
auto w   {
    
    1, 2};      // 错误: 初始化列表只能为单元素

/* C++17 增加的新规则 */
auto x = {
    
    1};         // 正确:x 推导为 std::initializer_list<int>
auto y = {
    
    1, 2};      // 正确:y 推导为 std::initializer_list<int>
auto z = {
    
    1, 2, 3.0}; // 错误: 初始化列表中的元素类型必须相同

强制省略拷贝优化

C++标准一直在试图减少某些临时变量或者拷贝的操作,从而实现性能的提升,这是C++标准一直试图解决的问题,C++17之前的编译器有些已经支持了此忽略拷贝操作,但是C++标准却不做强制要求。庆幸的是C++17把省略拷贝写进了标准中,所以从C++17以后忽略拷贝将变成强制要求。

C++17规定,省略拷贝优化会出现在RVO (Return Value Optimization)和NRVO (Named Return Value Optimization),异常捕获以及基于临时构建对象四种场景。

NRVO (Named Return Value Optimization)

如果一个函数返回一个Named Return Value Optimization,这时候强制省略拷贝优化将会生效。

class Thing 
{
    
    
public:
  Thing();
  virtual ~Thing();
  Thing(const Thing&);
};

Thing f() 
{
    
    
  Thing t;
  return t;
}

Thing t2 = f();

上述代码生成的汇编代码(https://godbolt.org/可以帮我们实现代码转换)

f():
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
        mov     rax, QWORD PTR [rbp-8]
        mov     rdi, rax
        call    Thing::Thing() [complete object constructor]
        nop
        mov     rax, QWORD PTR [rbp-8]
        leave
        ret
t2:
        .zero   1
__static_initialization_and_destruction_0():
        push    rbp
        mov     rbp, rsp
        mov     eax, OFFSET FLAT:t2
        mov     rdi, rax
        call    f()
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:t2
        mov     edi, OFFSET FLAT:_ZN5ThingD1Ev
        call    __cxa_atexit
        nop
        pop     rbp
        ret
_GLOBAL__sub_I_f():
        push    rbp
        mov     rbp, rsp
        call    __static_initialization_and_destruction_0()
        pop     rbp
        ret

我们可看出C++17标准下f()只调用了1次对象构造,然后将生成的对象采用move方式移交给t2。忽略了C++17之前函数返回临时变量的生成和构造。

RVO (Return Value Optimization)

C++17中,如果一个函数返回RVO (Return Value Optimization),其同样会被强制省略拷贝优化。

class Thing 
{
    
    
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};

Thing f()
{
    
    
  return Thing();
}

Thing t2 = f();

上述代码生成的汇编代码(https://godbolt.org/可以帮我们实现代码转换)

f():
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
        mov     rax, QWORD PTR [rbp-8]
        mov     rdi, rax
        call    Thing::Thing() [complete object constructor]
        mov     rax, QWORD PTR [rbp-8]
        leave
        ret
t2:
        .zero   1
__static_initialization_and_destruction_0():
        push    rbp
        mov     rbp, rsp
        mov     eax, OFFSET FLAT:t2
        mov     rdi, rax
        call    f()
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:t2
        mov     edi, OFFSET FLAT:_ZN5ThingD1Ev
        call    __cxa_atexit
        nop
        pop     rbp
        ret
_GLOBAL__sub_I_f():
        push    rbp
        mov     rbp, rsp
        call    __static_initialization_and_destruction_0()
        pop     rbp
        ret

我们可看出C++17标准下f()只调用了1次对象构造,然后将生成的对象采用move方式移交给t2。忽略了C++17之前函数返回临时变量的生成和构造。实现形式和NRVO一样。

Constructed from a temporary

如果一个对象由临时变量构造时,这时候同样会被强制省略拷贝优化。参考举例:

class Thing 
{
    
    
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};

Thing t2 = Thing();
Thing t3 = Thing(Thing());

其生成的汇编代码(https://godbolt.org/可以帮我们实现代码转换)

t2:
        .zero   1
t3:
        .zero   1
__static_initialization_and_destruction_0():
        push    rbp
        mov     rbp, rsp
        mov     edi, OFFSET FLAT:t2
        call    Thing::Thing() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:t2
        mov     edi, OFFSET FLAT:_ZN5ThingD1Ev
        call    __cxa_atexit
        mov     edi, OFFSET FLAT:t3
        call    Thing::Thing() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:t3
        mov     edi, OFFSET FLAT:_ZN5ThingD1Ev
        call    __cxa_atexit
        nop
        pop     rbp
        ret
_GLOBAL__sub_I_t2:
        push    rbp
        mov     rbp, rsp
        call    __static_initialization_and_destruction_0()
        pop     rbp
        ret

从上述汇编代码,我们可以得出这样的结论:t2和t3都都是,首先完成一次对象构造,然后通过move的方式将对象移交给他们。中间忽略了多次的对象拷贝构造。

Exception object caught by value

异常抛出以值的方式捕获时,强制省略拷贝优化将会发生。参考举例:

struct Thing
{
    
    
  Thing();
  Thing(const Thing&);
};
 
void foo() 
{
    
    
  Thing c;
  throw c;
}
 
int main() 
{
    
    
  try 
  {
    
    
    foo();
  }
  catch(Thing c) 
  {
    
      
  }             
}

其生成的汇编代码(https://godbolt.org/可以帮我们实现代码转换)

foo():
        push    rbp
        mov     rbp, rsp
        push    r12
        push    rbx
        sub     rsp, 16
        lea     rax, [rbp-17]
        mov     rdi, rax
        call    Thing::Thing() [complete object constructor]
        mov     edi, 1
        call    __cxa_allocate_exception
        mov     rbx, rax
        lea     rax, [rbp-17]
        mov     rsi, rax
        mov     rdi, rbx
        call    Thing::Thing(Thing const&) [complete object constructor]
        mov     edx, 0
        mov     esi, OFFSET FLAT:typeinfo for Thing
        mov     rdi, rbx
        call    __cxa_throw
        mov     r12, rax
        mov     rdi, rbx
        call    __cxa_free_exception
        mov     rax, r12
        mov     rdi, rax
        call    _Unwind_Resume
main:
        push    rbp
        mov     rbp, rsp
        push    rbx
        sub     rsp, 24
        call    foo()
.L8:
        mov     eax, 0
        jmp     .L10
        mov     rbx, rax
        mov     rax, rdx
        cmp     rax, 1
        je      .L7
        mov     rax, rbx
        mov     rdi, rax
        call    _Unwind_Resume
.L7:
        mov     rax, rbx
        mov     rdi, rax
        call    __cxa_get_exception_ptr
        mov     rdx, rax
        lea     rax, [rbp-17]
        mov     rsi, rdx
        mov     rdi, rax
        call    Thing::Thing(Thing const&) [complete object constructor]
        mov     rax, rbx
        mov     rdi, rax
        call    __cxa_begin_catch
        call    __cxa_end_catch
        jmp     .L8
.L10:
        mov     rbx, QWORD PTR [rbp-8]
        leave
        ret
typeinfo for Thing:
        .quad   vtable for __cxxabiv1::__class_type_info+16
        .quad   typeinfo name for Thing
typeinfo name for Thing:
        .string "5Thing"

分析汇编代码,我们可发现catch(Thing c)中c对象由void foo()的c对象通过直接拷贝构造而来,只进行了一次拷贝构造,所以此处强制省略拷贝优化是生效的。

允许继承的聚合初始化

从C++17开始,聚合还可以有基类,可以初始化从其他类/结构体派生而来的结构体:

struct NewPerson: AggrPerson
{
    
    
    bool isMan;
};

NewPerson person{
    
    {
    
    "liuguang", 20}, true};

聚合初始化现在支持以嵌套括号传递初始值给基类的成员变量,嵌套括号也是可以省略的:

NewPerson person{
    
    "liuguang", 20, true};

从C++17开始,聚合定义如下:

  • 可以是一个数组
  • 或是一自定义类型(类、结构体、或联合),但需要:
    – 没有用户定义或显式的构造函数
    – 没有通过using声明继承而来的构造函数
    – 没有private或protected的非静态成员变量
    – 没有虚函数
    – 没有virtual,private或protected的基类

要能使用聚合,它还必须在初始化过程中没有private或protected的基类成员或构造函数。

struct A   // C++17聚合体
{
    
     
	A() = delete;
};
struct B   // C++17聚合体
{
    
     
	B() = default;
	int i = 0;
};
struct C  // C++17聚合体
{
    
    
	C(C&&) = default;
	int a, b;
};

A a{
    
    }; // C++17合法
B b = {
    
    1};  // C++17合法
auto* c = new C{
    
    2, 3};  // C++17合法

C++17引入了新的类型粹取is_aggregate<>用于测试某个类型是否是聚合。

template<typename T>
struct D : std::string, std::complex<T> 
{
    
    
    std::string data;
};


D<float> s{
    
    {
    
    "hello"}, {
    
    4.5,6.7}, "world"};  // C++17开始正确
std::cout << std::is_aggregate<decltype(s)>::value; // 输出: 1

C++20

C++20对聚合初始化,又有了深入进一步的优化提升,具体优化包括如下两个方面:第一,禁止聚合体有用户声明的构造函数,第二,兼容C语言的指定初始化。

禁止聚合体构造函数

C++20 [P1008R1]提议要求只要声明了构造函数,那么此class&struct将不是一个聚合体。例如:

struct A   // C++17及以前是聚合体,C++20 非聚合体
{
    
     
	A() = delete;
};
struct B   // C++17及以前是聚合体,C++20 非聚合体
{
    
     
	B() = default;
	int i = 0;
};
struct C  // C++17及以前是聚合体,C++20 非聚合体
{
    
    
	C(C&&) = default;
	int a, b;
};

A a{
    
    }; // C++20不合法,C++17及以前合法
B b = {
    
    1};  // C++20不合法,C++17及以前合法
auto* c = new C{
    
    2, 3};  // C++20不合法,C++17及以前合法

指定初始化

我们可以使用如下两种C++20支持的指定初始化方式:

T 对象 = {
    
     .指定符1 = 实参1 , .指定符2 {
    
     实参2 } ... };
T 对象 {
    
     .指定符1 = 实参1 , .指定符2 {
    
     实参2 } ... };
  • 每个指定符必须指名T的一个直接非静态数据成员,而表达式中所用的所有指定符必须按照与 T 的数据成员相同的顺序出现。例如:
struct A 
{
    
     
	int x; 
	int y; 
	int z; 
};
 
A a{
    
    .y = 2, .x = 1}; // Error:指定符的顺序不匹配声明顺序
A b{
    
    .x = 1, .z = 2}; // OK:b.y 被初始化为 0
  • 指定初始化器所指名的每个直接非静态数据成员,从它的指定符后随的对应花括号或等号指定器初始化。禁止窄化转换。
  • 指定初始化器可以用来将联合体初始化为它的首个成员之外的状态。只可以为一个联合体提供一个初始化器。
union u 
{
    
     
	int a; 
	const char* b; 
};
 
u f = {
    
    .b = "asdf"};         // OK:联合体的活跃成员是 b
u g = {
    
    .a = 1, .b = "asdf"}; // Error:只可提供一个初始化器
  • 对于非联合体的聚合体中未提供指定初始化器的元素,按上述针对初始化器子句的数量少于成员数量时的规则进行初始化(如果提供默认成员初始化器就使用它,否则用空列表进行初始化)
struct A
{
    
    
    string str;
    int n = 42;
    int m = -1;
};
 
A{
    
    .m = 21}  // 以 {} 初始化 str,这样会调用默认构造函数
            // 然后以 = 42 初始化 n
            // 然后以 = 21 初始化 m
  • 如果指定初始化器子句初始化的聚合体拥有一个匿名union成员,那么对应的指定初始化器必须指名该匿名union的其中一个成员。
struct C
{
    
    
    union
    {
    
    
        int a;
        const char* p;
    };
 
    int x;
} c = {
    
    .a = 1, .x = 3}; // 以 1 初始化 c.a 并且以 3 初始化 c.x
  • 乱序的指定初始化、嵌套的指定初始化、指定初始化器与常规初始化器的混合,以及数组的指定初始化在 C 编程语言中受支持,但在 C++ 不允许。
struct A {
    
     int x, y; };
struct B {
    
     struct A a; };
 
struct A a = {
    
    .y = 1, .x = 2}; // C 中合法,C++ 中非法(乱序)
int arr[3] = {
    
    [1] = 5};        // C 中合法,C++ 中非法(数组)
struct B b = {
    
    .a.x = 0};       // C 中合法,C++ 中非法(嵌套)
struct A a = {
    
    .x = 1, 2};      // C 中合法,C++ 中非法(混合)

总结

变量的初始化是C++标准的一个重要组成部分,C++初始化按照是否存在拷贝,可将初始化划分为直接初始化和拷贝初始化;其他初始化均可以归类为此两大类。本文围绕此分类标准,分别介绍了:C++98标准的直接初始化,拷贝初始化,聚合初始化,圆括号初始化;C++11标准的列表初始化和initializer_list; C++17标准的强制省略拷贝优化,列表初始化类型推导,聚合初始化扩展(允许基类);C++20标准的聚合初始化的扩展,指定初始化器初始化。

猜你喜欢

转载自blog.csdn.net/liuguang841118/article/details/127912564