[关闭]
@lancelot-vim 2016-07-08T02:35:19.000000Z 字数 18939 阅读 986

1. 核心语言的运行时性能强化

c++

1.1 右值引用和 move 语义

本小节主要参考了 IBM developerWorkds 上的一篇博文: 《C++11 标准新特性: 右值引用与转移语义》: http://www.ibm.com/developerworks/cn/aix/library/1307_lisl_c11/,在此特向原作者表示感谢。

右值引用 (Rvalue Referene) 是 C++11 标准中引入的新特性 , 它实现了转移语义(Move Sementics)和完美转发(Perfect Forwarding)。它的主要目的有两个方面:

C++( 包括 C) 中所有的表达式和变量要么是左值,要么是右值。通俗的左值的定义就是非临时对象,那些可以在多条语句中使用的对象。所有的变量都满足这个定义,在多条代码中都可以使用,都是左值。右值是指临时的对象,它们只在当前的语句中有效。

在 C++11 之前,右值是不能被引用的,最大限度就是利用常量引用来绑定一个右值,如:

const int &a = 1; 

在这种情况下,右值不能被修改的。但是实际上右值是可以被修改的,如:

T().set().get(); 

T 是一个类,set 是一个函数为 T 中的一个变量赋值,get 用来取出这个变量的值。在这句中,T() 生成一个临时对象,就是右值,set() 修改了变量的值,也就修改了这个右值。

既然右值可以被修改,那么就可以实现右值引用。右值引用能够方便地解决实际工程中的问题,实现非常有吸引力的解决方案。

左值的声明符号为 ”&”, 为了和左值区分,右值的声明符号为 ”&&”。

如果临时对象通过一个接受右值的函数传递给另一个函数时,就会变成左值,因为这个临时对象在传递过程中,变成了命名对象。如下示例程序:

void process_value(int& i) { 
    std::cout << "LValue processed: " << i << std::endl; 
} 

void process_value(int&& i) { 
    std::cout << "RValue processed: " << i << std::endl; 
} 

void forward_value(int&& i) { 
    process_value(i); 
} 

int main() { 
    int a = 0; 
    process_value(a); 
    process_value(1); 
    forward_value(2); 
}

虽然 2 这个立即数在函数 forward_value 接收时是右值,但到了 process_value 接收时,变成了左值。

C++03 性能上被长期被诟病的其中之一,就是其耗时且不必要的深度拷贝。深度拷贝会发生在当对象是以传值的方式传递。举例而言,std::vector<T> 是内部保存了 C-style 数组的一个包装,如果一个std::vector<T> 的临时对象被建构或是从函数返回,要将其存储只能通过生成新的 std::vector<T> 并且把该临时对象所有的数据复制进去。该临时对象和其拥有的内存会被摧毁。(为了讨论上的方便,这里忽略返回值优化)

在 C++11,一个 std::vector 的 "move 构造函数" 对某个 vector 的右值引用可以单纯地从右值复制其内部 C-style 数组的指针到新的 std::vector,然后留下空的右值。这个操作不需要数组的复制,而且空的临时对象的析构也不会摧毁内存。传回 std::vector 临时对象的函数不需要显式地传回std::vector<T>&&。如果 std::vector 没有 move 构造函数,那么复制构造函数将被调用,以 const std::vector<T> & 的正常形式。 如果它确实有 move 构造函数,那么就会调用 move 构造函数,能够提高程序效率。

右值引用是用来支持转移语义的。转移语义可以将资源(堆,系统对象等)从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 程序的性能。临时对象的维护(创建和销毁)对性能有严重影响。

转移语义是和拷贝语义相对的,可以类比文件的剪切与拷贝,当我们将文件从一个目录拷贝到另一个目录时,速度比剪切慢很多。

通过转移语义,临时对象中的资源能够转移其它的对象里。

在现有的 C++ 机制中,我们可以定义拷贝构造函数和赋值函数。要实现转移语义,需要定义转移构造函数,还可以定义转移赋值操作符。对于右值的拷贝和赋值会调用转移构造函数和转移赋值操作符。如果转移构造函数和转移拷贝操作符没有定义,那么就遵循现有的机制,拷贝构造函数和赋值操作符会被调用。

