Skip to main content

Command Palette

Search for a command to run...

使用统一初始化(uniform initialization)

谈谈()和{}创建对象 和 std::initializer_list

Published
使用统一初始化(uniform initialization)

多种多样的初始化方法

在 C++11 中有多种初始化对象的方法

int x(0); // 使用小括号初始化
int y= 0; // 使用等号初始化
int z{ 0 }; // 使用花括号初始化

还有一种初始化int z = {0}; 使用花括号和=初始化,C++通常把它视作和只有花括号一样。

需要注意的是并不是所有带有=的语句都发生了赋值运算。

Widget w1; // 调用默认构造函数 Widget()
Widget w2 = w1; // 不是赋值运算,是初始化。调用拷贝构造函数(copy ctor) Widget(const Widget& w);
w1 = w2; // 是赋值运算,调用拷贝赋值运算符(copy op=) Widget& operator=(const Widget& w);

(花)括号表达式

  • 括号表示可以表达出C++11前表达不出来的东西。比如,指定一个容器的元素
    std::vector<int> v{1, 2, 3}; // v 初始化内容为1, 3, 5
    
  • 括号初始化也可以被用为指定类的非静态对象成员的默认初始值。
    class Widget {
    ...
    private:
    int x{100}; // 将x初始化为100
    int y = 200; // 将y初始化为200
    int z(0); // Error! 
    }
    
  • 对于不可拷贝的对象(std::atomic), 也可以使用{}()初始化,但不可以使用=初始化。
    std::atomic<int> ai1{ 0 };      //没问题
    std::atomic<int> ai2(0);        //没问题
    std::atomic<int> ai3 = 0;       //错误!
    

C++11使用统一初始化(uniform initialization)来整合不适用于所有场景的初始化方法。 所谓统一初始化是指单一初始化语法在任何初始化表达式存在的地方(上面的四个代码示例)都可以使用。统一初始化是一个概念上的东西,而括号初始化是一个具体的语法实现

带来的好处

  1. 禁止内置类型间的隐式的变窄转换(narrowing conversion)
    double x, y, z;
    int sum1 = x + y + z; // 正常: 兼容C++11前的逻辑
    int sum2(x + y + z); // 正常: 兼容C++11前的逻辑
    int sum3{x + y + z}; // Error: 括号表达式禁止变窄转换
    
  2. 免疫 most vexing parse

C++规定任何能被决议为一个声明的东西必须被决议为一个声明。这个规则的副作用是: 当我们想创建一个默认构造函数构造的对象时,却变成了函数声明。

    Widget w1(); // most vexing parse: 被决议为函数声明。
    std::cout << typeid(w1).name() << std::endl; // F6WidgetvE

用于函数声明中形参列表不能使用花括号,因此使用花括号表明调用默认构造函数是没有问题的。

    Widget w2{}; // 调用默认构造函数
    std::cout << typeid(w2).name() << std::endl; // 6Widget

download-1.jpg

匹配规则

  1. 当类中,不存在包含std::initializer_list形参的构造函数时,花括号初始化和小括号初始化的结果是相同的。
    class Widget {
    public:
     Widget(int i, int b) {
         std::cout << "Widget(int i, int b)" << std::endl;
     }
     Widget(int i, double d) {
         std::cout << "Widget(int i, double d)" << std::endl;
     }
    };
    int main() {
     Widget w1(10, true); // Widget(int i, int b)
     Widget w2{10, true}; // ditto
     Widget w3(10, 5.0); // Widget(int i, double d)
     Widget w4{10, 5.0}; // ditto
    }
    
  2. 存在包含std::initializer_list形参的构造函数,并且{}内实参可以转化std:: initializer_list时,编译器就会使用它。
class Widget {
public:
    Widget(int i, int b) {
        std::cout << "Widget(int i, int b)" << std::endl;
    }
    Widget(int i, double d) {
        std::cout << "Widget(int i, double d)" << std::endl;
    }
    Widget(std::initializer_list<long double> il) {
        std::cout << "Widget(std::initializer_list<long double> il)" << std::endl;
    }
};
int main() {
    Widget w1(10, true); // Widget(int i, int b)
    Widget w2{10, true}; // Widget(std::initializer_list<long double> il)
    Widget w3(10, 5.0); // Widget(int i, double d)
    Widget w4{10, 5.0}; // Widget(std::initializer_list<long double> il)
}

编译器热衷于把括号初始化与使std::initializer_list构造函数匹配了,尽管最佳匹配std::initializer_list构造函数不能被调用也会凑上去。

class Widget {
public:
    Widget(int i, int b) {
        std::cout << "Widget(int i, int b)" << std::endl;
    }
    Widget(int i, double d) {
        std::cout << "Widget(int i, double d)" << std::endl;
    }
    Widget(std::initializer_list<bool> il) {
        std::cout << "Widget(std::initializer_list<long double> il)" << std::endl;
    }
};
int main() {
    Widget w1(10, true); // Widget(int i, int b)
    Widget w2{10, true}; // Error: Constant expression evaluates to 10 which cannot be narrowed to type 'bool'
    Widget w3(10, 5.0); // Widget(int i, double d)
    Widget w4{10, 5.0}; // Error: Type 'double' cannot be narrowed to 'bool' in initializer list

    bool a = 10; // 可以转化,但发生了变窄转换(narrowing conversion)
    bool b = 5.0; // 可以转化,但发生了变窄转换(narrowing conversion)
}

