跳转至

1

语言可用性的强化*

nullptr 和 constexpr*

nullptr*

引入 nullptr 用以区分 NULL 和 0,需要使用 NULL 时应使用 nullptr。

constexpr*

让用户显式地声明函数或对象构造函数在编译期会称为常数。

constexpr 的函数还可以使用递归:

constexpr int fibonacci(const int n) {
    return n == 1 || n == 2 ? 1 : fibonacci(n-1)+fibonacci(n-2);
}

C++14 之后还允许在 constexpr 函数内部使用局部变量、循环和分支等简单语句,C++11 不允许。

类型推导*

传统 C 和 C++,参数类型必须明确定义,对编码没有任何帮助。

C++11 引入了 autodecltype 两个关键字实现了类型推导。

auto*

auto 原本对应 register,如果一个变量没有声明为 register,将自动被视为一个 auto 变量。C++11 后,使用 auto 进行类型推导。

for(vector<int>::const_iterator itr = vec.cbegin(); itr != vec.cend(); ++itr)
// auto
for(auto itr = vec.cbegin(); itr != vec.end(); ++itr)

auto 无法用于函数传参,且不能用于推导数组类型。

decltype*

decltype 关键字是为了解决 auto 关键字只能对变量进行类型推导的缺陷而出现的。

auto x = 1;
auto y = 2;
decltype(x + y) z; // z 是 int 类型

尾返回类型、auto 与 decltype 配合*

auto 推导函数的返回类型,尾返回类型,利用 auto 关键字将返回类型后置:

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

C++14 之后则可以直接用 auto 推导函数类型。

区间迭代*

区间迭代是指基于范围的 for 循环,C++11 引入了类似于 python 的简洁形式:

std::vector<int> arr(5, 100);
for(std::vector<int>::iterator i = arr.begin(); i != arr.end(); ++i) {
    std::cout << *i << std::endl;
}
// 现在就很简单
for(auto i : arr) {    
    std::cout << i << std::endl;
}

初始化列表*

传统 C++ 中,普通数组等类型可以使用 {} 进行初始化,即初始化列表。对于类对象的初始化,要么需要通过拷贝构造、要么就需要使用 () 进行,不能通用。

C++11 首先把初始化列表的概念绑定到了类型上,并将其称之为 std::initializer_list,允许构造函数或其他函数像参数一样使用初始化列表。

#include <initializer_list>

class Magic {
public:
    // 初始化列表构造函数
    Magic(std::initializer_list<int> list) {}
};

Magic magic = {1,2,3,4,5};
std::vector<int> v = {1, 2, 3, 4};

初始化列表除了用在对象构造上,还能将其作为普通函数的形参:

void func(std::initializer_list<int> list) {
    return;
}

func({1,2,3});

模板增强*

外部模板*

传统 C++ 中,模板只有在使用时才会被编译器实例化。重复实例化导致编译的时间增加,并且我们没办法通知编译器不要出发模板实例化。

C++11 引入了外部模板,显式地告诉编译器何时进行模板的实例化:

template class std::vector<bool>;            // 强行实例化
extern template class std::vector<double>;  // 不在该编译文件中实例化模板 

尖括号 '>'*

C++11 开始,连续的右尖括号合法:

std::vector<std::vector<int>> wow;

类型别名模板*

模板是用来生产类型的。typedef 可以为类型定义一个新的名称,但无法作用于模板,因为模板不是类型。typedef 对函数指针的别名语法不同,不易阅读。

C++11 使用 using,同时支持传统的用法:

typedef int (*process)(void *);  // 定义了一个返回类型为 int,参数为 void* 的函数指针类型,名字叫做 process
using process = int(*)(void *); // 同上, 更加直观

template <typename T>
using NewType = SuckType<int, T, 1>;    // 合法

默认模板参数*

C++11 中可以指定模板的默认参数。

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

变长参数模板*

C++11 允许任意个数、任意类别的模板参数,同时也不需要在定义时将参数的个数固定。

template<typename... Ts> class Magic;
class Magic<int, 
            std::vector<int>, 
            std::map<std::string, 
                     std::vector<int>>> darkMagic;

变长参数模板也能直接被调整到模板函数上,如何对参数进行解包:

// sizeof 计算参数个数
template<typename... Args>
void magic(Args... args) {
    std::cout << sizeof...(args) << std::endl;
}

对参数解包的两种经典处理手法:

  • 递归模板函数,不断递归地向函数传递模板参数,进而达到递归遍历所有模板参数的目的:
    #include <iostream>
    template<typename T>
    void printf(T value) {
        std::cout << value << std::endl;
    }
    template<typename T, typename... Args>
    void printf(T value, Args... args) {
        std::cout << value << std::endl;
        printf(args...);
    }
    int main() {
        printf(1, 2, "123", 1.1);
        return 0;
    }
  • 初始化列表展开,递归需要定义一个终止递归的函数:
    template<typename T, typename... Args>
    auto print(T value, Args... args) {
        std::cout << value << std::endl;
        return std::initializer_list<T>{([&] {
            std::cout << args << std::endl;
        }(), value)...};
    }
    int main() {
        print(1, 2.1, "123");
        return 0;
    }
