C++标准系列3—C++11之可用性优化

1.前言

让C++更加方便的使用,增加类型的安全性,减少代码重复

2.初始化列表

2.1.std::initializer_list

C++标准从C语言中带来了初始化列表的概念:struct或者数组能够依据成员在该结构内定义的顺序在大括号中赋予一个参数列表。初始化列表是递归的,所以struct(数组)里的struct(数组)可以用花括号里嵌套花括号的方式来初始化。在C++03中,只允许严格遵守POD(POD 类型即 plain old data 类型,简单来说,是可以直接使用 memcpy 复制的对象)的数据结构使用这项机制,非POD的类型不能使用,哪怕是比较常用的STL容器也不行。
C++11则把初始化列表的概念用一个新的类型来表示:std::initializer_lis。这允许构造函数或者其他函数可以更方便的使用初始化列表。下面例子的第一个构造函数称之为,初始化列表构造函数。有初始化列表构造函数时,在统一初始化时,会被特别对待。

#include <iostream>
#include <initializer_list>

struct A
{
    A(std::initializer_list<int> l)
    {
        std::cout << "initializer_list constructor" << std::endl;
    }
    A()
    {
        std::cout << "common constructor" << std::endl;
    }
};

int main()
{
    A obj0 = { 1, 2, 3 }; // initializer_list constructor
    A obj1; // common constructor
}

std::initializer_list 对象在这些时候自动构造:

  • 用花括号初始化器列表列表初始化一个对象,其中对应构造函数接受一个std::initializer_list 参数
  • 以花括号初始化器列表为赋值的右运算数,或函数调用参数,而对应的赋值运算符/函数接受 std::initializer_list 参数
  • 绑定花括号初始化器列表到 auto ,包括在范围 for 循环中initializer_list 可由一对指针或指针与其长度实现。复制一个 std::initializer_list 不会复制其底层对象。
int main()
{
    int a = 3;
    constexpr int b = 4;
    auto t1 = { 1, 2, 3, a, b };
    std::cout << typeid(t1).name() << std::endl; // class std::initializer_list<int>
}

上面例子可以看到,直接用花括号列表里的元素是同类型(允许const和constexpr修饰),用这个列表来初始化生成的对象就是std::initializer_list类型

std::vector<int> v = { 1, 2, 3 };

STL容器也可以用初始化列表来初始化

void fun(std::initializer_list<int> l){}

int main()
{
    fun({ 1, 2, 3 });
}

普通函数也可以用std::initializer_list来作为形参
具体内容烦请阅读:C++日积月累—返回值优化

2.2.统一的初始化列表

C++98/03中允许的初始化方式:

struct A
{
    A(int t): a(t) {}
    A(const A &obj): a(obj.a) {}
    
    int a;
}obj0(1), obj1(obj0); // 拷贝构造函数

struct B
{
    int b;
};

struct C
{
    int c;
    B obj_b;
}obj2{ 1, {2} }, obj3{ 1, 2 }; // POD初始化

int main()
{
    A obj4(0); // 直接初始化
}

可以看到C++中的初始化方法有多种,都有各自的适用范围和作用。但是,这么多的初始化方法,并没有一种可以通用所有情况的方法。
为了统一初始化方法,C++11提出了列表初始化(List-initialization)的概念。
对于数组和POD类型的结构体、类,在C++98/03就能使用列表的方法来初始化,比如上面例子的obj2和obj3。C++11,把这种列表初始化的适用性大大拓展了,使其可以用于任何对象的初始化。

struct B
{
    int b;
}obj{ 0 };

B fun()
{
    return { 1 };
}

int main()
{
    int a0{ 0 };
    int a1 = { 1 };
    int* p0 = new int{ 2 };
    int* arr0 = new int[3]{ 1, 2, 3 };  // 使用了std::initializer_list来实现
    B obj0{ 0 };
    B obj1 = { 1 };
}

上面例子的a0、p0、arr0、obj0,在C++98/03会报错,但是C++11就可以
可以看到obj、obj0、obj1、a0、a1、p0、arr0的初始化形式是统一的,都是用一个初始化列表来初始化。
C++11可以对vector类型和堆上动态分配的数组也可以使用初始化列表进行初始化
fun函数可以看出,列表初始化还可以直接使用在函数的返回值上。

2.3.构造函数优先级问题

  • 统一初始化不会取代构造函数语法
  • 如果一个类拥有初始化列表构造函数(TypeName(initializer_list<SomeType>);),而初始化列表与构造函数的参数类型一致时,初始化列表构造函数的优先级较高。可以参考下面例子:
struct A
{
    A(std::initializer_list<int> l)
    {
        std::cout << "initializer_list constructor" << std::endl;
    }
    A(int a, int b)
    {
        std::cout << "common constructor" << std::endl;
    }
};

int main()
{
    std::vector<int> v0{4};  //size:1
    std::vector<int> v1(4); // size:4
    A obj0{ 1, 2 }; // initializer_list constructor
    A obj1(1, 2); //common constructor
}

3.自动类型推导

3.1.auto 类型推导的语法和规则

auto本身和static关键字相对,在C++98/03中很鸡肋。C++11赋予了新特性,可以自动类型推导。auto只是占位符,C++ 中的变量必须是有明确类型的,只是这个类型是由编译器自己推导出来的。

    auto n = 1; // int
    auto f = 1.0; // double
    auto c = "hello"; // const char*
    auto p0 = &n; // p0为int*,auot推导为int*
    auto *p1 = &n; // p1为int*,但是auto推导为int
    auto &r0 = n; // r0为int&,auot推导为int
    auto r1 = r0; // r1为int,auot推导为int;auot会把引用抛弃,推导为原始类型
    const auto n1 = n; // n1为const int,auto推导为int
    auto n2 = n1; // n2为int,auot推导为int;auot会把const抛弃,推导为原始类型
    const auto& n3 = n; // n3为const int&类型,auot被推导为int
    auto n4 = n3; // n4为int,auot推导为int;auot会同时把const和引用抛弃,推导为原始类型
    auto &n5 = n3; // n5为const int&类型,auot被推导为const int
    const auto n6 = n3; // n6为const int,抛弃了引用
    auto * p2 = &n6; // p2为const int*,指针保留了const

总结:

  • auto不保留引用属性;
  • 当且仅当类型是引用或者指针时,auot保留cv 限定符的属性;(cv 限定符:const和volatile,volatile:不让 CPU 将数据缓存到寄存器,而是从原始的内存中读取)
  • 对类型的推导不得有二义性,且auto类型推导的变量必须马上初始化;
  • 不能在函数中使用,不能作用于类的非静态成员变量,不能定义数组,不能作用于模板参数。

3.2.auto进阶

  • auto不影响运行效率,因为auto是在编译时进行了类型推导
  • auto几乎不影响编译速度,因为编译时本来也要对右侧推导然后判断与左侧是否匹配
  • auto真正的优势是在泛型编程,比如下面这个例子,就不用关心返回的类型。如果按照以前的语法,就需要对func多加一个模板参数才能实现同样的逻辑。
class A {
public:
    static int get(void){
        return 1;
    }
};

class B {
public:
    static const char* get(void) {
        return "hello";
    }
};

template <typename T>
void func(void) {
    auto val = T::get();
    // todo
}

int main(void) {
    func<A>();
    func<B>();
    return 0;
}

3.3.decltype类型推导

decltype 是“declare type”的缩写

decltype也是可以自动推导出变量的类型,语法如下:

decltype(exp) var0 = value; 
decltype(exp) var1;

var是变量名,value是赋给var的值,exp是表达式。decltype根据exp表达式推导出变量的类型,跟=右边的value没有关系。decltype可以不初始化变量。

推导规则:

  • 如果 exp 是一个不被括号( )包围的表达式,或者是一个类成员访问表达式,或者是一个单独的变量,那么 decltype(exp) 的类型就和 exp 一致,这是最普遍最常见的情况。
  • 如果 exp 是函数调用,那么 decltype(exp) 的类型就和函数返回值的类型一致。注意:1、exp可以是任意复杂的形式,但是我们要保证exp是有类型的,不能是void;2、函数不会执行。
  • 如果 exp 是一个左值,或者被括号( )包围,那么 decltype(exp) 的类型就是 exp 的引用;假设 exp 的类型为 T,那么 decltype(exp) 的类型就是 T&。
  • decltype保留cv限定符属性
class Base {
public:
    int m_var = 1;
    static float s_var;
};

