引言

要是世上不曾存在C++14和C++17该有多好!constexpr是好东西,但是让编译器开发者痛不欲生;新标准库的确好用,但改语法细节未必是明智之举,尤其是3年一次的频繁改动。C++带了太多历史包袱,我们都是为之买账的一员。

我没那么多精力考虑C++14/17的问题,所以本文基于C++11标准。

知其所以然,是学习C++越发复杂的语法的最佳方式。因此,我们从列表初始化的动机讲起。

 

动机

早在2005年,Bjarne Stroustrup就提出要统一C++中的初始化语法。这是因为在C++11以前,初始化存在一系列问题,包括:

  • 4种初始化方式:X t1 = v;X t2(v);X t3 = { v };X t4 = X(v);

  • 聚合(aggregate)初始化;

  • defaultexplicit

  • ……

虽然每一个都有办法解决,但加在一起将会变得非常复杂,对编译器和开发者都是负担。换句话说,唯一的需求就是一种统一的初始化语法,其适用范围能涵盖先前的各种问题。

于是,列表初始化诞生了。

 

语法

正因为列表初始化是为解决初始化问题而生,列表初始化的适用范围是任何初始化。你能想到的都写写看,写对就是赚到。

当然,全凭感觉是行不通的,还是得讲点道理。列表初始化分为两类:直接初始化与拷贝初始化。