普通的函数和操作符也可以利用右值引用操作符实现转移语义。

以一个简单的 string 类为示例,实现拷贝构造函数和拷贝赋值操作符。

class MyString { 
private: 
    char* _data; 
    size_t   _len; 
    void _init_data(const char *s) { 
       _data = new char[_len+1]; 
       memcpy(_data, s, _len); 
       _data[_len] = '\0'; 
    }

public: 
    MyString() { 
        _data = NULL; 
        _len = 0; 
    } 

    MyString(const char* p) { 
        _len = strlen (p); 
        _init_data(p); 
    } 

    MyString(const MyString& str) { 
        _len = str._len; 
        _init_data(str._data); 
        std::cout << "Copy Constructor is called! source: " << str._data << std::endl; 
    } 

    MyString& operator=(const MyString& str) { 
        if (this != &str) { 
            _len = str._len; 
            _init_data(str._data); 
        } 
        std::cout << "Copy Assignment is called! source: " << str._data << std::endl; 
        return *this; 
    } 

    virtual ~MyString() { 
        if (_data) free(_data); 
   } 
}; 

int main() { 
    MyString a; 
    a = MyString("Hello"); 
    std::vector<MyString> vec; 
    vec.push_back(MyString("World")); 
}

运行结果 :

Copy Assignment is called! source: Hello 
Copy Constructor is called! source: World 

这个 string 类已经基本满足我们演示的需要。在 main 函数中,实现了调用拷贝构造函数的操作和拷贝赋值操作符的操作。MyString(“Hello”) 和 MyString(“World”) 都是临时对象,也就是右值。虽然它们是临时的,但程序仍然调用了拷贝构造和拷贝赋值,造成了没有意义的资源申请和释放的操作。如果能够直接使用临时对象已经申请的资源,既能节省资源,有能节省资源申请和释放的时间。这正是定义转移语义的目的。

我们先定义转移构造函数。

MyString(MyString&& str) { 
    std::cout << "Move Constructor is called! source: " << str._data << std::endl; 
    _len = str._len; 
    _data = str._data; 
    str._len = 0; 
    str._data = NULL; 
} 

和拷贝构造函数类似,需要注意几点:

  1. 参数(右值)的符号必须是右值引用符号,即“&&”。
  2. 参数(右值)不可以是常量,因为我们需要修改右值。
  3. 参数(右值)的资源链接和标记必须修改。否则,右值的析构函数就会释放资源。转移到新对象的资源也就无效了。

现在我们定义转移赋值操作符。

MyString& operator=(MyString&& str) { 
    std::cout << "Move Assignment is called! source: " << str._data << std::endl; 
    if (this != &str) { 
        _len = str._len; 
        _data = str._data; 
        str._len = 0; 
      str._data = NULL; 
    } 
    return *this; 
 } 

由此看出,编译器区分了左值和右值,对右值调用了转移构造函数和转移赋值操作符。节省了资源,提高了程序运行的效率。

有了右值引用和转移语义,我们在设计和实现类时,对于需要动态申请大量资源的类,应该设计转移构造函数和转移赋值函数,以提高应用程序的效率。

既然编译器只对右值引用才能调用转移构造函数和转移赋值函数,而所有命名对象都只能是左值引用,如果已知一个命名对象不再被使用而想对它调用转移构造函数和转移赋值函数,也就是把一个左值引用当做右值引用来使用,怎么做呢?标准库提供了函数 std::move,这个函数以非常简单的方式将左值引用转换为右值引用。

基于安全的理由,具名的参数将永远不被认定为右值,即使它是被如此声明的;为了获得右值必须使用 std::move<T>()

完美转发适用于这样的场景:需要将一组参数原封不动的传递给另一个函数。

“原封不动”不仅仅是参数的值不变,在 C++ 中,除了参数值之外,还有一下两组属性:

左值/右值和 const/non-const。完美转发就是在参数传递过程中,所有这些属性和参数值都不能改变。在泛型函数中这样的需求非常普遍。

1.2 泛化的常量表达式 constexpr

