解析之C++的列表初始化语法

聚合初始化

先从std::array的内部实现说起。为了让std::array表现得像原生数组,C++中的std::array与其他STL容器有很大区别——std::array没有定义任何构造函数,而且所有内部数据成员都是public的。这使得std::array成为一个聚合(aggregate)。

对聚合的定义,在每个C++版本中有少许的区别,这里简单总结下C++17中定义:一个class或struct类型,当它满足以下条件时,称为一个聚合[1]:

  • 没有private或protected数据成员;
  • 没有用户提供的构造函数(但是显式使用=default或=delete声明的构造函数除外);
  • 没有virtual、private或者protected基类;
  • 没有虚函数

直观的看,聚合常常对应着只包含数据的struct类型,即常说的POD类型。另外,原生数组类型也都是聚合。

聚合初始化可以用大括号列表。一般大括号内的元素与聚合的元素一一对应,并且大括号的嵌套也和聚合类型嵌套关系一致。在C语言中,我们常见到这样的struct初始化语句。

解了上面的原理,就容易理解为什么std::array的初始化在多一层大括号时可以成功了——因为std::array内部的唯一元素是一个原生数组,所以有两层嵌套关系。下面展示一个自定义的MyArray类型,它的数据结构和std::array几乎一样,初始化方法也类似:

struct S {
    int x;
    int y;
};

template<typename T, size_t N>
struct MyArray {
    T data[N];
};

int main()
{
    MyArray<int, 3> a1{{1, 2, 3}};  // 两层大括号
    MyArray<S, 3> a2{{{1, 2}, {3, 4}, {5, 6}}};  // 三层大括号
    return 0;
}

在上面例子中,初始化列表的最外层大括号对应着MyArray,之后一层的大括号对应着数据成员data,再之后才是data中的元素。大括号的嵌套与类型间的嵌套完全一致。这才是std::array严格、完整的初始化大括号写法。

可是,为什么当std::array元素类型是简单类型时,省掉一层大括号也没问题?——这就涉及聚合初始化的另一个特点:大括号省略。

大括号省略(brace elision)

C++允许在聚合的内部成员仍然是聚合时,省掉一层或多层大括号。当有大括号被省略时,编译器会按照内层聚合所含的元素个数进行依次填充。

下面的代码虽然不常见,但是是合法的。虽然二维数组初始化只用了一层大括号,但因为大括号省略特性,编译器会依次用所有元素填充内层数组——上一个填满后再填下一个。

int a[3][2]{1, 2, 3, 4, 5, 6}; // 等同于{{1, 2}, {3, 4}, {5, 6}}

知道了大括号省略后,就知道std::array初始化只用一层大括号的原理了:由于std::array的内部成员数组是一个聚合,当编译器看到{1,2,3}这样的列表时,会挨个把大括号内的元素填充给内部数组的元素。甚至,假设std::array内部有两个数组的话,它还会在填完上一个数组后依次填下一个。

这也解释了为什么省掉内层大括号,复杂类型也可以编译成功:

std::array<S, 3> a3{1, 2, 3, 4, 5, 6};  // 内层不加括号,编译成功

因为S也是个聚合类型,所以这里省略了两层大括号。编译期按照下面的顺序依次填充元素:数组0号元素的S::x、数组0号元素的S::y、数组1号元素的S::x、数组1号元素的S::y……

虽然大括号可以省略,但是一旦用户显式的写出了大括号,那么必须要和这一层的元素个数严格对应。因此下面的写法会报错:

std::array<S, 3> a1{{1, 2}, {3, 4}, {5, 6}};  // 编译失败!

编译器认为{1,2}对应std::array的内部数组,然后{3,4}对应std::array的下一个内部成员。可是std::array只有一个数据成员,于是报错:too many initializers for 'std::array<S, 3>'

需要注意的是,大括号省略只对聚合类型有效。如果S有个自定义的构造函数,省掉大括号就行不通了:

// 聚合
struct S1 {
    S1() = default;
    int x;
    int y;
};

std::array<S1, 3> a1{1, 2, 3, 4, 5, 6};  // OK

// 聚合
struct S2 {
    S2() = delete;
    int x;
    int y;
};

std::array<S2, 3> a2{1, 2, 3, 4, 5, 6};  // OK

// 非聚合,有用户提供的构造函数
struct S3 {
    S3() {};
    int x;
    int y;
};

std::array<S3, 3> a3{1, 2, 3, 4, 5, 6};  // 编译失败!

这里可以看出=default的构造函数与空构造函数的微妙区别。

std::initializer_list的另一个故事

上面讲的所有规则,都只对聚合初始化有效。如果我们给MyArray类型加上一个接受std::initializer_list的构造函数,情况又不一样了:

struct S {
    int x;
    int y;
};