在直接初始化中,无论构造函数是否explicit,都有可能被调用:

  1. T object { arg1, arg2, ... };,用arg1, arg2, ...构造T类型的对象object——参数可以是一个值,也可以是一个初始化列表,下同;

  2. Class { T member { arg1, arg2, ... }; };,构造member成员对象——花括号的优势在这里体现出来,因为如果是圆括号的话member会被看作一个函数;

  3. T { arg1, arg2, ... },构造临时对象;

  4. new T { arg1, arg2, ... },构造heap上的对象;

  5. Class::Class() : member{arg1, arg2, ...} {...,成员初始化列表——除了2以外,其余都与用()初始化没有区别。

在拷贝初始化中,无论构造函数是否explicit都会被考虑,但是如果重载决议为一个explicit函数,则此调用错误:

  1. T object = {arg1, arg2, ...};,与直接初始化中的1类似,除了explicit以外都相同,operator=不会被调用;

  2. object = { arg1, arg2, ... },赋值语句,调用operator=

  3. Class { T member = { arg1, arg2, ... }; };,与直接初始化中的2类似,explicit同理;

  4. function( { arg1, arg2, ... } ),构造函数参数;

  5. return { arg1, arg2, ... } ;,构造返回值;

  6. object[ { arg1, arg2, ... } ],构造operator[]的参数;

  7. U( { arg1, arg2, ... } ),构造U构造函数的参数。

4~7可以概括为,在该有一个对象的地方,可以用一个列表来构造它。这句话不是很严谨,因为除了operator()operator[]以外,其他运算符的参数都不能用列表初始化。

还有一个要注意的地方,是列表初始化不允许窄化转换(narrowing conversion),即可能丢失信息的转换,如float转换为int

#include <iostream>
#include <utility> struct Test
{
Test(int, int)
{
std::cout << "Test(int, int)" << std::endl;
}
explicit Test(int, int, int)
{
std::cout << "explicit Test(int, int, int)" << std::endl;
}
void operator[](std::pair<int, int>)
{
std::cout << "void operator[](std::pair<int, int>)" << std::endl;
}
void operator()(std::pair<int, int>)
{
std::cout << "void operator()(std::pair<int, int>)" << std::endl;
}
}; Test test()
{
return { 1, 2 };
} int main()
{
Test t{ 1, 2 };
Test t1 = { 1, 2 };
Test t2 = { 1, 2, 3 }; // error
t[{ 1, 2 }];
t({ 1, 2 });
}

 

initializer_list

列表不是表达式,更不属于任何类型,所以decltype({1, 2})是非法的,这还适用于模板参数推导。但是在以下几种情况中,列表可以转换成std::initializer_list<T>实例:

  1. 直接初始化中,对应构造函数参数类型为std::initializer_list<T>

  2. 拷贝初始化中,对应参数类型为std::initializer_list<T>

  3. 绑定到auto上(列表元素类型必须严格一致),包括范围for(range for)循环——当绑定auto&&时,变量的实际类型为std::initializer_list<T>&&,这是转发引用的特例。

std::initializer_list是为列表初始化提供的特殊的工具,是一个轻量级的数组代理(proxy),其元素类型为const T。虽然你能在<initializer_list>中看到std::initializer_list类模板的实现,但它实际上是与编译器内部绑定的,你无法用一个自己写的相似的类替换它(除非改编译器)。

std::initializer_list有构造函数、sizebeginend函数,用法与其他STL顺序容器类似。迭代器解引用得到const T&类型,元素是不能修改的。

std::initializer_list带来的最明显的进步就是STL容器可以用列表来初始化,无需再写那么多push_back了。

 

重载决议

struct Test
{
Test(int, int)
{
std::cout << "Test(int, int)" << std::endl;
}
Test(std::initializer_list<int>)
{
std::cout << "Test(std::initializer_list<int>)" << std::endl;
}
};

如果我写Test{1, 2},哪个构造函数会被调用呢?回答这个问题,需要对与列表相关的重载决议有所了解。

对于涉及到构造函数的列表初始化(不涉及到的包括聚合初始化等),各构造函数分两个阶段考虑:

  1. 如果有构造函数第一个参数为std::initializer_list,没有其他参数或其他参数都有默认值,则匹配该构造函数(这里似乎允许窄化转换,我测试起来也是如此)——std::initializer_list优先级高

  2. 否则,所有构造函数参与重载决议,除了窄化转换不允许,以及拷贝初始化与explicit的冲突依然有效。

所以上面那段程序中Test{1, 2}会匹配第二个构造函数。

如果有多个std::initializer_list重载呢?众所周知,重载决议中参数转换有完美、提升、转换三个等级,std::initializer_list参数的转换等级定义为所有元素中最差的(不允许窄化转换),然后找出等级最高的调用,如果有多个则为二义调用。

如果没有std::initializer_list重载呢?由于从列表到参数本身就是转换,属于最差的等级,如果有多个函数可以通过参数转换后匹配,则该调用就是二义调用;只有当只有一个函数可行时才合法。

 

总结

列表初始化是一种万能的初始化语法,适用范围广导致其规则比较复杂,我们应当结合其动机来理解标准规定的行为。

列表初始化包括直接初始化与拷贝初始化,后者涵盖了参数与返回值等情形。当我们不想要隐式拷贝初始化时,要用explicit关键字来拒绝。

列表不属于任何类型,但一些情况下可以转换成std::initializer_list。在重载决议中,std::initializer_list有更高的优先级。

C++统一初始化语法(列表初始化)的更多相关文章

  1. initializer_list 列表初始化

    initializer_list 列表初始化 用花括号初始化器列表初始化一个对象,其中对应构造函数接受一个 std::initializer_list 参数. #include <iostrea ...

  2. c++11——列表初始化

    1. 使用列表初始化 在c++98/03中,对象的初始化方法有很多种,例如 int ar[3] = {1,2,3}; int arr[] = {1,2,3}; //普通数组 struct A{ int ...

  3. C++新标准:列表初始化

    一.列表初始化意义 C++新标准为vector提供了一种新的初始化方式:列表初始化.适用于知道多个成员具体值的情况. 二.列表初始化用法 /*1.空vector<int>*/ vector ...

  4. 列表初始化(list initialization)

    列表初始化啊就是大括号来初始化: 列表初始化的好处:

  5. 【ZZ】C++11之统一初始化语法 | 桃子的博客志

    C++11之统一初始化语法 | 桃子的博客志 https://taozj.net/201710/list-initialize.html 在当前新标准C++11的语法看来,变量合法的初始化器有如下形式 ...

  6. 大括号之谜:C++的列表初始化语法解析

    有朋友在使用std::array时发现一个奇怪的问题:当元素类型是复合类型时,编译通不过. struct S { int x; int y; }; int main() { int a1[3]{1, ...

  7. 列表初始化 分析initializer_list<T>的实现

    列表初始化(1)_统一初始化 1. 统一初始化(Uniform Initialization) (1)在C++11之前,很多程序员特别是初学者对如何初始化一个变量或对象的问题很容易出现困惑.因为可以用 ...

  8. C++11常用特性介绍——列表初始化

    一.列表初始化 1)C++11以前,定义初始化的几种不同形式,如下: int data = 0;   //赋值初始化 int data = {0};   //花括号初始化 int data(0); / ...

  9. C++ Union妙用(将列表初始化用于数组元素)

    Union是个不被注意的关键字,意为联合体,这是个诡异的名字.若不是为了继承C语言,它也不会出现在C++中(虽说,union在C++中得到了扩充,完成了接近类的功能).它的作用主要是节省内存空间,在嵌 ...

随机推荐

  1. 如何更换 App icon

    每逢重大节日,App icon 就要跟一波"潮流"做一次更换,节日过后再换回普通.如何保证这两次切换流程丝滑顺畅呢? 应用内需要更换的 icon 包括两处,一个是 App 主 ic ...

  2. STM32F103ZET6的基本定时器

    1.定时器的分类 STM32F103ZET6总共有8个定时器,它们是:TIM1~TIM8.STM32的定时器分为基本定时器.通用定时器和高等定时器. TIM6.TIM7是基本定时器.基本定时器是只能向 ...

  3. 如何优雅的将文件转换为字符串(环绕执行模式&行为参数化&函数式接口|Lambda表达式)

    首先我们讲几个概念: 环绕执行模式: 简单的讲,就是对于OI,JDBC等类似资源,在用完之后需要关闭的,资源处理时常见的一个模式是打开一个资源,做一些处理,然后关闭资源,这个设置和清理阶段类似,并且会 ...

  4. Github使用SSH的步骤

    1) 打开git bash命令行工具 2) 输入下面命令生成一对私钥和公钥 ssh-keygen -t rsa -b 4096 -C "你的github的邮箱" 3) 输入公钥和私 ...

  5. PTA数据结构与算法题目集(中文) 7-36 社交网络图中结点的“重要性”计算 (30 分)

    PTA数据结构与算法题目集(中文)  7-36 社交网络图中结点的“重要性”计算 (30 分) 7-36 社交网络图中结点的“重要性”计算 (30 分)   在社交网络中,个人或单位(结点)之间通过某 ...

  6. 使用Putty登录亚马逊云主机

    亚马逊云主机配置之后会提示 提供一个 pem证书, 打开 SSH 客户端.(了解操作方法 使用 PuTTY 连接) 找到您的私有密钥文件(test001.pem).向导会自动检测您用于启动实例的密钥. ...

  7. API网关--Kong的实践

    1. 什么是Kong 目前互联网后台架构一般是采用微服务,或者类似微服务的形式,应用的请求通常需要访问多个后台系统.如果让每一个后台系统都实现鉴权.限流.负载均衡.审计等基础功能是不合适的,通用的做法 ...

  8. 数据结构和算法(Golang实现)(30)查找算法-2-3-4树和普通红黑树

    文章首发于 阅读更友好的GitBook. 2-3-4树和普通红黑树 某些教程不区分普通红黑树和左倾红黑树的区别,直接将左倾红黑树拿来教学,并且称其为红黑树,因为左倾红黑树与普通的红黑树相比,实现起来较 ...

  9. String 对象-->大小比较

    1.定义和用法 规则:从左至右依次对比相同下标处的字符,当两个字符不相等时,哪个字符的ASCII值大,那就哪个字符串就大. 返回值为1,左边大于右边 返回值为-1,右边大于左边 返回值为0,则相等 举 ...

  10. String 对象-->charAt() 方法

    1.定义和用法 charAt() 方法获取指定下标的字符,下标从0开始 语法: string.charAt(index) 参数: index:指定的下标 举例:获取下标为2的字符 var str = ...