int main()
{
    int n0 = 0;
    decltype(n0) n1 = 1; // int
    decltype(Base()) obj; // Base
    decltype(Base().m_var) n2 = 2; // int
    decltype(Base::s_var) f0 = 3; // float
    
    // exp为函数调用
    int fun_int(void);
    int& fun_int_r(int);
    const int& fun_int_cr(double);

    decltype(fun_int()) n3 = 4; // int
    decltype(fun_int_r(1)) r0 = n3; // int&
    decltype(fun_int_cr(1.0)) cr1 = n3; // const int&

    // 带括号的表达式
    decltype(n0 + n1) n4; // int
    decltype((n4)) r1 = n0; // int&,带括号的是引用,并且一定要赋值
    
    // 左值
    decltype(n4 = 1) r2 = n0; // int&,左值的结果是引用

    return 0;
}

应用场景
在模板里定义一个迭代器类型变量,代码中看m_iter的定义似乎没有什么问题。但是如果实例化的类型是一个const类型的容器,编译就会爆出一大堆错误,并且错误还不是那么好找。这是因为T::iterator类型的变量不能被赋值为const_iterator类型的值。但是我们用decltype就没有这个问题了

template <typename T>
class A {
public:
    void set_iter(T& container) {
        m_iter = container.begin();
    }
private:
    typename T::iterator m_iter;
};

void fun()
{
    std::vector<int> v;
    A<std::vector<int>> obj;
    obj.set_iter(v);
}
template <typename T>
class B {
public:
    void set_iter(T& container) {
        m_iter = container.begin();
    }
private:
    decltype(T().begin()) m_iter;
};

3.4.decltype和auto的区别

  • auot会丢失引用属性,decltype不会
  • auto会可能会丢失cv限定符属性,decltype不会
  • auto虽然用起来看上去比decltype简单,但是auto可能会丢失属性,可能会造成未知的错误。而decltype能确保类型不变,推导结果更加可控。实际应用中需要选择需要的,decltype和auto一起使用会更为有用,因为auto参数的类型只有编译器知道。然而decltype对于那些大量运用运算符重载和特化的类型的代码的表示也非常有用。

4.基于范围的for循环

这是个很好用的for循环使用方法,有点脚本语言那味了。
语法是:

for (declaration : expression){
    //循环体
}

declaration是变量,可以在循环体中使用。declaration可以用auto来定义类型,如果想要改变迭代器的值,需要使用引用类型。如果只是遍历,可以用const的引用类型,避免内存复制。
expression表示要遍历的序列,可以是已经定义好的数组、容器、或者std::initializer_list(可以用花括号列表,下图汇编可以证明)

image.png

int main()
{
    char arr[] = { "hello world" };
    for (const auto& i: arr)
    {
        std::cout << i;
    }
    std::cout << "#" << std::endl;
    std::vector<char> v(std::begin(arr), std::end(arr));
    for (const auto& i : v)
    {
        std::cout << i;
    }
    std::cout << "#" << std::endl;
}
/*结果:
hello world #
hello world #
*/

注意:

  • 字符数组的'\0'也打印出来了,所以结果#前面有空白
  • 对于容器来说,遍历的是元素而不是迭代器,如果declaration是引用类型,是可以真的改变了容器里的元素
  • 基于范围的for循环不支持指针类型,比如指针指向的数组、常量字符串等都不支持。原因是需要明确遍历的范围,而指针并没有明确范围
  • 如果expression是个表达式,整个遍历过程中,这个表达式只会计算一次
  • 在遍历过程对容器进行增删可能造成位置的错误,遍历的结果不是我们想要的,下面例子可以证实
  • STL中关联式容器(包括哈希容器)底层存储机制的限制:
    1、不允许修改 map、unordered_map、multimap 以及 unordered_multimap 容器存储的键的值;
    2、不允许修改 set、unordered_set、multiset 以及 unordered_multiset 容器中存储的元素的值
int main()
{
    std::vector<int> v{ 1, 2};
    v.reserve(1024);
    for (const auto& i : v)
    {
        std::cout << i << std::endl;
        v.push_back(1);
    }
}

打印的结果并不是预期想要的,可以看一看for循环前后vector元素的地址:

int main()
{
    std::vector<int> v{ 1, 2};
    std::cout << &v[0] << "\t" << &v[1] << std::endl;
    for (const auto& i : v)
    {
        std::cout << &i << std::endl;
        v.push_back(1);
    }
    std::cout << &v[0] << "\t" << &v[1] << std::endl;
}

发现vector的元素地址在for循环时已经发生变化,这是因为在push_back时,vector发生了动态扩容。for循环中依旧在遍历旧buff的地址,而vector的值实际已经存在了新的buff。如果对STL的内存管理感兴趣,可以看一看《STL源码剖析》的“空间配置器”一章。

5.Lambda函数与表达式

5.1.什么是Lambda

