C++统一初始化语法(列表初始化)

简介:

C++统一初始化语法(列表初始化)

引言
要是世上不曾存在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)初始化;
default与explicit;
……
虽然每一个都有办法解决,但加在一起将会变得非常复杂,对编译器和开发者都是负担。换句话说,唯一的需求就是一种统一的初始化语法,其适用范围能涵盖先前的各种问题。

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

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

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

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

T object { arg1, arg2, ... };,用arg1, arg2, ...构造T类型的对象object——参数可以是一个值,也可以是一个初始化列表,下同;
Class { T member { arg1, arg2, ... }; };,构造member成员对象——花括号的优势在这里体现出来,因为如果是圆括号的话member会被看作一个函数;
T { arg1, arg2, ... },构造临时对象;
new T { arg1, arg2, ... },构造heap上的对象;
Class::Class() : member{arg1, arg2, ...} {...,成员初始化列表——除了2以外,其余都与用()初始化没有区别。
在拷贝初始化中,无论构造函数是否explicit都会被考虑,但是如果重载决议为一个explicit函数,则此调用错误:

T object = {arg1, arg2, ...};,与直接初始化中的1类似,除了explicit以外都相同,operator=不会被调用;
object = { arg1, arg2, ... },赋值语句,调用operator=;
Class { T member = { arg1, arg2, ... }; };,与直接初始化中的2类似,explicit同理;
function( { arg1, arg2, ... } ),构造函数参数;
return { arg1, arg2, ... } ;,构造返回值;
object[ { arg1, arg2, ... } ],构造operator[]的参数;
U( { arg1, arg2, ... } ),构造U构造函数的参数。
4~7可以概括为,在该有一个对象的地方,可以用一个列表来构造它。这句话不是很严谨,因为除了operator()和operator[]以外,其他运算符的参数都不能用列表初始化。

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

include

include

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实例:

直接初始化中,对应构造函数参数类型为std::initializer_list;
拷贝初始化中,对应参数类型为std::initializer_list;
绑定到auto上(列表元素类型必须严格一致),包括范围for(range for)循环——当绑定auto&&时,变量的实际类型为std::initializer_list&&,这是转发引用的特例。
std::initializer_list是为列表初始化提供的特殊的工具,是一个轻量级的数组代理(proxy),其元素类型为const T。虽然你能在中看到std::initializer_list类模板的实现,但它实际上是与编译器内部绑定的,你无法用一个自己写的相似的类替换它(除非改编译器)。

std::initializer_list有构造函数、size、begin和end函数,用法与其他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},哪个构造函数会被调用呢?回答这个问题,需要对与列表相关的重载决议有所了解。

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

如果有构造函数第一个参数为std::initializer_list,没有其他参数或其他参数都有默认值,则匹配该构造函数(这里似乎允许窄化转换,我测试起来也是如此)——std::initializer_list优先级高;
否则,所有构造函数参与重载决议,除了窄化转换不允许,以及拷贝初始化与explicit的冲突依然有效。
所以上面那段程序中Test{1, 2}会匹配第二个构造函数。

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

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

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

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

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

原文地址https://www.cnblogs.com/jerry-fuyi/p/12806284.html

相关文章
|
14天前
|
编译器 C语言 C++
C++一分钟之-C++11新特性:初始化列表
【6月更文挑战第21天】C++11的初始化列表增强语言表现力,简化对象构造,特别是在处理容器和数组时。它允许直接初始化成员变量,提升代码清晰度和性能。使用时要注意无默认构造函数可能导致编译错误,成员初始化顺序应与声明顺序一致,且在重载构造函数时避免歧义。利用编译器警告能帮助避免陷阱。初始化列表是高效编程的关键,但需谨慎使用。
25 2
|
2天前
|
编译器 C++ 开发者
C++一分钟之-属性(attributes)与属性语法
【7月更文挑战第3天】C++的属性(attributes)自C++11起允许附加编译器指令,如`[[nodiscard]]`和`[[maybe_unused]]`,影响优化和警告。注意属性放置、兼容性和适度使用,以确保代码清晰和可移植。示例展示了如何使用属性来提示编译器处理返回值和未使用变量,以及利用编译器扩展进行自动清理。属性是提升代码质量的工具,但应谨慎使用。
22 13
|
7天前
|
编译器 程序员 C++
C++一分钟之-属性(attributed)与属性语法
【6月更文挑战第28天】C++的属性为代码添加元数据,帮助编译器理解意图。C++11引入属性语法`[[attribute]]`,但支持取决于编译器。常见属性如`nodiscard`提示检查返回值,`maybe_unused`防止未使用警告。问题包括兼容性、过度依赖和误用。使用属性时需谨慎,确保团队共识,适时更新以适应C++新特性。通过示例展示了`nodiscard`和`likely/unlikely`的用法,强调正确使用属性能提升代码质量和性能。
28 13
|
11天前
|
安全 C++ 开发者
C++一分钟之-RAII资源获取即初始化
【6月更文挑战第24天】RAII是C++中一种关键的资源管理技术,它利用对象生命周期自动获取和释放资源,减少内存泄漏。通过构造函数获取资源,析构函数释放资源,确保异常安全。优势包括自动性、异常安全和代码清晰。使用智能指针如`std::unique_ptr`和`std::shared_ptr`,以及标准库容器,可以避免手动管理。自定义RAII类适用于非内存资源。代码示例展示了智能指针和自定义RAII类如何工作。掌握RAII能提升程序的可靠性和可维护性。
20 6
|
11天前
|
编译器 C语言 C++
|
18天前
|
C++
C++的引用定义语法和应用
C++的引用定义语法和应用
|
4天前
|
编译器 C++
【C++】详解初始化列表,隐式类型转化,类静态成员,友元
【C++】详解初始化列表,隐式类型转化,类静态成员,友元
|
7天前
|
存储 编译器 C++
【C++】类和对象④(再谈构造函数:初始化列表,隐式类型转换,缺省值
C++中的隐式类型转换在变量赋值和函数调用中常见,如`double`转`int`。取引用时,须用`const`以防修改临时变量,如`const int& b = a;`。类可以有隐式单参构造,使`A aa2 = 1;`合法,但`explicit`关键字可阻止这种转换。C++11起,成员变量可设默认值,如`int _b1 = 1;`。博客探讨构造函数、初始化列表及编译器优化,关注更多C++特性。
|
1月前
|
存储 自然语言处理 编译器
【C++语言1】基本语法
【C++语言1】基本语法
|
10天前
|
容器
C++11 列表初始化(initializer_list),pair
C++11 列表初始化(initializer_list),pair