w2w4由于Widget存在含有std::initializer_list的构造函数, 并且可以转换,因此{}初始化会忽略掉哪怕是最佳匹配的Widget(int i, double d), 去匹配Widget(std::initializer_list<bool> il) 。但因为存在变窄转化(int->bool, double->bool),因此编译失败。

  1. 存在包含std::initializer_list形参的构造函数,但{}内实参无法转化std:: initializer_list时,编译器才会回到正常的决议流程。

使用std::initializer_list<std::string>代替std::initializer_list<bool>, 编译器无法将boolint转化为string

std::string a = 10; // Error: No viable conversion from 'int' to 'std::string'
std::string b = 5.0; // Error: No viable conversion from 'double' to 'std::string'

因此,非std::initializer_list的构造参数,才会再次成为函数决议的候选者。

Widget w1(10, true); // Widget(int i, int b)
Widget w2{10, true}; // ditto
Widget w3(10, 5.0); // Widget(int i, double d)
Widget w4{10, 5.0}; // ditto

花括号初始化是空集

假设一个类存在默认构造函数,和含有std::initializer_list的构造函数。空的{}意味着没有实参,会使用默认构造函数

class Widget { 
public:  
    Widget();                                   //默认构造函数
    Widget(std::initializer_list<int> il);      //std::initializer_list构造函数//没有隐式转换函数
};

Widget w1;                      //调用默认构造函数
Widget w2{};                    //也调用默认构造函数
Widget w3();                    //最令人头疼的解析!声明一个函数

如果想用空std::initializer来调用std::initializer_list构造函数,就得创建一个空花括号作为函数实参

Widget w4({});                  //使用空花括号列表调用std::initializer_list构造函数
Widget w5{{}};                  //同上

揭秘std::initializer_list

  • 定义
// Defined in header <initializer_list>
template< class T >
class initializer_list;
  • 实现

std::initializer_list由一对指针或者一个指针和长度实现。底层数组为const T[N], 其中每个元素都从原始初始化器列表的对应元素复制初始化。 复制一个std::initializer_list不会赋值底层的对象。

  • 生命周期

底层数组的生存期与任何其他临时对象相同,除了从数组初始化 initializer_list 对象会延长数组的生存期。

如何抉择{}()

  1. 作为一个类库作者, 你需要意识到如果你的一堆重载的构造函数中有一个或者多个含有std::initializer_list形参,用户代码如果使用了括号初始化,可能只会看到你std::initializer_list版本的重载的构造函数。这里的暗语是如果一个类没有std::initializer_list构造函数,然后你添加一个,用户代码中如果使用括号初始化,可能会发现过去被决议为非std::initializer_list构造函数而现在被决议为新的函数。所以如果你要加入std::initializer_list构造函数,请三思而后行。

  2. 作为一个类库使用者,你必须认真的在花括号和小括号之间选择一个来创建对象。大多数开发者都使用其中一种作为默认情况,只有当他们不能使用这种的时候才会考虑另一种。关于花括号和小括号的使用没有一个一致的观点,所以我的建议是用一个,并坚持使用。

Ref

  1. https://cntransgroup.github.io/EffectiveModernCppChinese/3.MovingToModernCpp/item7.html
  2. https://en.cppreference.com/w/cpp/utility/initializer_list

More from this blog

<Programming with Types>随想: Chapter 2. Basic types

类型限制了一个变量可以接受的有效值的集合,对数据可以进行的操作,数据的意义。 空类型(The empty type) 根据类型的定义,类型定义了可以接受的有效值集合,那么这个集合有没有可能为空?答案是有可能的,TypeScirpt 的 never 就是这种类型。 需要注意的是,空类型不同与 void, 后者是有效值集合当中只有一个值,但这个值没有任何意义。而空类型的有效值集合本身是空的。 使用场景 控制流分析 在函数调用时,标志一个函数不会返回任何值: 在调用过程中抛出异常、死循环或者程序崩溃...

Mar 5, 2023
<Programming with Types>随想: Chapter 2. Basic types

精读《设计机器学习系统》-ch04: 训练数据

不同于 Chapter03 从系统的角度来处理数据,这一章从数据科学的视角来处理数据。这章的标题是“training Data”,而非“training dataset”,因为 数据集(dataset) 意味着有限(finite)和固定(stationary), 而现实生产环境中的 数据(data) 通常是 无限 并且 不固定 的。 抽样 抽样方法在 ML 项目的生命周期中无处不在,在这一节中,我们使用生成训练数据作为例子。 那为什么需要抽样?直接使用全部数据不可以吗? 首先,在现实世界中,并不...

Dec 10, 2022
精读《设计机器学习系统》-ch04: 训练数据
7

70 Talk

19 posts