C++ 本来就已具备常量表示式(constant expression)的概念。C++11引进关键字 constexpr 允许用户保证函数或是对象建构式是编译期常量。

constexpr 可以修饰变量,函数,和类的构造函数。

当 constexpr 修饰变量时,应满足:

当 constexpr 修饰函数时,应满足:

当 constexpr 修饰类构造函数时,应满足:

请看下例(参考):

#include <iostream>
#include <stdexcept>

// constexpr functions use recursion rather than iteration
constexpr int factorial(int n)
{
    return n <= 1 ? 1 : (n * factorial(n-1));
}

// literal class
class conststr {
    const char * p;
    std::size_t sz;
 public:
    template<std::size_t N>
    constexpr conststr(const char(&a)[N]) : p(a), sz(N-1) {}
    // constexpr functions signal errors by throwing exceptions from operator ?:
    constexpr char operator[](std::size_t n) const {
        return n < sz ? p[n] : throw std::out_of_range("");
    }
    constexpr std::size_t size() const { return sz; }
};

constexpr std::size_t countlower(conststr s, std::size_t n = 0,
                                             std::size_t c = 0) {
    return n == s.size() ? c :
           s[n] >= 'a' && s[n] <= 'z' ? countlower(s, n+1, c+1) :
           countlower(s, n+1, c);
}

// output function that requires a compile-time constant, for testing
template<int n> struct constN {
    constN() { std::cout << n << '\n'; }
};

int main()
{
    std::cout << "4! = " ;
    constN<factorial(4)> out1; // computed at compile time

    volatile int k = 8; // disallow optimization using volatile
    std::cout << k << "! = " << factorial(k) << '\n'; // computed at run time

    std::cout << "Number of lowercase letters in \"Hello, world!\" is ";
    constN<countlower("Hello, world!")> out2; // implicitly converted to conststr
}

1.3 对 POD 类型定义的修正

本小节主要来源于维基百科C++11词条中的《对POD定义的修正》:

在标准C++,一个结构(struct)为了能够被当成 POD,必须遵守几条规则。有很好的理由使我们想让大量的类型符合这种定义,符合这种定义的类型能够允许产生与C兼容的对象布局(object layout)。然而,C++03 的规则太严苛了。

C++11 将会放宽关于 POD 的定义。

当 class/struct 是极简的(trivial)、属于标准布局(standard-layout),以及他的所有非静态(non-static)成员都是 POD 时,会被视为 POD。

一个极简的类型或结构符合以下定义:

一个标准布局(standard-layout)的类型或结构符合以下定义:

2. 核心语言的构建时性能强化

2.1 外部模板

该段摘自维基百科C++11中文词条中《外部模板》一节,并做少量修改。

在标准 C++ 中,只要在编译单元内遇到完整定义的模板,编译器都必须将其实例化(instantiate)。这会大大增加编译时间,特别是模板在许多编译单元内使用相同的参数实例化。

C++11 引入了外部模板这一概念。C++ 已经有了强制编译器在特定位置开始实例化的语法:

template class std::vector<MyClass>;

而 C++ 所缺乏的是阻止编译器在某个编译单元内实例化模板的能力。C++11 简单地扩充语法如下:

extern template class std::vector<MyClass>;

这样就告诉编译器 不要 在该编译单元内将该模板实例化。

3. 核心语言的可用性强化

3.1 初始化列表(std::initializer_list)

C++03 支持数组和简单对象(POD)的初始化值列表,例如:

int array[5] = {-3, -1, 0, 1, 3}; // 数组初始化列表

struct Person {
    int id;
    std::string name;
};
Person p = {111, "Tom"}; // 简单对象的初始化列表

但是上面的语法对复杂类型不可用,例如,在 C++03 标准中初始化一个 std::vector<int> 对象可能需编写如下代码:

int a[] = {0, 1, 2, 3};
std::vector<int> vec(a, a + sizeof(a));

如果 C++ 对复杂类型也能提供一种类似初始化列表的构造方法,那么上面的代码可以简化为:

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

事实上,C++11 标准扩大了初始化列表的概念,并提供了 std::initializer_list 模板类(在 <initializer_list> 头文件中定义):

