C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C++中使用大括号初始化

C++中使用大括号初始化时需要注意哪些问题?一文总结

作者:fpcc

还在为C++列表初始化的各种奇怪问题头疼吗?本文详细剖析了std::initializer_list的只读性、生命周期、构造函数优先级等常见陷阱,并给出安全使用建议,帮你避开大括号初始化的坑,写出更稳健的代码

一、列表初始化和初始化列表

对于使用大括号{}进行初始化,在前面反复分析过,给出的建议是可能的情况下尽量使用大括号进行初始化。但这并不意味着大括号是完美的初始化方式。

这里要显式的说明一下大括号{}和std::initializer_list两种机制。从形式让讲前面的大括号叫列表初始化,而后面的std::initializer_list可以叫做初始化列表。而本质上,std::initializer_list作为c++11引入的轻量级的模板类,专门用于支持前者列表初始化。

这是一件好事,但可能在某些情况下,好事也会变成不好的事。注意,这里没说是坏事。

这里需要注意std::initializer_list的几个特点即只读性(内部元素视为const)、不支持移动语义、浅拷贝以及类型统一要求。看看这几点,哪一点单独拿出来都是在走钢丝。所以,它的问题,一定会在使用不当的时候暴露出来。

二、主要的问题

对于使用大括号进行初始化,除了前面提到的窄化转换问题,其它还有几个重要的问题需要说明。

主要包括:

1、initializer_list构造函数优先级过高

对于一些类中定义了支持std::initializer_list的构造函数时,编译器会优先选择更匹配std::initializer_list的构造函数,这是最容易产生意外行为的问题。

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v1(10, 9); // 10个元素,每个都是9
    std::vector<int> v2{10, 9}; // 两个元素:10和9

    std::cout << v1.size() << std::endl; // 10
    std::cout << v2.size() << std::endl; // 2
}

如果一个类同时有普通构造函数和 initializer_list构造函数,使用 {}时会优先尝试后者:

#include <initializer_list>
#include <iostream>

class Demo {
public:
    Demo(int count, int value)
    {
        std::cout << "普通构造函数"<<std::endl;
    }

    Demo(std::initializer_list<int>)
    {
        std::cout << "initializer_list构造函数"<<std::endl;
    }
};

int main()
{
    Demo a(10, 9); // 普通构造函数
    Demo b{10, 9}; // initializer_list构造函数
}

遇到这一类问题,解决的首先是判定是否支持initializer_list,特别是对STL的库容器等,要严格小心注意。也可以从应用层出发,对于参数设置,则直接调用构造函数即使用小括号的机制;而对于一组元素的初始化时,则使用大括号。另外,如果是自写编写相关类,没有显式的要求下,不要提供initializer_list的构造函数。

2、initializer_list元素只读,不能拷贝

在前面的注意点中提到了std::initializer_list 的元素类型相当于 const T。

所以在一些需要移动结合应用的情况下,代码就会出现问题:

#include <memory>
#include <vector>

std::vector<std::unique_ptr<int>> Data{
    std::make_unique<int>(1),
    std::make_unique<int>(2)
};

大家都知道,unique_ptr只可移动不可复制,而列表中的元素是 const,意味着无法进行移动。

要想实现这个效果则可以使用:

std::vector<std::unique_ptr<int>> Data;
Data.reserve(2);

Data.push_back(std::make_unique<int>(1));
Data.push_back(std::make_unique<int>(2));

当然,这里更建议使用 emplace_back,但资源所有权场景通常优先使用 make_unique。

3、initializer_list的生命周期问题

std::initializer_list可以看作一种底层数据的上层 的View,所以不要返回指向临时列表底层数组的 initializer_list。

这和不要返回临时变量的指针有些类似:

#include <initializer_list>

std::initializer_list<int> getData()
{
    return {3, 6, 9}; // 危险:返回后底层数组已经失效
}

应返回拥有数据所有权的容器,其实就是进行了拷贝:

#include <array>

std::array<int, 3> getData()
{
    return {3, 6, 9};
}