1
使用了 C++11 提供了初始化列表以及 Lambda 表达式的特性,以及 `std::initializer_list` 容器。

面向对象增强*

委托构造*

C++11 引入了委托构造的概念,这使得构造函数可以在同一个类中一个构造函数调用另一个构造函数,达到简化代码的目的:

class Base {
public:
    int value1;
    int value2;
    Base() {
        value1 = 1;
    }
    Base(int value) : Base() {  // 委托 Base() 构造函数
        value2 = 2;
    }
};

int main() {
    Base b(2);
    std::cout << b.value1 << std::endl;
    std::cout << b.value2 << std::endl;
}

继承构造*

在传统 C++ 中,构造函数如果需要继承是需要将参数一一传递的,效率地下。C++11 利用关键字 using 引入了继承构造函数的概念。

class Base {
public:
    int value1;
    int value2;
    Base() {
        value1 = 1;
    }
    Base(int value) : Base() {// 委托 Base() 构造函数
        value2 = 2;
    }
};
class Subclass : public Base {
public:
    using Base::Base;  // 继承构造
};
int main() {
    Subclass s(3);
    std::cout << s.value1 << std::endl;
    std::cout << s.value2 << std::endl;
}

显式虚函数重载*

传统 C++ 中,经常容易发生以外重载虚函数的情况:

struct Base {
    virtual void foo();
};
struct SubClass: Base {
    void foo();
};

SubClass::foo 可能并不是要重载虚函数,只是恰好加入了一个具有相同名字的函数。另一个可能的情形是,当基类的虚函数被删除后,子类拥有旧的函数就不再重载该虚拟函数,而变成一个普通的类方法,这将造成灾难性结果。

C++11 引入 overridefinal 关键字来防止上述情形发生:

  • override

    重载虚函数时,引入 override 关键字将显式地告知编译器进行重载,编译器将检查基函数是否存在这样的虚函数,否则无法通过编译。

    struct Base {
        virtual void foo(int);
    };
    struct SubClass: Base {
        virtual void foo(int) override; // 合法
        virtual void foo(float) override; // 非法, 父类没有此虚函数
    };
  • final

    final 则是为了防止类被继续继承以及终止虚函数继续重载引入的。

    struct Base {
        virtual void foo() final;
    };
    struct SubClass1 final: Base {}; // 合法

    struct SubClass2 : SubClass1 {}; // 非法, SubClass 已 final

    struct SubClass3: Base {
            void foo(); // 非法, foo 已 final
    };

显式禁用默认函数*

传统 C++ 中,编译器会默认为对象生成默认构造函数、复制构造、赋值运算符以及析构函数。另外,C++ 也为所有类定义了主语 new delete 这样的运算符。程序员需要时,可以重载这部分函数。

这就导致无法精确控制默认函数的生成行为。例如禁止类的拷贝时,必须将赋值构造函数与赋值算符声明为 private。尝试使用这些未定义的函数将导致编译或连接错误。

并且,编译器产生的默认构造函数与用户定义的构造函数无法同时存在,但有时候我们却希望同时拥有这两种构造函数。

C++11 提供了上述需求的解决方案,允许显式的声明采用或拒绝编译器自带的函数:

class Magic {
public:
    Magic() = default;  // 显式声明使用编译器生成的构造
    Magic& operator=(const Magic&) = delete; // 显式声明拒绝编译器生成构造
    Magic(int magic_number);
}

强类型枚举*

传统 C++ 中,枚举类型并非类型安全,枚举类型被视作整数,则会让两种完全不同的枚举类型可以进行直接的比较,甚至枚举类型的枚举值名字不能相同。以前的 enum 则相当于 #define 的集合。

C++11 引入了枚举类,并使用 enum class 的语法进行声明:

enum class new_enum : unsigned int {
    value1,
    value2,
    value3 = 100,
    value4 = 100
};

这样定义的枚举实现了类型安全,首先无法被隐式地转换为整数,同时也不能将其与整数数字进行比较,更不可能对不同的枚举类型的枚举值进行比较。但相同枚举值之间如果指定值i相同,可以比较:

if (new_enum::value3 == new_enum::value4) {
    // 会输出
    std::cout << "new_enum::value3 == new_enum::value4" << std::endl;
}

在语法中,枚举类型后面使用了冒号及类型关键字指定枚举中枚举值的类型,使得我们可以为枚举赋值。我们希望获得枚举值的值时,将必须显式地进行类型转换:

enum class new_enum : unsigned int
{
    value1, // 0
    value2, // 1
    value3 = 100,
    value4 = 100
};
cout << static_cast<unsigned int>(new_enum::value4) << endl;

或可以通过重载 << 运算符来进行输出:

template<typename T>
std::ostream& operator<<(typename std::enable_if<std::is_enum<T>::value, std::ostream>::type& stream, const T& e)
{
    return stream << static_cast<typename std::underlying_type<T>::type>(e);
}

总结*

本节介绍了 C++11/14 中对语言可用性的增强,以下几个特性是几乎所有人都需要了解并熟练使用的:

  1. auto 类型推导
  2. 范围 for 迭代
  3. 初始化列表
  4. 变参模板

最后更新: November 26, 2020