template<typename T, size_t N>
struct MyArray {
public:
    MyArray(std::initializer_list<T> l)
    {
        std::copy(l.begin(), l.end(), std::begin(data));
    }
    T data[N];
};

int main()
{
    MyArray<S, 3> a{{{1, 2}, {3, 4}, {5, 6}}};  // OK
    MyArray<S, 3> b{{1, 2}, {3, 4}, {5, 6}};  // 同样OK
    return 0;
}

当使用std::initializer_list的构造函数来初始化时,无论初始化列表外层是一层还是两层大括号,都能初始化成功,而且a和b的内容完全一样。

这又是为什么?难道std::initializer_list也支持大括号省略?

这里要提一件趣事:《Effective Modern C++》这本书在讲解对象初始化方法时,举了这么一个例子[2]:

class Widget {
public:
  Widget();                                   // default ctor
  Widget(std::initializer_list<int> il);      // std::initializer_list ctor
  …                                          // no implicit conversion funcs
}; 

Widget w1;          // calls default ctor
Widget w2{};        // also calls default ctor
Widget w3();        // most vexing parse! declares a function!    

Widget w4({});      // calls std::initializer_list ctor with empty list
Widget w5{{}};      // ditto <-注意!

然而,书里这段代码最后一行w5的注释却是个技术错误。这个w5的构造函数调用时并非像w4那样传入一个空的std::initializer_list,而是传入包含了一个元素的std::initializer_list。

即使像Scott Meyers这样的C++大牛,都会在大括号的语义上搞错,可见C++的相关规则充满着陷阱!

连《Effective Modern C++》都弄错了的规则

幸好,《Effective Modern C++》作为一本经典图书,读者众多。很快就有读者发现了这个错误,之后Scott Meyers将这个错误的阐述放在了书籍的勘误表中[3]。

Scott Meyers还邀请读者们和他一起研究正确的规则到底是什么,最后,他们把结论写在了一篇文章里[4]。文章通过3种具有不同构造函数的自定义类型,来揭示std::initializer_list匹配时的微妙差异。代码如下:

#include <iostream>
#include <initializer_list>

class DefCtor {
  int x;
public:
  DefCtor(){}
};

class DeletedDefCtor {
  int x;
public:
  DeletedDefCtor() = delete;
};

class NoDefCtor {
  int x;
public:
  NoDefCtor(int){}
};

template<typename T>
class X {
public:
  X() { std::cout << "Def Ctor\n"; }

  X(std::initializer_list<T> il)
  {
    std::cout << "il.size() = " << il.size() << '\n';
  }
};

int main()
{
  X<DefCtor> a0({});           // il.size = 0
  X<DefCtor> b0{{}};           // il.size = 1

  X<DeletedDefCtor> a2({});    // il.size = 0
  // X<DeletedDefCtor> b2{{}};    // error! attempt to use deleted constructor

  X<NoDefCtor> a1({});         // il.size = 0
  X<NoDefCtor> b1{{}};         // il.size = 0
}

对于构造函数已被删除的非聚合类型,用{}初始化会触发编译错误,因此b2的表现是容易理解的。但是b0和b1的区别就很奇怪了:一模一样的初始化方法,为什么一个传入std::initializer_list的长度为1,另一个长度为0?

构造函数的两步尝试

问题的原因在于:当使用大括号初始化来调用构造函数时,编译器会进行两次尝试:

1.把整个大括号列表连同最外层大括号一起,作为构造函数的std::initializer_list参数,看看能不能匹配成功;

2.如果第一步失败了,则将大括号列表的成员作为构造函数的入参,看看能不能匹配成功。

对于b0{{}}这样的表达式,可以直观理解第一步尝试是:b0({{}}),也就是把{{}}整体作为一个参数传给构造函数。对b0来说,这个匹配是能够成功的。因为DefCtor可以通过{}初始化,所以b0的初始化调用了X(std::initializer_list<T>),并且传入含有1个成员的std::initializer_list作为入参。

对于b1{{}},编译器同样会先做第一步尝试,但是NoDefCtor不允许用{}初始化,所以第一步尝试会失败。接下来编译器做第二步尝试,将外层大括号剥掉,调用b1({}),发现可以成功,这时传入的是空的std::initializer_list。

再回头看之前MyArray的例子,现在我们可以分析出两种初始化分别是在哪一步成功的:

MyArray<S, 3> a{{{1, 2}, {3, 4}, {5, 6}}};  // 在第二步,剥掉外层大括号后匹配成功
MyArray<S, 3> b{{1, 2}, {3, 4}, {5, 6}};  // 第一步整个大括号列表匹配成功

综合小测试

到这里,大括号初始化在各种场景下的规则就都解析完了。不知道读者是否彻底掌握了?