template< class T >
class initializer_list;

当你使用类的初始化值列表时,C++11 会寻找参数类型为 std::initializer_list 的构造函数。

C++11 中引入 std::initializer_list 给 C++ 语言可用性带来了极大的提升。现在,初始化列表不再仅限于数组。

<initializer_list> 头文件摘要如下:

namespace std {
    template<class E> class initializer_list {
        public:
            typedef E value_type;
            typedef const E& reference;
            typedef const E& const_reference;
            typedef size_t size_type;
            typedef const E* iterator;
            typedef const E* const_iterator;
            initializer_list() noexcept; // 默认构造函数
            size_t size() const noexcept; // 初始化列表元素的个数.
            const E* begin() const noexcept; // 返回指向初始化列表中第一个元素的指针.
            const E* end() const noexcept; // 返回指向最末尾元素的后续位置的指针.
    };

    template<class E> const E* begin(initializer_list<E> il) noexcept;
    template<class E> const E* end(initializer_list<E> il) noexcept;
}

std::initializer_list 默认构造函数将会创建一个空的初始化列表。另外,在以下两种情况下,编译器会自动构建一个非空的初始化列表对象:

  1. 在遇到初始化列表表达式(注: {1, 2, 3, 4} 即为一个简单的初始化列表表达式)时,编译器会自动构建一个非空的初始化列表对象,主要用于函数调用时初始化列表对象作为函数参数传入,或者在赋值表达式中设置某初始化列表对象的值。
  2. 在 auto 修饰符限定下的初始化表达式中(包括基于范围的 for 循环),编译器也会自动构建一个非空的初始化列表对象。

请看下例(参考):

#include <iostream>
#include <initializer_list>

int main() 
{
    std::initializer_list<int> empty_list;
    std::cout << "empty_list.size(): " << empty_list.size() << '\n';

    // create initializer lists using list-initialization
    std::initializer_list<int> digits{1, 2, 3, 4, 5};
    std::cout << "digits.size(): " << digits.size() << '\n';

    // special rule for auto means 'fractions' has the
    // type std::initializer_list<double>
    auto fractions = {3.14159, 2.71828};
    std::cout << "fractions.size(): " << fractions.size() << '\n';
}

下面例子介绍了初始化列表的基本用法(参考):

#include <iostream>
#include <vector>
#include <initializer_list>

template <class T>
struct S {
    std::vector<T> v;
    S(std::initializer_list<T> l) : v(l) {
         std::cout << "constructed with a " << l.size() << "-element list\n";
    }
    void append(std::initializer_list<T> l) {
        v.insert(v.end(), l.begin(), l.end());
    }
    std::pair<const T*, std::size_t> c_arr() const {
        return {&v[0], v.size()};  // list-initialization in return statement
                                   // this is NOT a use of std::initializer_list
    }
};

template <typename T>
void templated_fn(T) {}

int main()
{
    S<int> s = {1, 2, 3, 4, 5}; // direct list-initialization
    s.append({6, 7, 8});        // list-initialization in function call

    std::cout << "The vector size is now " << s.c_arr().second << " ints:\n";

    for (auto n : s.v) std::cout << ' ' << n;

    std::cout << '\n';

    std::cout << "range-for over brace-init-list: \n";

    for (int x : {-1, -2, -3}) // the rule for auto makes this ranged for work
        std::cout << x << ' ';
    std::cout << '\n';

    auto al = {10, 11, 12};   // special rule for auto

    std::cout << "The list bound to auto has size() = " << al.size() << '\n';

    // templated_fn({1, 2, 3}); // compiler error! "{1, 2, 3}" is not an expression,
                                // it has no type, and so T cannot be deduced
    templated_fn<std::initializer_list<int>>({1, 2, 3}); // OK
    templated_fn<std::vector<int>>({1, 2, 3});           // also OK
}

函数执行结果如下:

constructed with a 5-element list
The vector size is now 8 ints:
 1 2 3 4 5 6 7 8
range-for over brace-init-list: 
-1 -2 -3 
The list bound to auto has size() = 3

3.2 统一的初始化方式