Lambda在脚本语言中被广泛应用,例如python。Lambda应用场景还是很多,特别是简单又重复执行的代码、各种回调函数等
C++11 Lambda的语法:
[外部变量访问方式说明符] (参数) mutable noexcept/throw() -> 返回值类型
{
函数体;
};
举一个很简单的例子:

    auto lambda_fun = [=](int a) ->int{return a * g_a; };
    auto ret = lambda_fun(10);

最简单的lambda匿名函数:

[]{}

下面就对各个部分分别讲解。
参数
和普通函数的定义一样,唯一的区别是,如果没有参数,()可以省掉

noexcept/throw()
可以省略,如果使用,前面的()不能省略。

默认情况lambda函数的函数体可以抛出任何类型的异常,标志noexcept关键字后,则表示函数体不会抛出任何错误,具体noexcept的用法可以参阅: 《C++日积月累—异常处理》——noexcept

而使用throw()可以指定lambda函数内部可以抛出的异常类型。

返回值类型
指明 lambda 匿名函数的返回值类型。值得一提的是,如果 lambda 函数体内只有一个 return 语句,或者该函数返回 void,则编译器可以自行推断出返回值类型,此情况下可以直接省略。

函数体
和普通函数一样,lambda 匿名函数包含的内部代码都放置在函数体中。该函数体内除了可以使用指定传递进来的参数之外,还可以使用指定的外部变量以及全局范围内的所有全局变量

举个例子,升序变降序:

    int arr[]{ 1, 2, 3, 4, 5 };
    std::sort(std::begin(arr), std::end(arr), [](int x, int y) -> bool { return x > y; });

外部变量访问方式说明符比较复杂,和mutatle放下节讲。

5.2.外部变量访问方式说明符与mutable

[ ] 方括号用于向编译器表明当前是一个 lambda 表达式,其不能被省略。在方括号内部,可以注明当前 lambda 函数的函数体中可以使用哪些“外部变量”(lambda函数能访问到的所有变量)。


image.png

可以参考如下例子:

    int total = 0;
    int value = 5;
    [&, value](int x) noexcept { total += (x * value); };

在成员函数中指涉对象的this指针,必须要显式的传入lambda函数,否则成员函数中的lambda函数无法使用任何该对象的参数或函数。

[this]() { this->SomePrivateMemberFunction(); };

mutable
此关键字可以省略,如果使用了这个关键字,那么前面的()不能省略。默认值传递的方式导入的变量是右值,不能再匿名函数体中修改。如果使用了mutable,就可以修改传值进去的变量,当然修改的只是拷贝的那一份,并不会真的修改外部变量。

注意:

  • 变量不得以相同的传递方式导入多次。例如 [=,x] 中,x先后被以值传递的方式导入了 2 次,这是非法的。
  • 以值传递的方式导入的变量并且没有使用mutable关键字,则表示传入的变量是右值,不能在函数体修改。
  • 在类的成员函数中定义 lambda 时,this 指针是个特殊情况。因为类的成员函数中,默认都可以通过 this 指针访问类的成员变量,因此在 C++11 中新增 lambda 的时候,要求 this 指针只能使用引用方式,并且要求使用 “ = ” 缺省按值捕捉时,其他变量按引用捕捉都应该使用 “ & ” 来表示引用,并且特别指出 [ =, this ] 的写法是不合法的。但按引用方式访问类成员变量不一定能满足所有需求,因此在C++17中,增加了按值的方式访问类成员变量,也就是捕捉 *this 。这样对按值还是按引用捕捉 this 带来了差异,因此在C++20中,要求对 this 的捕捉要显式说明,不在 “ = & ” 的缺省捕捉中。同时,原来的 [ =, this ] 语法是不合法的,现在也改成合法了。

6.返回类型后置的函数声明

在C++98/03标准的泛型编程中,如果返回值是通过参数的运算来得到的,那么返回值类型的定义是无法实现的,想要得到返回值只能通过一些骚操作来实现。
那么我们用C++11的auto或者decltype来实现是否可行呢?
先试一试decltype:

template <typename T, typename U>
decltype(x + y) fun(T x, U y)
{
    return x + y;
}

上面代码会直接编译失败,这是因为C++的返回值是前置语法(这也是为什么decltype(fun()),并不需要真的执行fun),在返回值定义的时候参数变量还不存在。
但是我们可以通过下面方法来实现:

template <typename T, typename U>
decltype(*(T*)0 + *(U*)0) fun(T x, U y)
{
    return x + y;
}

