初始化|这些年踩过的坑

2023-11-24 17:49:37 浏览数 (1)

你好,我是雨乐!

最近在整理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;

这些初始化方式依赖于其具体类型

•对于基础类型,则可以使用赋值方式直接初始化

代码语言:javascript复制
int a = 42;
double b = 1.2;

•对于类类型,在其只有一个参数的情况下,也可以使用赋值方式进行初始化

代码语言:javascript复制
class foo
{
  int a_;
public:
  foo(int a):a_(a) {}
};
foo f1 = 42;

•对于非聚合类,也可以使用后面跟括号的方式(括号中传入参数),对于不需要参数的则不能添加括号,否则编译器会认为是函数声明

代码语言:javascript复制
foo f1;           // default initialization
foo f2(42, 1.2);
foo f3(42);
foo f4();         // function declaration

•聚合类可以通过花括号的方式进行初始化

代码语言:javascript复制
bar b = {42, 1.2};
int a[] = {1, 2, 3, 4, 5};

除了以上初始化方式之外,对于标准容器来说,都是先声明一个对象,然后通过插入的方式进行初始化,不过,std::vector是个例外,其可以从先前使用聚合初始化初始化的数组中分配,如下:

代码语言:javascript复制
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)。

统一初始化,用{}方式进行初始化,如下:

代码语言:javascript复制
T object {other};   
T object = {other}; 

下面是关于统一初始化的一些例子:

•标准库中的容器

代码语言:javascript复制
std::vector<int> v { 1, 2, 3 };
std::map<int, std::string> m { {1, "one"}, { 2, "two" }};

•动态数组分配

代码语言:javascript复制
int* arr2 = new int[3]{ 1, 2, 3 };

•数组

代码语言:javascript复制
int arr1[3] { 1, 2, 3 };

•内置类型

代码语言:javascript复制
int i { 42 };
double d { 1.2 };

•自定义类型

代码语言:javascript复制
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类型

代码语言:javascript复制
struct bar { int a_; double b_;};
bar b{ 42, 1.2 };

一些细节

在前面的两节中,分别讲解了Modern C 之前的初始化方式以及统一初始化方式,从使用方式上来看,更加统一,显然统一初始化是我们进行初始化时候的首选,当然了,需要注意一些细节,尤其是对于存在参数为std::initializer_list的容器类型来说。

代码语言:javascript复制
// 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中这块的源码:

代码语言:javascript复制
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()) { }
模板

继续看个例子:

代码语言:javascript复制
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可以是任何类型变量的原因~

接着看另外一个例子:

代码语言:javascript复制
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。

类型推导

再看一个例子:

代码语言:javascript复制
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>

还记得在前面的例子中,使用统一初始化的时候,相当于插入一个元素么,即:

代码语言:javascript复制
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的话,则必须显示指定类型,即如下:

代码语言:javascript复制
std::vector<std::vector<int>> v2{std::vector{1, 2}};
类型转换

统一初始化的另外一个特点是防止缩小初始化,想必我们都写过如下这种代码:

代码语言:javascript复制
double d = 1.5;
int x = d; // x is 1 (double converts to int).

如果使用统一初始化的话:

代码语言:javascript复制
int x{d}; // ERROR: cannot be narrowed.

则编译器会报错,为了解决编译器报错的问题,可以采用如下方式:

代码语言:javascript复制
int x{(int)d};              
int x{int(d)};              
int x{static_cast<int>(d)}; // modern C  建议的方式
解析

经常能够遇到下面这个问题,是编译器在某些情况下解决语法歧义的方式:

代码语言:javascript复制
class MyClass {};
MyClass f();

在编译的时候,会报错如下:

代码语言:javascript复制
remove parentheses to default-initialize a variable

意思是去掉后面的**()以便调用默认构造函数。之所以有这个报错,是因为当C 无法区分“对象创建”和“函数声明”时,编译器默认将该语句解释为“函数声明”。**

继续看如下代码:

代码语言:javascript复制
std::vector<int> v(5, 0); // {0, 0, 0, 0, 0}.

这段代码很简单吧,就是初始化vector,但是如果将其放入如下代码中,则编译器会报错,虽然我们的目的是进行初始化:

代码语言:javascript复制
class MyClass {
 public:
  MyClass() { ... }

 private:
  std::vector<int> v(5, 0); // ERROR
};

为了解决这种这个问题,可以采用如下方式:

代码语言:javascript复制
class MyClass {
 public:
  MyClass() : v(5, 0) { ... }

 private:
  std::vector<int> v;
};

也可以这样:

代码语言:javascript复制
class MyClass {
 public:
  MyClass() { ... }

 private:
  std::vector<int> v = std::vector<int>(5, 0);
};
初始化列表

在前面内容中,有提到过,统一初始化,又称为列表初始化,列表无非是以std::initializer_list这种方式存在。编译器有个特点,对于以花括号初始化的方式则认为是统一初始化,如果构造函数中同样存在std::initializer_list为参数的构造函数,那么则优先调用

代码语言:javascript复制
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的构造函数,但是有个例外:

代码语言:javascript复制
class MyClass {
 public:
  MyClass() { ... }
  MyClass(std::initializer_list<int> z) { ... }
};

int main() {
  MyClass obj{}; // Calls the first constructor.
};

如果我们想让编译器调用第二个构造函数,可以像如下这样写:

代码语言:javascript复制
MyClass obj( {} );
MyClass obj{ {} };

结语

这块终于写完了,一边写一边改,内容确实太杂了,本来想的是把遇到的坑都写出来,一时半会想不起来,只能等以后了。

0 人点赞