C++11 将会提供一种统一的语法初始化任意的对象,它扩充了初始化列表语法,无论是 POD 还是非 POD 类型(有关 POD 类型的定义,请参考本文)都可以使用 obj = { ... } 的方式来进行初始化或赋值。对于非 POD 类型,若通过 { ... } 形式进行对象的初始化和赋值,编译器将自动匹配并调用构造函数或 "=" 赋值操作,参见 3.1 初始化列表。因此,构造函数 T(x, y) 现在也可以统一写成:T{x, y}T var{x, y} 等。

请看下例(参考):

struct BasicStruct
{
 int x;
 float y;
};

struct AltStruct
{
  AltStruct(int _x, float _y) : x(_x), y(_y) {}

private:
  int x;
  float y;
};

BasicStruct var1{5, 3.2f};
AltStruct var2{2, 4.3f};

另外,在需要返回一个 T 类型对象时,可以直接写:return {x, y};,统一的初始化语法能够免除指明特定类型的必要,例如(参考):

struct IdString
{
  std::string name;
  int identifier;
};

IdString var3{"SomeName", 4};

该语法将会使用 const char * 参数初始化 std::string,在函数返回某个 IdString 对象时,可以直接编写如下代码:

IdString GetString()
{
  return {"SomeName", 4}; // 不需指明特定的类型。
}

3.3 类型推导(auto 和 decltype 关键字)

C++03 标准中变量和参数必须明确指明类型,但是随着模板类型的出现以及模板元编程的技巧,对象的类型特别是函数的返回类型就不容易表示了。C++11 标准针对上面的情况引入了 auto 和 decltype 关键字(实际上,auto 关键字在旧的 C++ 标准中即存在,只不过在 C++11 标准中新增了类型自动推导语义)。