上面代码的方法虽然成功的使用decltype完成了返回值的推导,但是写法过于晦涩且可读性低且反人类。因此C++11增加了返回类型后置(trailing-return-type,又称跟踪返回类型)语法,将 decltype 和 auto 结合起来完成返回值类型的推导。

template <typename T, typename U>
auto fun(T x, U y) -> decltype(x + y)
{
    return x + y;
}

这种语法也能套用到一般的函数定义与声明:

auto fun()->int { return 1; }

注意:使用auto作为返回类型,跟auto的自动推导一样,auto不保留引用属性,当且仅当类型是引用或者指针时,auot保留cv 限定符的属性。

后续C++14对返回类型后置做了优化可以不用->来指定类型。

template <typename T, typename U>
auto fun(T x, U y)
{
    return x + y;
}

7.显示虚函数重载:override与final

7.1.虚函数重载的问题

C++中子类的重载虚函数有时会发生意料之外的事情,举个例子:

struct Base {
    virtual void some_func();
};

struct Derived : Base {
    void some_func();
};
  • Derived::some_func可能只是想要加一个普通函数,恰好和基类函数同名,被编译器当做重载虚函数
  • Derived::some_func可能是真的想重载虚函数,但是因为形参列表不同导致被编译器当做一个新定义的成员函数
  • 当基类的Base::some_func删了后,Derived::some_func就不再是一个虚函数,变成一个普通函数(当然老司机都知道在子类重载的函数中加上virtual)

7.2.override

C++11标准提供了override关键字来显示地告知编译器进行了虚函数重载,编译器将检查基类中是否存在这样的虚函数,如果不存在则编译报错。override可以清晰告知coder和编译器写这段代码的意图。

struct Base {
    virtual void some_func();
};

struct Derived : Base {
    void some_func() override;
};

7.3.final

我们可以把类中的某个函数指定为final,之后任何尝试覆盖该函数的操作都会引发错误,用于防止类被继续继承或者终止虚函数继续重载。

struct Base {
    virtual void some_func() final;
};

8.空指针

C语言中,对NULL表示空指针,让NULL和0分别代表空指针和常量0.NULL可以被定义为((void*)0)或者0。
然而C++不允许将void*隐式转换为其他类型的指针,为了使char* p = NULL;能通过编译,NULL只能定义为0。这就使得函数重载时,出现了0的歧义,比如下面例子:

void fun(int) {}
void fun(char*) {}

fun(NULL); // 执行的是void fun(int) {}

C++11引入了新的关键字来代表空指针常量:nullptr,将空指针和0的概念拆开。nullptr的类型是nullptr_t,可称为“指针空值类型”。nullptrnullptr_t的右值常量,nullptr 可以被隐式转换成任意的指针类型。使用nullptr来给指针赋值后,就不会再出现0的歧义问题。
比如下面这个例子,是将nullptr隐式转换为int*。

int* p = nullptr; // nullptr隐式转换为int*

9.强类型枚举

枚举类型的隐患:

  • C++03中,枚举类型不是类型安全的。枚举类型被视为整形,使得两种不同枚举类型之间可以相互比较。C++03唯一的安全机制是,整型或者枚举类型不能隐式转换为另一个枚举类型。
  • 枚举类型所使用的的整形及其大小都是由编译器定义,无法明确指定。
  • 同一作用域不能定义两个相同的枚举名字

C++11标准可以为枚举指定类型(int、long、short以及它们的unsigned),其他用法与C++03一致

enum MyEnum: unsigned short int
{
    ENUM_1 = 1u,
};
enum MyEnum2: int
{
    ENUM_2 = 20,
};

C++11标准还新增了强类型枚举,使用方法参考下面例子:

enum class MyEnum: unsigned short int
{
    ENUM_1 = 1u,
};
enum class MyEnum2: int
{
    ENUM_2 = 20,
};

强类型枚举不能隐式转换为整形或者比较,基本类型相同的两个枚举之间也不能隐式转换或者比较。但是可以强制转换。

enum Enum1;                     // C++与C++11中不合法;无法判別大小
enum Enum2 : unsigned int;      // 合法的C++11
enum class Enum3;               // 合法的C++11,枚举类型使用默认类型int 
enum class Enum4: unsigned int; // 合法的C++11
enum Enum2 : unsigned short;    // 不合法的C++11,Enum2已被声明为unsigned int

10.角括号

