初始化|这些年踩过的坑原创
你好,我是雨乐!
最近在整理Modern CPP的某些新特性,恰好到了这块,所以就聊聊咯~~
统一初始化又称为列表初始化,自C++11引入,使用花括号(Brace-initialization)方式,主要目的是为了简化和统一不同的初始化方式,提高代码的可读性和可维护性,同时减少了某些特殊情况下可能出现的二义性。是Modern C++开发人员最应该了解和掌握的新特性之一。它的出现,消除了以前在初始化基本类型、聚合类型和非聚合类型、以及数组和标准容器之间的区别,以提供更一致的初始化语法。
目的
在C++11之前,初始化对象的方式有多种,包括:
1.直接初始化:Type variable(value);
2.拷贝初始化:Type variable = value;
3.列表初始化:Type variable{value};
或 Type variable = {value};
4.默认初始化:Type variable;
这些初始化方式依赖于其具体类型
•对于基础类型,则可以使用赋值方式直接初始化
int a = 42;
double b = 1.2;
•对于类类型,在其只有一个参数的情况下,也可以使用赋值方式进行初始化
class foo
{
int a_;
public:
foo(int a):a_(a) {}
};
foo f1 = 42;
•对于非聚合类,也可以使用后面跟括号的方式(括号中传入参数),对于不需要参数的则不能添加括号,否则编译器会认为是函数声明
foo f1; // default initialization
foo f2(42, 1.2);
foo f3(42);
foo f4(); // function declaration
•聚合类可以通过花括号的方式进行初始化
bar b = {42, 1.2};
int a[] = {1, 2, 3, 4, 5};
除了以上初始化方式之外,对于标准容器来说,都是先声明一个对象,然后通过插入的方式进行初始化,不过,std::vector是个例外,其可以从先前使用聚合初始化初始化的数组中分配,如下:
nt arr[] = {1, 2, 3, 4, 5}; // 使用聚合初始化初始化数组
std::vector<int> vec(std::begin(arr), std::end(arr)); // 使用数组的值初始化 std::vector
用法
在上节中,我们看到在C++11之前有多种初始化方式,开发人员往往需要对每种的场景都需要了解,以防止性能损失或者编译错误,正是为了解决这个问题,自C++11起,引入了统一初始化(List initialization或者Uniform initialization)。
统一初始化,用{}方式进行初始化,如下:
T object {other};
T object = {other};
下面是关于统一初始化的一些例子:
•标准库中的容器
std::vector<int> v { 1, 2, 3 };
std::map<int, std::string> m { {1, "one"}, { 2, "two" }};
•动态数组分配
int* arr2 = new int[3]{ 1, 2, 3 };
•数组
int arr1[3] { 1, 2, 3 };
•内置类型
int i { 42 };
double d { 1.2 };
•自定义类型
class foo
{
int a_;
double b_;
public:
foo():a_(0), b_(0) {}
foo(int a, double b = 0.0):a_(a), b_(b) {}
};
foo f1{};
foo f2{ 42, 1.2 };
foo f3{ 42 };
•POD类型
struct bar { int a_; double b_;};
bar b{ 42, 1.2 };
一些细节
在前面的两节中,分别讲解了Modern C++之前的初始化方式以及统一初始化方式,从使用方式上来看,更加统一,显然统一初始化是我们进行初始化时候的首选,当然了,需要注意一些细节,尤其是对于存在参数为std::initializer_list的容器类型来说。
// a vector containing two elements: 10 and 20
std::vector<int> v{10, 20};
// a vector containing 10 elements: all 20
std::vector<int> w(10, 20);
emm!!上述代码的区别其实已经在注释里面讲了,对于v来说用的是列表初始化方式,其构建了一个vector,里面有2个元素10和20;对于w,其也是构建了一个vector,里面有10个元素,且每个元素的值都为20,下面是STL中这块的源码:
vector(size_type __n, const _Tp& __value,
const _Allocator& __a = _Allocator())
: _Base(__n, __value, __a), _M_guaranteed_capacity(__n) { }
vector(initializer_list<value_type> __l,
const allocator_type& __a = allocator_type())
: _Base(__l, __a), _Safe_base(),
_M_guaranteed_capacity(__l.size()) { }
模板
继续看个例子:
template <class T>
T copy(T const& val) {
return T{val};
}
auto a = copy(std::string{});
auto b = copy(std::vector<int>{});
auto c = copy(std::vector<std::any>{});
好了,请闭眼思考下,看看上面abc的内容分别是什么?
首先,创建了一个模板函数copy,其内部实现就是用返回一个参数的拷贝,需要注意的是使用的统一初始化的方式。
a是一个空字符串的拷贝,b是一个空std::vector 的拷贝,那么c会不会像b一样,也是空std::vector<>的拷贝呢?确实,其类型为std::vector<std::any>,但是,size却不是0,而是1,这是因为std::any可以是任何类型变量的原因~
接着看另外一个例子:
template<typename T>
std::vector<T> create()
{
return std::vector<T>{10};
}
int main()
{
auto a = create<std::string>();
auto b = create<int>();
auto c = create<char>();
auto d = create<std::vector<int>>();
std::cout << a.size() << " " << b.size() << " " << c.size() << " " << d.size();
}
上述代码中,abcd的类型就不需要多说了吧,我们先猜测下上述代码的输出。。。
emm,编译运行后,输出结果为10 1 1 10,是不是很奇怪,下面进行简单的分析。
在模板函数create中,使用统一初始化并返回,对于a来说,因为其传入的是std::string,那么在函数create中,将变成**return std::vector<std::string>{10}**,乍一看,应该是用10进行初始化,但因为数据类型是std::string,所以用10进行初始化失败,那么退而求其次,调用了std::vector<std::string>(10);这就是a的size为10的原因,同理,b和c的size是1,d的size为10。
类型推导
再看一个例子:
std::vector<int> v{1, 2, 3, 4, 5};
std::vector<int> w{v.begin() + 1, v.end()};
std::vector w2{v.begin() + 1, v.end()};
上述几个例子中,都是通过统一初始化的方式进行初始化,v和w的类型一样,都是std::vector<int>,但是w2的类型却是std::vector<std::vector<int>::iterator>
还记得在前面的例子中,使用统一初始化的时候,相当于插入一个元素么,即:
std::vector<int> v1{1, 2, 3};
std::vector v2{std::vector{1, 2}};
在上述代码中v1的值有3个,分别为1 2 3,那么按照该规则,v2的类型岂不是std::vector<std::vector<int>>,在一开始学习这块的时候,我曾经也这么以为~~~通过cppinsights分析,发现v2的类型是std::vector<int>,如果想让v2的类型是vector 的话,则必须显示指定类型,即如下:
std::vector<std::vector<int>> v2{std::vector{1, 2}};
类型转换
统一初始化的另外一个特点是防止缩小初始化,想必我们都写过如下这种代码:
double d = 1.5;
int x = d; // x is 1 (double converts to int).
如果使用统一初始化的话:
int x{d}; // ERROR: cannot be narrowed.
则编译器会报错,为了解决编译器报错的问题,可以采用如下方式:
int x{(int)d};
int x{int(d)};
int x{static_cast<int>(d)}; // modern C++建议的方式
解析
经常能够遇到下面这个问题,是编译器在某些情况下解决语法歧义的方式:
class MyClass {};
MyClass f();
在编译的时候,会报错如下:
remove parentheses to default-initialize a variable
意思是去掉后面的**()以便调用默认构造函数。之所以有这个报错,是因为当C++无法区分“对象创建”和“函数声明”时,编译器默认将该语句解释为“函数声明”。**
继续看如下代码:
std::vector<int> v(5, 0); // {0, 0, 0, 0, 0}.
这段代码很简单吧,就是初始化vector,但是如果将其放入如下代码中,则编译器会报错,虽然我们的目的是进行初始化:
class MyClass {
public:
MyClass() { ... }
private:
std::vector<int> v(5, 0); // ERROR
};
为了解决这种这个问题,可以采用如下方式:
class MyClass {
public:
MyClass() : v(5, 0) { ... }
private:
std::vector<int> v;
};
也可以这样:
class MyClass {
public:
MyClass() { ... }
private:
std::vector<int> v = std::vector<int>(5, 0);
};
初始化列表
在前面内容中,有提到过,统一初始化,又称为列表初始化,列表无非是以std::initializer_list这种方式存在。编译器有个特点,对于以花括号初始化的方式则认为是统一初始化,如果构造函数中同样存在std::initializer_list为参数的构造函数,那么则优先调用
:
class MyClass {
public:
MyClass(int x, double y) { ... }
MyClass(std::initializer_list<bool> z) { ... }
};
int main() {
MyClass obj{5, 1.0};
};
我们可能期望MyClass obj{5, 1.0};
调用第一个构造函数(以int和double作为参数的构造函数),但由于存在以std::initializer_list
参数作为参数的构造函数重载,因此该构造函数将是首选。在这种情况下,编译器甚至会抛出错误,因为它检测到从int和double
的缩小转换bool
。试想一下,如果不涉及缩小转换(例如,第二个构造函数接受 in std::initializer_list<double>
,则代码将使用第二个构造函数(在初始值设定项列表中int 5
转换为double 5.0
)默默执行,而开发人员则认为它正在使用第一个构造函数,emm,后果不堪设想~~
在上面提了,编译器会优先调用参数为std::initializer_list的构造函数,但是有个例外:
class MyClass {
public:
MyClass() { ... }
MyClass(std::initializer_list<int> z) { ... }
};
int main() {
MyClass obj{}; // Calls the first constructor.
};
如果我们想让编译器调用第二个构造函数,可以像如下这样写:
MyClass obj( {} );
MyClass obj{ {} };
结语
这块终于写完了,一边写一边改,内容确实太杂了,本来想的是把遇到的坑都写出来,一时半会想不起来,只能等以后了。
以上~~
如果对本文有疑问可以加笔者微信直接交流,笔者也建了C/C++相关的技术群,有兴趣的可以联系笔者加群。