如果某个对象在初始化时类型已经明确,那么可以使用 auto 关键字来简化对象的声明,该对象的类型会根据初始化算子(initializer: 姑且翻译为"初始化算子"吧,包括类的构造函数或某个基本类型的变量值(比方说整型(int) 5)自动推导出来,使用 auto 来声明变量的语法如下:

auto variable initializer 

例如:

auto otherVariable = 5; // otherVariable 的类型为 int。
const auto *v = &x, u = 6; // 正确,v 的类型为 const int*, u 的类型为 const int。
static auto y = 0.0; // y 推导为 double 类型。
auto int r; // 错误: auto 在 C++11 中不是存储类型修饰符。

auto 类型修饰符也可以出现在带有返回类型的函数(比如 std::vector<T>::begin() 等)的返回值前面,用于指定该函数返回值的类型,在返回值类型很复杂的情况下,auto 的类型推导可以减少大量冗赘代码。例如:

// someStrangeCallableType 是某个类的成员函数类型,该类型也可以使用 std::function<> 来声明。
auto someStrangeCallableType = std::bind(&SomeFunction, _2, _1, someObject);

auto 的另外一种用法是修饰函数,主要用于函数声明,在很多情况下,我们并不能提前知道函数的返回值类型(即函数的返回值类型通常由其参数决定),那么此时 auto 关键字就可以派上用场了,auto 关键字修饰函数的语法如下:

auto function -> return type

请看下面完整的例子:参考

#include <iostream>
#include <cmath>
#include <typeinfo>

template<class T, class U>
auto add(T t, U u) -> decltype(t + u) // the return type of add is the type of operator+(T,U)
{
    return t + u;
}

auto get_fun(int arg)->double(*)(double) // same as double (*get_fun(int))(double)
{
    switch (arg) {
        case 1: return std::fabs;
        case 2: return std::sin;
        default: return std::cos;
    }
}

int main()
{
    auto a = 1 + 2;
    std::cout << "type of a: " << typeid(a).name() << '\n';
    auto b = add(1, 1.2);
    std::cout << "type of b: " << typeid(b).name() << '\n';
    //auto int c; //compile-time error
    auto d = {1, 2};
    std::cout << "type of d: " << typeid(d).name() << '\n';

    auto my_lambda = [](int x) { return x + 3; };
    std::cout << "my_lambda: " << my_lambda(5) << '\n';

    auto my_fun = get_fun(2);
    std::cout << "type of my_fun: " << typeid(my_fun).name() << '\n';
    std::cout << "my_fun: " << my_fun(3) << '\n';
}

decltype 和 auto 一起使用会更为有用,因为 auto 参数的类型只有编译器知道。然而 decltype 对于那些大量运用运算符重载和特化的类型的代码的表示也非常有用。

auto 对于减少冗赘的代码也很有用。例如:

for (vector<int>::const_iterator itr = myvec.cbegin(); itr != myvec.cend(); ++itr)

可以改写成更简洁的:

for (auto itr = myvec.cbegin(); itr != myvec.cend(); ++itr)

Decltype 主要对值和表达式的类型推导,decltype 推导规则如下:

  1. 如果表达式 e 是一个变量,那么由 decltype 推导出来的类型就是这个变量的类型。
  2. 如果表达式 e 是一个函数,那么由 decltype 推导出来的类型就是这个函数返回值的类型。
  3. 如果不符合 1 和 2,如果 e 是左值,类型为 T,那么 decltype(e) 是 T&;如果是右值,则是 T。

3.4 基于范围的 for 循环

基于范围的for循环可以用非常简单的方式迭代集合中的每一项,C++11 标准中规定基于范围的 for 循环具有如下形式:

for (for-range-declaration : expression) statement

基于范围的 for 语句可以构造出在某个范围内执行的循环语句,上述语句可以做如下解释:为 expression 的每个元素重复并按顺序执行 statement。例如:

int array[5] = { 1, 2, 3, 4, 5 };
for (int& x : array)
    x *= 2;

更完整的例子如下

#include <iostream>
#include <vector>
using namespace std;

int main() 
{
    // Basic 10-element integer array.
    int x[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

    // Range-based for loop to iterate through the array.
    for( int y : x ) { // Access by value using a copy declared as a specific type. 
                       // Not preferred.
        cout << y << " ";
    }
    cout << endl;

    // The auto keyword causes type inference to be used. Preferred.

    for( auto y : x ) { // Copy of 'x', almost always undesirable
        cout << y << " ";
    }
    cout << endl;

    for( auto &y : x ) { // Type inference by reference.
        // Observes and/or modifies in-place. Preferred when modify is needed.
        cout << y << " ";
    }
    cout << endl;

    for( const auto &y : x ) { // Type inference by reference.
        // Observes in-place. Preferred when no modify is needed.
        cout << y << " ";
    }
    cout << endl;
    cout << "end of integer array test" << endl;
    cout << endl;

    // Create a vector object that contains 10 elements.
    vector<double> v;
    for (int i = 0; i < 10; ++i) {
        v.push_back(i + 0.14159);
    }

    // Range-based for loop to iterate through the vector, observing in-place.
    for( const auto &j : v ) {
        cout << j << " ";
    }
    cout << endl;
    cout << "end of vector test" << endl;
}

基于范围的 for 循环语句在如下条件下会退出执行,1、break,2、return,3、goto 至该 for 循环语句之外某一的标签语句处。

注意,基于范围的 for 语句会:

3.5 lambda 表达式

C++11 新增了很多特性,lambda 表达式是其中之一。很多语言都提供了 lambda 表达式,如 Python,Java 8。lambda 表达式可以方便地构造匿名函数,如果你的代码里面存在大量的小函数,而这些函数一般只被调用一次,那么不妨将他们重构成 lambda 表达式。

[ capture ] ( params ) mutable exception attribute -> ret { body } (1)  
[ capture ] ( params ) -> ret { body } (2)  
[ capture ] ( params ) { body } (3)  
[ capture ] { body } (4)  

其中:

mutable 修饰符说明 lambda 表达式体内的代码可以修改被捕获的变量,并且可以访问被捕获对象的 non-const 方法。

exception 说明 lambda 表达式是否抛出异常(noexcept),以及抛出何种异常,类似于 void f() throw(X, Y)

attribute 用来声明属性。

另外,capture 指定了在可见域范围内 lambda 表达式的代码内可见得外部变量的列表,具体解释如下:

此外,params 指定 lambda 表达式的参数。

请看下面 lambda 表达式的例子:

#include <vector>
#include <iostream>
#include <algorithm>
#include <functional>

int main()
{
    std::vector<int> c { 1,2,3,4,5,6,7 };
    int x = 5;
    c.erase(std::remove_if(c.begin(), c.end(), [x](int n) { return n < x; } ), c.end());

    std::cout << "c: ";
    for (auto i: c) {
        std::cout << i << ' ';
    }
    std::cout << '\n';

    auto func1 = [](int i) { return i + 4; };
    std::cout << "func1: " << func1(6) << '\n'; 

    std::function<int(int)> func2 = [](int i) { return i + 4; };
    std::cout << "func2: " << func2(6) << '\n'; 
}

3.6 另一种可选的函数语法

本小节主要来自于维基百科中文 C++11 介绍

标准 C 函数声明语法对于 C 语言已经足够。 演化自 C 的 C++ 除了 C 的基础语法外,又扩充额外的语法。 然而,当 C++ 变得更为复杂时,它暴露出许多语法上的限制, 特别是针对函数模板的声明。 下面的示例,不是合法的 C++03:

template< typename LHS, typename RHS> 
Ret AddingFunc(const LHS &lhs, const RHS &rhs) { return lhs + rhs; } //Ret的类型必须是(lhs+rhs)的类型

Ret 的类型由 LHS 与 RHS 相加之后的结果的类型来决定。 即使使用 C++11 新加入的 decltype 来声明 AddingFunc 的返回类型,依然不可行。

template< typename LHS, typename RHS> 
decltype(lhs+rhs) AddingFunc(const LHS &lhs, const RHS &rhs) { return lhs + rhs; } //不合法的 C++11

不合法的原因在于 lhs 及 rhs 在定义前就出现了。 直到编译器解析到函数原型的后半部,lhs 与 rhs 才是有意义的。

针对此问题,C++11 引进一种新的函数声明与定义的语法:

template< typename LHS, typename RHS> 
auto AddingFunc(const LHS &lhs, const RHS &rhs) -> decltype(lhs+rhs) { return lhs + rhs; }

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

struct SomeStruct
{
    auto FuncName(int x, int y) -> int;
};

auto SomeStruct::FuncName(int x, int y) -> int
{
      return x + y;
}

关键字 auto 的使用与其在自动类型推导代表不同的意义。

3.7 对象创建优化

在 C++11 中,一个构造函数可以调用该类中的其它构造函数来完成部分初始化任务(类似于 C# 中的委托)。声明成员时可以直接指定默认初始值,例如:

class SomeType {
  int number;
  string name;
  SomeType( int i, string& s ) : number(i), name(s){}
public:
  SomeType( )           : SomeType( 0, "invalid" ){}
  SomeType( int i )     : SomeType( i, "guest" ){}
  SomeType( string& s ) : SomeType( 1, s ){ PostInit(); }
};

委托可以在一定程度上简化与类初始化相关代码,也更利于编译器优化。

3.8 显式虚函数重载

3.9 空指针常量(nullptr)

C++03 标准中的 NULL 是一个与实现相关的空指针常量,即:

#define NULL /*implementation-defined*/

某些编译器将其定义为整数 0,然而也有编译器将其定义为 void 指针类型: (void*)0。在某些情况下,这会造成二义性,例如:

void func(int); // 参数为整型
void func(char *);// 参数为指针类型

func(NULL); //二义性,无法区别调用 func(int); 还是 func(char *);

C++11 引入了一个新的常量空指针 nullptr, 其类型为 std::nullptr_t, nullptr_t 定义如下:

typedef decltype(nullptr) nullptr_t;

nullptr 是一个纯右值(prvale, pure rvalue),有关 C++11 左值与右值的解释,可参考 此文

在 C++11 中,调用 func(nullptr) 将会直接调用 func(char*),此时不存在二义性。

nullptr 作为函数参数也通过模板类型进行转发,请参考下例:

#include <cstddef>
#include <iostream>

template<class F, class A>
void Fwd(F f, A a)
{
    f(a);
}

void g(int* i)
{
    std::cout << "Function g called\n";
}

int main()
{
    Fwd(g, nullptr);   // 正确
    // Fwd(g, NULL);   // 错误: 没有定义 g(int)
}

如果一个函数存在多种指针类型的重载,建议提供一个 std::nullptr_t 类型的重载版本。示例如下,参考

#include <cstddef>
#include <iostream>

void f(int* pi)
{
   std::cout << "Pointer to integer overload\n";
}

void f(double* pd)
{
   std::cout << "Pointer to double overload\n";
}

void f(std::nullptr_t nullp)
{
   std::cout << "null pointer overload\n";
}

int main()
{
    int* pi; double* pd;

    f(pi);
    f(pd);
    f(nullptr);  // would be ambiguous without void f(nullptr_t)
    // f(NULL);  // ambiguous overload: all three functions are candidates
}

3.10 强类型枚举

本小节主要来自维基百科中文词条 C+11 强类型枚举 一节

在标准 C++ 中,枚举类型不是类型安全的。枚举类型被视为整数,这使得两种不同的枚举类型之间可以进行比较。C++03 唯一提供的安全机制是一个整数或一个枚举型值不能隐式转换到另一个枚举别型。

此外,枚举所使用整数类型及其大小都由实现方法定义,无法明确指定。 最后,枚举的名称全数暴露于一般作用域中,因此两个不同的枚举,不可以有相同的枚举名。 (例如 enum Side{ Right, Left };enum Thing{ Wrong, Right }; 不能一起使用。)

C++11 引入了一种特别的 "枚举类",可以避免上述的问题。使用 enum class 的语法来声明:

enum class Enumeration
{
  Val1,
  Val2,
  Val3 = 100,
  Val4 /* = 101 */,
};

上述枚举为类型安全的。枚举类型不能隐式地转换为整数;也无法与整数数值做比较。 (表达式 Enumeration::Val4 == 101 将会触发编译期错误)。

枚举类型所使用类型必须显式指定。在上面的示例中,使用的是默认类型 int,但也可以指定其他类型:

enum class Enum2 : unsigned int {Val1, Val2};

枚举类型的作用域(scoping)定义为枚举类型的名称范围中。 使用枚举类型的枚举名时,必须明确指定其所属范围。 以前述枚举类型 Enum2 为例,Enum2::Val1 是有意义的表示法, 而单独的 Val1 则不是有意义的表示法。

此外,C++11 允许为传统的枚举指定类型:

enum Enum3 : unsigned long {Val1 = 1, Val2};

枚举名 Val1 定义于 Enum3 的枚举范围中(Enum3::Val1),但为了兼容性, Val1 仍然可以于一般的范围中单独使用。

在 C++11 中,枚举类型的前置声明 (forward declaration) 也是可行的,只要使用可指定类型的新式枚举即可。 之前的 C++ 无法写出枚举的前置声明,是由于无法确定枚举参数所占的空间大小, C++11 解决了这个问题:

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

3.11 右尖括号(>)

标准 C++03 的语法分析器一律将 ">>" 视为右移运算符。 但在模板定义式中,很多情况下 ">>" 其实代表了两个连续右角括号。C++11 新标准不在要求声明嵌套模板时使用空格将尖括号分开。

3.12 显式类型转换操作符

3.13 模板别名

为了定义模板的别名,C++11 增加了以下的语法:

template< typename first, typename second, int third>
class SomeType;

template< typename second>
using TypedefName = SomeType<OtherType, second, 5>;

using 也能在 C++11 中定义一般类型的别名,等同 typedef

typedef void (*Function)(double);            // 传统语法
using Function = void (*)(double);           // C++11 新增语法

3.14 无限制 union

在标准 C++ 中,并非任意的类型都能做为 union 的成员。比方说,带有 non-trivial 构造函数的类型就不能是 union 的成员。在新的标准里,移除了所有对 union 的使用限制,但规定 union 的成员不能为引用类型。

请看下例:

struct Point
{
  Point() {}
  Point(int x, int y): x_(x), y_(y) {}
  int x_, y_;
};

union
{
  int z;
  double w;
  Point p;  // 在 C++03 标准中非法; Point 有一 non-trivial 构造函数
            // 但是在 C++11 标准中合法
};
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注