广告
返回顶部
首页 > 资讯 > 后端开发 > 其他教程 >解析之C++的列表初始化语法
  • 742
分享到

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

2024-04-02 19:04:59 742人浏览 八月长安
摘要

目录聚合初始化大括号省略(brace elision)std::initializer_list的另一个故事连《Effective Modern 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++的列表初始化语法的资料请关注编程网其它相关文章!

--结束END--

本文标题: 解析之C++的列表初始化语法

本文链接: https://www.lsjlt.com/news/126385.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • 解析之C++的列表初始化语法
    目录聚合初始化大括号省略(brace elision)std::initializer_list的另一个故事连《Effective Modern C++》都弄错了的规则构造函数的两步...
    99+
    2022-11-12
  • C++学习之初始化列表详解
    目录前言一、类的初始化表二、initializer_list前言 本文主要介绍C++中地初始化列表 目前对初始化列表应该有两个方面的定义,一个是类的构造函数中使用的那个初始化表,另一...
    99+
    2023-03-19
    C++初始化列表 C++ 列表
  • C++初始化函数列表详细解析
    在以下三种情况下需要使用初始化成员列表: 一,需要初始化的数据成员是对象的情况; 二,需要初始化const修饰的类成员; 三,需要初始化引用成员数据; 原因:C++可以定义引用类型的...
    99+
    2022-11-15
    初始化函数列表
  • C++11系列学习之列表初始化
    目录前言:旧标准初始化方式C++11标准初始化方式初始化列表技术细节总结前言: 由于旧标准初始化方式太过繁杂,限制偏多,因此在新标准中统一了初始化方式,为了让初始化具有确定的效果,于...
    99+
    2022-11-13
  • C++学习笔记之初始化列表
    目录一、用初始化列表初始化对象1.初始化列表用法2.初始化列表特性二、explicit关键字1.内置类型的隐式转换2.如何避免单参构造函数初始化发生隐式类型转换三、匿名对象1.匿名对...
    99+
    2023-05-17
    c++ 初始化列表 如何初始化列表 c++ 初始化
  • C++示例讲解初始化列表方法
    目录定义特性初始化阶段计算阶段成员变量的初始化顺序定义 我们先来看一个例子 Date(int year, int month, int day)//带参构造函数 :_...
    99+
    2022-11-13
  • C++深入讲解初始化列表的用法
    目录一、小问题二、类成员的初始化三、类中的 const 成员四、初始化与赋值的不同五、小结一、小问题 下面的类定义是否合法 如果合法,ci 的值是什么,存储在哪里 下面编写代码一探...
    99+
    2022-11-13
  • c++基础语法:构造函数初始化列表
    C++为类中提供类成员的初始化列表 类对象的构造 顺序是这样的:1.分配内存,调用构造函数 时,隐式/显示的初始化各数据 成员2.进入构造函数后在构造函数中执行一般计算 使用初始化...
    99+
    2022-11-15
    构造函数 初始化列表
  • C++初始化列表的方法有哪些
    本篇内容介绍了“C++初始化列表的方法有哪些”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!一、类的初始化表首先是类中使用构造函数时的初始化表...
    99+
    2023-07-05
  • C++之谈谈构造函数的初始化列表
    目录一、引入二、初始化的概念区分三、语法格式及使用四、注意事项【⭐】五、总结与提炼一、引入 我们知道,对于下面这个类A的成员变量_a1和_a2属于【声明】,还没有在内存中为其开辟出一...
    99+
    2023-05-15
    C++构造函数 C++构造函数初始化列表 C++初始化列表
  • C++构造函数的初始化列表详解
    目录1.问题2.解决方法(初始化列表)3.顺序问题总结 1.问题 class A { private: int m_a; public: A(int a) { cout ...
    99+
    2022-11-12
  • C++11列表初始化是怎样的
    这篇文章主要介绍“C++11列表初始化是怎样的”,在日常操作中,相信很多人在C++11列表初始化是怎样的问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”C++11列表初始化是怎样的”的疑惑有所帮助!接下来,请跟...
    99+
    2023-06-19
  • C++11新特性之列表初始化的具体使用
    目录统一的初始化方法列表初始化的一些使用细节初始化列表1、任何长度的初始化列表2、std::initialzer-list的使用细节列表初始化防止类型收窄在我们实际编程中,我们经常会...
    99+
    2022-11-13
  • C++11中初始化列表initializer lists的使用方法
    C++11引入了初始化列表来初始化变量和对象。自定义类型,如果想用初始化列表就要包含initializer_list头文件。 C++11将使用大括号的初始化(列表初始化)作为一种通用...
    99+
    2022-11-12
  • C++构造函数初始化列表的实现详解
    目录1.前言2.初始化列表3.注意事项1.前言 初始化就是给变量一个初始值。 初始化的目的是为了让变量有值,防止使用时出现异常。 在构造函数中,有一项重要功能就是对成员变量进行初始化...
    99+
    2022-11-13
  • 如何解析ThinkPHP5之 _initialize()初始化方法
    小编给大家分享一下如何解析ThinkPHP5之 _initialize()初始化方法,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!ThinkPHP5之 _initialize() 初始化方法详解前言_initialize(...
    99+
    2023-06-14
  • C++11关联容器的列表怎么初始化
    本篇内容介绍了“C++11关联容器的列表怎么初始化”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!什么是关联容器关联容器(associativ...
    99+
    2023-06-19
  • C++是怎么构造函数的初始化列表
    C++是怎么构造函数的初始化列表,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。1.问题class A {private:int m_a;publi...
    99+
    2023-06-22
  • C++11中列表初始化机制的概念与实例详解
    目录概述 实现机制详解 POD类型的列表初始化 含有构造函数的类的列表初始化(C++11) 列表初始化用于函数返回值 引入std::initializer_list 代码验证 应用 ...
    99+
    2022-11-12
  • C++11中列表初始化机制的概念是什么
    本篇内容介绍了“C++11中列表初始化机制的概念是什么”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!概述定义:列表初始化是C++11引入的新...
    99+
    2023-06-25
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作