C++03的分析器一律将>>视为右移运算符。但是在嵌套模板定义式中,大多数代表的是两个连续的右角括号。为了避免分析器错误,code时不能把右角括号连着写。
C++11变更了分析器的解读规则:当遇到连续的右角括号时,会在合理的情况下将右尖括号解析为模板引用的结束符号。给使用>,>=,>>的表达式加上圆括号,可以避免其与圆括号外部的左尖括号相匹配。

template<bool bTest> class SomeType;
std::vector<SomeType <1>2 >> x1;   // 解读为std::vector of "SomeType<true> 2>", 所以是非法的表示式,常量1被转换为bool类型true
std::vector<SomeType<(1 > 2)>> x1; // 解读为std::vector of "SomeType<false>", 合法的C++11表示式,(1>2)被转换为bool类型false

11.显式类型转换:explicit

简单回顾一下C++03的explicit的用法,在构造函数中加上explicit关键字后,没法再使用拷贝构造函数把int隐式类型转换为A。

struct A
{
    explicit A(int a){}
};
// A obj = 1; 编译失败
A obj = static_cast<A>(1);

而在C++11中,标准将explicit的使用范围扩展到了自定义类型转换操作符上,以支持“显式类型转换”。

struct A
{
};

struct B
{
    explicit operator A() const {};
};

B obj0;
//A obj1 = obj0; 编译失败
A obj1 = A(obj0);

12.模板别名

12.1.模板类与类模板

类模板,本身是模板不是类,没法像类一样直接构造对象。可以看一看STL的vector的定义,template开头,_ty和_Vector_alloc是模板参数列表,这种形式就是一个类模板。所以vector是个类模板,想要使用vector必须要用类似vector<int>的形式才能构造对象。所以vector<int>、vector<float>、vector<A>等等都是模板类,模板类就可以直接构造对象。

template<class _Ty,
    class _Alloc = allocator<_Ty>>
    class vector
    : public _Vector_alloc<_Vec_base_types<_Ty, _Alloc>>
    {   // varying size array of values
    }

12.2.模板别名

先看一看模板类的别名这么写:

template<class T> struct A;
typedef A<int> A_Int;

如果我们想怼类模板新增别名,C++03中是做不到的,比如下面两行代码都会编译报错

typedef A A_Type;
typedef std::vector<A> Vec_A_Type;

C++11就加了个新语法可以给模板也设置别名,用法如下所示:

template<class U> using A_Type = A<U>;
template<class U> using Vec_A_Type = std::vector<A_Type<U>>;
Vec_A_Type<int> vec_a;

12.3.模板参数的缺省值

C++98支持类模板参数默认值,且默认值需要从右到左依次出现
C++11开始支持函数模板参数默认值,且没有从右到左出现的约束
通常,如果能够从函数实参中推导出类型的话,那么默认模板参数就不会被使用,反之,默认模板参数则可能会被使用。

template<class T=int, int x=1, class U, class V=int, int i=0>
void fun(T t, U u, V v=nullptr);
//some code
fun<int>(1, 2, 'A');

注意:形参设置了默认参数v=nullptr,如果调用到时候是fun<int>(1, 2);就会报错。

13.非受限的union

在C++03标准中,union的成员必须是POD,POD一定可以作为union的成员。
C++11标准移除了除引用类型外的所有union的使用限制

struct Point
{
  Point() {}
  Point(int x, int y): x_(x), y_(y) {}
  int x_, y_;
};
union U
{
     int z;
     double w;
     Point p;  // 在C++03中是不合法(point是non-trivial),但是在C++11是合法的
     U() {} // 由于 Point 成员的存在,必须要定义一个构造函数
     U(const Point& pt) : p(pt) {} // 通过初始化列表构造 Point 对象
     U& operator=(const Point& pt) { new (&p) Point(pt); return *this; } // 通过原地new方式赋值构造Point对象
};

观察一下上面代码,有两点需要注意:

  • C++11 规定,如果非受限联合体内有一个非 POD 的成员,而该成员拥有自定义的构造函数,那么这个非受限联合体的默认构造函数将被编译器删除;其他的特殊成员函数,例如默认拷贝构造函数、拷贝赋值操作符以及析构函数等,也将被删除。所以需要在联合体定义一个构造函数
  • 构造时,采用 placement new 将 pt 构造在其地址 &p 上,这里 placement new 的唯一作用只是调用了一下 Point 类的构造函数。注意,在析构时还需要调用 Point 类的析构函数。

对象构造的改良:

//todo

Reference

C++11 - 维基百科

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容