//或

#include <vector>

std::vector<int> getData()
{
    return {3, 6, 9};
}

不过如果可以保障其安全的作用域,是可以正常使用的。比如作为函数参数临时使用通常是安全的:

void print(std::initializer_list<int> data);
print({3, 6, 9}); // 列表在整个函数调用期间有效

但是如果对列表的 begin()等指针进行保存并在函数调用结束后继续使用,仍然会出现问题。

4、模板参数的推导

在前面学习CTAD时,模板可以自动进行模板参数的推导。

但如果是从单纯的{}往往推导无法成功,如下:

template<class T>
void set(const T& v){}

// set({3, 3, 3}); // 编译错误:无法推导 T

这时可以显式的构造对象类型,以供推导。如:

#include <vector>

set(std::vector<int>{3, 3, 3});

或者增加重载函数,使用std::initializer_list:

#include <initializer_list>

template<class T>
void set(std::initializer_list<T> v){}

int main()
{
    set({3, 3, 3}); // T 推导为 int
}

同样,下面的代码一般也无法自动推导:

std::make_unique<std::vector<int>>({3, 3, 3}); // 无法推导

和上面一样,或者使用initializer_list或者预先显式的构造容器:

auto p = std::make_unique<std::vector<int>>(
    std::initializer_list<int>{3, 3, 3}
);
auto p = std::make_unique<std::vector<int>>(
    std::vector<int>{3, 3, 3}
);

5、列表初始化的应用方式对explicit构造的影响

对于拥有显式的explicit构造函数的类,不能使用隐式的调用它。

这在大括号初始化中同样适用:

class Demo {
public:
    explicit Demo(int value) {}
};

Demo d{10};      // ok:直接列表初始化
// Demo d1 = {10}; // err:隐式调用 explicit构造函数

正如例程中一样,只能是显式的调用构造函数:

Demo d{10};

在通过函数传递参数时,也应如此:

void set(Demo d);
set(Demo{10});

6、空列表可能导致歧义

看下面的代码:

void set(std::vector<int>);
void set(std::string);

int main()
{
    // set({}); // err:重载匹配错误
}

处理这种问题,只能是显式的给出调用参数的数据类型:

set(std::vector<int>{});
set(std::string{});

7、聚合类型初始化的顺序问题

在新的标准中提供了聚合类型的初始化,但顺序的依赖性也由此产生,不能跳着初始化或个别不初始化。如下:

struct Data
{
    int id;
    char* name;//char name[100];
};
Data d{31,"tom"};

如果换顺序则可能会出现各种问题。如果想忽略某个值的初始化,可使用c++20中的指定初始化,但其仍然需要严格按顺序进行。

8、对auto支持的问题

有个c++大牛强力推荐使用auto。但它在遇到初始化列表时,也要注意。

看下面的代码:

auto a{1};        // int
auto b = {2};     // std::initializer_list<int>
auto c = {3, 4};  // std::initializer_list<int>

// auto d{5, 6};     // err:直接列表初始化只能有一个元素
// auto e = {7, 8.0}; // err:元素类型不统一

c/c++天然对int支持的好啊。没办法,胎里带。处理这类问题,还是显式的给出类型。不再给出例子。

三、应用说明

在明白了上述大括号初始化的细节问题后,其实就也有一个清晰的正确使用的方式:

总体上来说,使用大括号这种列表初始化的机制,统一、方便。仍然是对象初始化的优先使用方式,但在使用方便时,也要看到其中细节的问题,不能一头栽进去,出了问题还不知道为什么。

四、总结

在任何情况下都不要想当然,都要严格的按标准行事。扩大影响或使用范围,都需要当前环境或工具的显式支持。只有这样,才可能写出安全稳定的c++代码。

列表初始化作为一个被广泛推荐的初始化方式,仍然在c++中存在着这样那样的细节上的陷阱。所以,对于c++开发者来说,不能轻易的滥用任务技术。自己的大脑中始终要保持清醒和警惕!

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文