不妨来试一试下面的小测试:这段代码里有一个仅含一个元素的std::array,其元素类型是std::tuple,tuple只有一个成员,是自定义类型S,S定义有默认构造函数和接受std::initializer_list<int>的构造函数。对于这个类型,初始化时允许使用几层大括号呢?下面的初始化语句有哪些可以成功?分别是为什么?

struct S {
    S() = default;
    S(std::initializer_list<int>) {}
};

int main()
{
    using MyType = std::array<std::tuple<S>, 1>;
    MyType a{};             // 1层
    MyType b{{}};           // 2层
    MyType c{{{}}};         // 3层
    MyType d{{{{}}}};       // 4层
    MyType e{{{{{}}}}};     // 5层
    MyType f{{{{{{}}}}}};   // 6层
    MyType g{{{{{{{}}}}}}}; // 7层
    return 0;
}

以上就是解析之C++的列表初始化语法的详细内容,更多关于C++的列表初始化语法的资料请关注我们其它相关文章!

(0)

相关推荐

  • C++获取zip文件列表方法

    // ZipFile.h // #ifndef ZIPFILE_H #define ZIPFILE_H #include <string> #include <vector> #define ZIP_OK 0 #define ZIP_ERR_OPEN 1 #define ZIP_ERR_WRONG_FILE 2 #define ZIP_ERR_WRONG_HEADER 3 #define BYTE unsigned char #define ui32 unsigned int #d

  • 简述C++11就地初始化与列表初始化

    1.就地初始化 1.1简介 在C++11之前,只能对结构体或类的静态常量成员进行就地初始化,其他的不行. class C { private: static const int a=10; //yes int a=10; //no } 在C++11中,结构体或类的数据成员在申明时可以直接赋予一个默认值,初始化的方式有两种,一是使用等号"=",二是使用大括号列表初始化的方式.注意,使用参考如下代码: class C { private: int a=7; //C++11 only int

  • 关于C++类的成员初始化列表的相关问题

    在以下四中情况下,要想让程序顺利编译,必须使用成员初始化列表(member initialization list): 1,初始化一个引用成员(reference member): 2,初始化一个常量对象(const member); 3,调用一个基类的构造函数,且该基类的构造函数有一组参数: 4,调用一个成员类(member class)的构造函数,且该构造函数有一组参数 这四种情况程序可以正常编译,但是效率有所欠缺(下面会具体说到). class Word{ String _name; in

  • c++ 构造函数的初始化列表

    首先,运行下图中的C++代码,输出是什么? 复制代码 代码如下: class A{private: int n1; int n2;public: A(): n2(0) , n1(n2 + 2) { } void Print() {  cout<<"n1:"<<n1<<",n2:"<<n2<<endl; }};int main(void){ A a; a.Print(); return 0;} 答案:输出n1

  • C++初始化函数列表详细解析

    在以下三种情况下需要使用初始化成员列表: 一,需要初始化的数据成员是对象的情况: 二,需要初始化const修饰的类成员: 三,需要初始化引用成员数据: 原因:C++可以定义引用类型的成员变量,引用类型的成员变量必须在构造函数的初始化列表中进行初始化.对于类成员是const修饰,或是引用类型的情况,是不允许赋值操作的,(显然嘛,const就是防止被错误赋值的,引用类型必须定义赋值在一起),因此只能用初始化列表对齐进行初始化.成员类型是没有默认构造函数的类.若没有提供显示初始化式,则编译器隐式使用成

  • C++初始化列表学习

    何谓初始化列表与其他函数不同,构造函数除了有名字,参数列表和函数体之外,还可以有初始化列表,初始化列表以冒号开头,后跟一系列以逗号分隔的初始化字段.在C++中,struct和class的唯一区别是默认的克访问性不同,而这里我们不考虑访问性的问题,所以下面的代码都以struct来演示. 复制代码 代码如下: struct foo{    string name ;    int id ;    foo(string s, int i):name(s), id(i){} ; // 初始化列表}; 构

  • C++ 初始化列表详解及实例代码

    C++ 初始化列表 何谓初始化列表 与其他函数不同,构造函数除了有名字,参数列表和函数体之外,还可以有初始化列表,初始化列表以冒号开头,后跟一系列以逗号分隔的初始化字段.在C++中,struct和class的唯一区别是默认的访问性不同,而这里我们不考虑访问性的问题,所以下面的代码都以struct来演示. struct foo { string name ; int id ; foo(string s, int i):name(s), id(i){} ; // 初始化列表 }; 构造函数的两个执行

  • C++11特性小结之decltype、类内初始化、列表初始化返回值

    作用:返回表达式或变量的类型 返回值规则: 若e是一个左值(lvalue,即"可寻址值"),则decltype(e)将返回T& 若e是一个临终值(xvalue),则返回值为T&& 若e是一个纯右值(prvalue),则返回值为T decltype()不会执行括号内的表达式,decltype返回的类型是用于声明的,不能用于单纯的判断.比如decltype(a)==int,是不可以的,只能是在定义新的变量.返回值的地方使用: int a=1; decltype(a)

  • 解析之C++的列表初始化语法

    聚合初始化 先从std::array的内部实现说起.为了让std::array表现得像原生数组,C++中的std::array与其他STL容器有很大区别--std::array没有定义任何构造函数,而且所有内部数据成员都是public的.这使得std::array成为一个聚合(aggregate). 对聚合的定义,在每个C++版本中有少许的区别,这里简单总结下C++17中定义:一个class或struct类型,当它满足以下条件时,称为一个聚合[1]: 没有private或protected数据成

  • 关于C++11的统一初始化语法示例详解

    前言 本文主要给大家介绍了C++11统一初始化语法的相关内容,关于在当前新标准C++11的语法看来,变量合法的初始化器有如下形式: X a1 {v}; X a2 = {v}; X a3 = v; X a4(v); 其实,上面第一种和第二种初始化方式在本质上没有任何差别,添加=则是一种习惯上的行为.使用花括号进行的列表初始化语法,其实早在C++98时代就有了,只不过历史上他们只是被用来对数组元素进行初始化操作,以及初始化自定义POD类型的数据(简单理解就是可以memcpy复制对象的类型).比如:

  • C++11中列表初始化机制的概念与实例详解

    目录 概述 实现机制详解 POD类型的列表初始化 含有构造函数的类的列表初始化(C++11) 列表初始化用于函数返回值 引入std::initializer_list 代码验证 应用 列表初始化防止类型收窄 总结 概述 定义:列表初始化是C++11引入的新标准,目的是统一初始化方式 C++11以前只能使用列表初始化来初始化内置类型数组和POD类型对象,C++11中列表初始化可以用于初始化任何类型对象 POD(plain old data)类型:仅由内置类型变量构成且不含指针的类,简单来说是可以直

  • 实例解析Java中的构造器初始化

    1.初始化顺序 当Java创建一个对象时,系统先为该对象的所有实例属性分配内存(前提是该类已经被加载过了),接着程序开始对这些实例属性执行初始化,其初始化顺序是:先执行初始化块或声明属性时制定的初始值,再执行构造器里制定的初始值. 在类的内部,变量定义的先后顺序决定了初始化的顺序,即时变量散布于方法定义之间,它们仍就会在任何方法(包括构造器)被调用之前得到初始化. class Window { Window(int maker) { System.out.println("Window(&quo

  • C++11新特性之列表初始化的具体使用

    目录 统一的初始化方法 列表初始化的一些使用细节 初始化列表 1.任何长度的初始化列表 2.std::initialzer-list的使用细节 列表初始化防止类型收窄 在我们实际编程中,我们经常会碰到变量初始化的问题,对于不同的变量初始化的手段多种多样,比如说对于一个数组我们可以使用 int arr[] = {1,2,3}的方式初始化,又比如对于一个简单的结构体: struct A { int x; int y; }a={1,2}; 这些不同的初始化方法都有各自的适用范围和作用,且对于类来说不能

  • C++11系列学习之列表初始化

    目录 前言: 旧标准初始化方式 C++11标准初始化方式 初始化列表技术细节 总结 前言: 由于旧标准初始化方式太过繁杂,限制偏多,因此在新标准中统一了初始化方式,为了让初始化具有确定的效果,于是提出了列表初始化概念. 旧标准初始化方式 普通数组初始化: int i_arr[3] = {1, 2, 3} POD类型初始化(即plain old data类型,可以直接使用memcpy复制的对象): struct A { int x; struct B { int i; int j; } b; }

  • 解析Java虚拟机中类的初始化及加载器的父委托机制

    类的初始化 在初始化阶段,Java虚拟机执行类的初始化语句,为类的静态变量赋予初始值. 在程序中,静态变量的初始化有两种途径: 1.在静态变量的声明处进行初始化: 2.在静态代码块中进行初始化. 没有经过显式初始化的静态变量将原有的值. 一个比较奇怪的例子: package com.mengdd.classloader; class Singleton { // private static Singleton mInstance = new Singleton();// 位置1 // 位置1输

  • 关于命令行执行Python脚本的传参方式

    目录 命令行执行Python脚本的传参 应用场景 方式一 方式二 python-命令行传参sys.argv实际运用 argv获取参数 getopt模块 实例 实际场景运用 命令行执行Python脚本的传参 应用场景 在对ABAQUS进行二次开发时,需要将核心脚本的外部数据传递到脚本内部并执行 核心脚本在运行时,可以调用所传递的变量参数 命令行执行或者用户子程序执行 方式一 使用sys.args 简单示例 import sys def test_sys_args():     if len(sys

随机推荐