首页 > 代码库 > C++11 FAQ

C++11 FAQ



C++11 FAQ

auto – 从初始化中推断数据类型
当数据类型未知或不便书写时,使用auto可让编译器自动根据用以初始化变量的表达式类型来推导变量类型。考虑如下代码:
template<class T > void printall(const vector< T>& v)
{
        // 根据v.begin()的返回值类型自动推断p的数据类型
        for ( auto p = v.begin(); p != v.end(); ++p)
              cout << *p << “n”;
}
枚举类——具有类域和强类型的枚举
枚举类主要用来解决传统的C++枚举的三个问题:
  • 传统C++枚举会被隐式转换为int,这在那些不应被转换为int的情况下可能导致错误
  • 传统C++枚举的每一枚举值在其作用域范围内都是可见的,容易导致名称冲突(同名冲突)
  • 不可以指定枚举的底层数据类型,这可能会导致代码不容易理解、兼容性问题以及不可以进行前向声明

        enum Alert { green , yellow , election , red };        // 传统枚举
        enum class Color { red, blue };                     // 新的具有类域和强类型的枚举类
                                                            // 它的枚举值在类的外部是不可直接访问的,需加“类名::”
                                                             // 不会被隐式转换成int
        enum class TrafficLight { red , yellow , green };
        Alert a = 7;                                        // 错误,传统枚举不是强类型的,a没有数据类型
        Color c = 7;                                        // 错误,没有int到Color的隐式转换
        int a2 = red;                                       // 正确,Alert被隐式转换成int
        int a3 = Alert:: red;                                // 在 C++98中是错误的,但是在C++11中正确的
        int a4 = blue;                                      // 错误,blue并没有在类域中
        int a5 = Color:: blue;                               // 错误,没有Color到int的默认转换
        Color a6 = Color:: blue;                             // 正确

正如上面的代码所展示的那样,传统的枚举可以照常工作,但是你现在可以通过提供一个类名来改善枚举的使用,使其成为一个强类型的具有类域的枚举。因为新的具有类名的枚举具有传统的枚举的功能(命名的枚举值),同时又具有了类的一些特点(枚举值作用域处于类域之内且不会被隐式类型转换成int),所以我们将其称为枚举类(enum class)。
因为可以指定枚举的底层数据类型,所以可以进行简单的互通操作以及保证枚举值所占的字节大小:
enum class Color : char { red , blue };                                    // 紧凑型表示(一个字节)
enum EE : unsigned long { EE1 = 1, EE2 = 2, EEbig = 0xFFFFFFF0U };          // C11中我们可以指定枚举值的底层数据类型大小
同时,由于能够指定枚举值的底层数据类型,所以前向声明得以成为可能:
enum class Color_code : char ;     // (前向) 声明
void foobar(Color_code * p);        // 使用
// ...
// 定义
enum class Color_code : char { red , yellow , green , blue };
常量表达式(constexpr) — 一般化的受保证的常量表达式
常量表达式机制是为了:
  • 提供一种更加通用的常量表达式
  • 允许用户自定义的类型成为常量表达式
  • 提供了一种保证在编译期完成初始化的方法(可以在编译时期执行某些函数调用)

考虑下面这段代码:
        enum Flags { good = 0, fail = 1, bad = 2, eof = 4 };
       constexpr int operator|( Flags f1, Flags f2)
       {
               return Flags( int( f1) | int( f2));
       }
        void f( Flags x)
       {
               switch ( x) {
               case bad:         /* … */ break ;
               case eof:         /* … */ break ;
               case bad | eof:   /* … */ break ;
               default:          /* … */ break ;
              }
       }
在这里,常量表达式关键字constexpr表示这个重载的操作符“|”只应包含形式简单的运算,如果它的参数本身就是常量 ,那么这个操作符应该在编译时期就应该计算出它的结果来。(译注: 我们都知道,switch的分支条件要求常量,而使用constexpr关键字重载操作符“|”之后,虽然“bad|eof”是一个表达式,但是因为这两个参数都是常量,在编译时期,就可以计算出它的结果,因而也可以作为常量对待。)
除了可以在编译时期被动地计算表达式的值之外,我们希望能够强制要求表达式在编译时期计算其结果值,从而用作其它用途,比如对某个变量进行赋值。当我们在变量声明前加上constexpr关键字之后,可以实现这一功能,当然,它也同时会让这个变量成为常量。
        constexpr int x1 = bad | eof;    // ok
        void f(Flags f3)
       {
               // 错误:因为f3不是常量,所以无法在编译时期计算这个表达式的结果值
              constexpr int x2 = bad | f3;
               int x3 = bad | f3;     // ok,可以在运行时计算
       }

需要注意的是,constexpr并不是const的通用版,反之亦然:

  • const主要用于表达“对接口的写权限控制”,即“对于被const修饰的量名(例如const指针变量),不得通过它对所指对象作任何修改”。(但是可以通过其他接口修改该对象)。另外,把对象声明为const也为编译器提供了潜在的优化可能。具体来说就是,如果把一个量声明为const,并且没有其他地方对该量作取址运算,那么编译器通常(取决于编译期实现)会用该量的实际常量值直接替换掉代码中所有引用该量的地方,而不用在最终编译结果中生成对该量的存取指令。
  • constexpr的主要功能则是让更多的运算可以在编译期完成,并能保证表达式在语义上是类型安全的。(译注:相比之下,C语言中#define只能提供简单的文本替换,而不具任何类型检查能力)。与const相比,被constexpr修饰的对象则强制要求其初始化表达式能够在编译期完成计算。之后所有引用该常量对象的地方,若非必要,一律用计算出来的常量值替换。
decltype – 推断表达式的数据类型
decltype(E)是一个标识符或者表达式的推断数据类型(“declared type”),它可以用在变量声明中作为变量的数据类型。
void f(const vector<int>& a, vector<float>& b)
{
        // 推断表达式a[0]*b[0]的数据类型,并将其定义为Temp类型
        typedef decltype( a[0] * b[0]) Tmp;
        // 使用Tmp作为数据类型声明变量,创建对象

        for ( int i = 0; i < b.size(); ++i) {
               Tmp* p = new Tmp( a[i] * b[i]);
               // …
       }
        // …
} 
如果你仅仅是想根据初始化值为一个变量推断合适的数据类型,那么使用auto是一个更加简单的选择。当你只有需要推断某个表达式的数据类型,例如某个函数调用表达式的计算结果的数据类型,而不是某个变量的数据类型时,你才真正需要delctype。
控制默认函数——默认或者禁用
我们都知道,在我们没有显式定义类的复制构造函数和赋值操作符的情况下,便编译器会为我们生成默认的复制构造函数和赋值操作符,以内存复制的形式完成对象的复制。虽然这种机制可以为我们节省很多编写复制构造函数和赋值操作符的时间,但是在某些情况下,比如我们不希望对象被复制,这种机制却是多此一举。
关于类的“禁止复制”,现在可以使用delete关键字完美地直接表达:
class X {
        // …

        X& operator=( const X&) = delete;   // 禁用类的赋值操作符
       X( const X&) = delete;
};
相反地,我们也可以使用default关键字,显式地指明我们希望使用默认的复制操作:
class Y {
        // …
        // 使用默认的赋值操作符和复制构造函数
        Y& operator=( const Y&) = default;   // 默认的复制操作
       Y( const Y&) = default;
};
显式地使用default关键字声明使用类的默认行为,对于编译器来说明显是多余的,但是对于代码的阅读者来说,使用default显式地定义复制操作,则意味着这个复制操作就是一个普通的默认的复制操作。 将默认的操作留给编译器去实现将更加简单,更少的错误发生 ,并且通常会产生更好的目标代码。
“default”关键字可以用在任何的默认函数中,而“delete”则可以用于修饰任何函数。例如,我们可以通过下面的方式排除一个不想要的函数参数类型转换:
struct Z {
        // …

       Z( long long);     // 可以通过long long初始化
        // 但是不能使用更短的long进行初始化,
        // 也就不是不允许long到long long的隐式转型(感谢abel)
       Z( long) = delete;
};
委托构造函数(Delegating constructors)

在C++98中,如果你想让两个构造函数完成相似的事情,可以写两个大段代码相同的构造函数,或者是另外定义一个init()函数,让两个构造函数都调用这个init()函数。这样的实现方式重复罗嗦,并且容易出错。并且,这两种方式的可维护性都很差。所以,在C++0x中,我们可以在定义一个构造函数时调用另外一个构造函数:
class X {
        int a;
public:
       X( int x) { if (0< x && x <= max) a = x; else throw bad_X( x); }
        // 构造函数X()调用构造函数X(int x)
       X() : X{ 42 } { }
        // 构造函数X(string s)调用构造函数X(int x)
       X(string s) : X{ lexical_cast< int>(s) } { }  // lexical_cast 是boost库中的万能转化函数
        // …
};
序列for循环语句
这里所谓的区间可以是任一种能用迭代器遍历的区间,例如STL中由begin()和end()定义的序列。所有的标准容器,例如std::string、 初始化列表、数组,甚至是istream,只要定义了begin()和end()就行。
void f(const vector& v)
{
        for ( auto x : v) cout << x << ‘n’;
        for ( auto& x : v) ++x;    // 使用引用,方便我们修改容器中的数据
}
返回类型后置语法
如下代码要如何判断返回值呢?
template<class T, class U>
? ? ? mul(T x, U y)
{
        return x*y;
}
C++11中引入了返回类型后置语法可以解决这个问题
template<class T, class U>
auto mul(T x, U y) -> decltype (x*y)
{
        return x*y;
}
返回值后置语法最初并不是用于模板和返回值类型推导的,它实际是用于解决作用域问题的。
struct List
{
        struct Link{
               /* ... */
       };
        Link* erase( Link* p);
};

List::Link * List ::erase(Link *p )
{
        /* ... */
}

auto List ::erase(Link * p ) -> Link *
{      
        /* ... */
}
类内部成员的初始化
允许非静态(non-static)数据成员在其声明处(在其所属类内部)进行初始化。这样,在运行时,需要初始值时构造函数可以使用这个初始值。考虑下面的代码:
class A {
public:
        int a = 7;
};
这等同于:
class A {
public:
        int a;
       A() : a(7) {}
};
这样的好处是多个构造函数时,不用每个构造函数中都对变量进行初始化了。如果一个成员同时在类内部初始化时和构造函数内被初始化,则只有构造函数的初始化有效(这个初始化值“优先于”默认值)(译注:可以认为,类内部初始化先于构造函数初始化进行,如果是对同一个变量进行初始化,构造函数初始化会覆盖类内部初始化)。
class A {
public:
       A() {}
       A( int a_val) : a( a_val) {} // 优先级高

        int a = 7;
        int b = 5;
};
初始化列表
可以接受一个“{}列表”对变量进行初始化的机制实际上是通过一个可以接受参数类型为std::initializer_list的函数(通常为构造函数)来实现的。std::vector拥有一个参数类型为int的显式构造函数及一个带有初始化列表的构造函数:
template<class E > class vector {
public:
        // 初始化列表构造函数
        vector( std::initializer_list<E > s )
       {
               // 预留出合适的容量
               reserve(s .size());    //
               // 初始化所有元素
               uninitialized_copy(s .begin(), s.end(), elem);
               sz = s. size(); // 设置容器的size
       }
        // ... 其他部分保持不变 ...
};
 // ...
vector<double > v1 (7);       // OK: v1有7个元素
vector<double > v2 { 7, 9 };  // OK: v2有2个元素,其值为7.0和9.0
v1 = 9;                       // Err: 无法将int转换为vector
vector<double > v3 = 9;       // Err: 无法将int转换为vector
v1 { 7, 9 };                // OK: v1有2个元素,其值为7.0和9.0
仅具有一个std::initializer_list的单参数构造函数被称为初始化列表构造函数。函数可以将initializer_list作为一个不可变的序列进行读取。
template <class T >
void printAll (initializer_list<T> args)
{
        for ( auto & item : args)
               cout << item << endl;
}
printAll({ 0, 1, 2, 3, 4, 5, 6 });
printAll({ "asdf", "adadf" , "234" });
内联命名空间
内联命名空间旨在通过”版本”的概念,来实现库的演化。考虑如下代码:
// 文件:V99.h
inline namespace V99 {
        void f( int);     // 对V98版本进行改进
        void f( double);  // 新特性
        // …
}
// 文件:V98.h
namespace V98 {
        void f( int);        // V98版本只实现基本功能
        // …
}
// 文件:Mine.h
namespace Mine {
#include “V99.h”
#include “V98.h”
}
上述命名空间Mine中同时包含了较新的版本(V99)以及早期的版本(V98),如果你需要显式使用(某个版本的函数),你可以:
#include “Mine.h”
using namespace Mine ;
// …
V98::f (1);        // 早期版本
V99::f (1);        // 较新版本
f(1);            // 默认版本(V99)
此处的要点在于,被inline修饰的内联命名空间,其内部所包含的所有类/函数/变量等声明,看起来就好像是直接在外围的命名空间中进行声明的一样。
Lambda表达式
C++ 11中的Lambda表达式用于定义并创建匿名的函数对象,以简化编程工作。Lambda的语法形式如下:
              [函数对象参数] (操作符重载函数参数) mutable或exception声明 ->返回值类型 {函数体}
      可以看到,Lambda主要分为五个部分:[函数对象参数]、(操作符重载函数参数)、mutable或exception声明、->返回值类型、{函数体}。下面分别进行介绍。
      一、[函数对象参数],标识一个Lambda的开始,这部分必须存在,不能省略。函数对象参数是传递给编译器自动生成的函数对象类的构造函数的。函数对象参数只能使用那些到定义Lambda为止时Lambda所在作用范围内可见的局部变量(包括Lambda所在类的this)。函数对象参数有以下形式:
           1、空。没有使用任何函数对象参数。
           2、=。函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括Lambda所在类的this),并且是值传递方式(相当于编译器自动为我们按值传递了所有局部变量)。
           3、&。函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括Lambda所在类的this),并且是引用传递方式(相当于编译器自动为我们按引用传递了所有局部变量)。
           4、this。函数体内可以使用Lambda所在类中的成员变量。
           5、a。将a按值进行传递。按值进行传递时,函数体内不能修改传递进来的a的拷贝,因为默认情况下函数是const的。要修改传递进来的a的拷贝,可以添加mutable修饰符。
           6、&a。将a按引用进行传递。
           7、a, &b。将a按值进行传递,b按引用进行传递。
           8、=,&a, &b。除a和b按引用进行传递外,其他参数都按值进行传递。
           9、&, a, b。除a和b按值进行传递外,其他参数都按引用进行传递。
      二、(操作符重载函数参数),标识重载的()操作符的参数,没有参数时,这部分可以省略。参数可以通过按值(如:(a,b))和按引用(如:(&a,&b))两种方式进行传递。
      三、mutable或exception声明,这部分可以省略。按值传递函数对象参数时,加上mutable修饰符后,可以修改按值传递进来的拷贝(注意是能修改拷贝,而不是值本身)。exception声明用于指定函数抛出的异常,如抛出整数类型的异常,可以使用throw(int)。
      四、->返回值类型,标识函数返回值的类型,当返回值为void,或者函数体中只有一处return的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略。
      五、{函数体},标识函数的实现,这部分不能省略,但函数体可以为空。
      下面给出了一段示例代码,用于演示上述提到的各种情况,代码中有简单的注释可作为参考。
#include <vector>
#include<algorithm> // for_each
using namespace std;
class CTest
{
public:
        CTest() : m_nData(20) { NULL; }
        void TestLambda()
       {
               vector<int > vctTemp ;
               vctTemp. push_back(1);
               vctTemp. push_back(2);

              // 无函数对象参数,输出:1 2
              {
                      for_each(vctTemp .begin(), vctTemp.end(), []( int v){ cout << v << endl; });
              }

              // 以值方式传递作用域内所有可见的局部变量(包括this),输出:11 12
              {
                      int a = 10;
                      for_each(vctTemp .begin(), vctTemp.end(), [=]( int v){ cout << v + a << endl; });
              }

              // 以引用方式传递作用域内所有可见的局部变量(包括this),输出:11 13 12
              {
                      int a = 10;
                      for_each(vctTemp .begin(), vctTemp.end(), [&]( int v) mutable{ cout << v + a << endl; a++; });
                      cout << a << endl;
              }

              // 以值方式传递局部变量a,输出:11 13 10
              {
                      int a = 10;
                      for_each(vctTemp .begin(), vctTemp.end(), [ a]( int v) mutable{ cout << v + a << endl; a++; });
                      cout << a << endl;
              }

              // 以引用方式传递局部变量a,输出:11 13 12
              {
                      int a = 10;
                      for_each(vctTemp .begin(), vctTemp.end(), [& a]( int v){ cout << v + a << endl; a++; });
                      cout << a << endl;
              }

              // 传递this,输出:21 22
              {
                      for_each(vctTemp .begin(), vctTemp.end(), [ this]( int v){ cout << v + m_nData << endl; });
              }

              // 除b按引用传递外,其他均按值传递,输出:11 12 17
              {
                      int a = 10;
                      int b = 15;
                      for_each(vctTemp .begin(), vctTemp.end(), [=, & b]( int v){ cout << v + a << endl; b++; });
                      cout << b << endl;
              }


              // 操作符重载函数参数按引用传递,输出:2 3
              {
                      for_each(vctTemp .begin(), vctTemp.end(), []( int & v){ v++; });
                      for_each(vctTemp .begin(), vctTemp.end(), []( int v){ cout << v << endl; });
              }

              // 空的Lambda表达式
              {
                     [](){}();
                     []{}();
              }
       }

private:
        int m_nData;
};
用作模板参数的局部类
在C++98中,局部类和未命名类不能作为模板参数,这或许是一个负担,C++11则放宽了这方面的限制:
void f (vector<X>& v)
{
        struct Less {
               bool operator()( const X& a, const X& b)
              {
                      return a. v < b. v;
              }
       };
        // C++98: 错误: Less是局部类
        // C++11: 正确
        sort(v .begin(), v.end(), Less());
}
当然除了这里的局部类之外,在C++11中,我们还可以采用Lambda表达式来做同样的事情:
void f (vector<X>& v)
{
        sort(v .begin(), v.end(),
              []( const X& a, const X& b) { return a. v < b. v; });
}
C++11同时也允许模板参数使用未命名类型的值:
template<typename T > void foo (T const & t ){}
enum X { x };
enum { y };

int main ()
{
        foo( x);     // C++98: ok; C++11: ok
        //(译注:y是未命名类型的值,C++98无法从这样的值中推断出函数模板参数)
        foo( y);     // C++98: error; C++11: ok
        enum Z { z };
        foo( z);     // C++98: error; C++11: ok
        //(译注:C++98不支持从局部类型值推导模板参数
}
预防窄转换(类型截断)
在C++11中,使用{}进行初始化不会发生这种窄转换(译注:也就是使用{}对变量进行初始化时,不会进行隐式的类型截断,编译器会产生一个编译错误,防止隐式的类型截断的发生。)如果一个值可以无损地用目标类型来存放,那么就不存在窄转换。
int x0 { 7.3 };    // 编译错误: 窄转换
int x1 = { 7.3 };    // 编译错误:窄转换
double d = 7;
int x2 { d };    // 编译错误:窄转换(double类型转化为int类型)
char x3 { 7 };  // OK:虽然7是一个int类型,但这不是窄转换
vector vi = { 1, 2.3, 4, 5.6 };  //错误:double至int到窄转换
请注意,double至int类型的转换通常都会被认为是窄转换,即使从7.0转换至7。
重写 (override) 的控制
override:
在 C++11中,我们可以使用新的override关键字,来让程序员可以更加明显地表明他对于重写的设计意图,增加代码的可读性。
struct B {
        virtual void f();
        virtual void g() const;
        virtual void h( char);
        void k();      // non-virtual
};
struct D : B {
        void f() override;     // OK: 重写 B::f()
        void g() override;     // error: 不同的函数声明,不能重写
        virtual void h( char);  // 重写 B::h( char ); 可能会有警告
        void k() override;     // error: B::k() 不是虚函数
};
需要注意的是,override仅在函数名称后才用于表达“重写”的语义,在其他地方,它并不作为关键字,仍然可以作为变量名使用
final:
在某些场景下,我们需要禁止继承类对基类的某些虚函数进行重写。在C++11中,可以通过使用“final”关键字来实现。
struct B {
        virtual void f() const final;   // 禁止重写
        virtual void g();
};

struct D : B {
        void f() const; // 编译错误: D::f()不能对声明为final的B::f()进行重写
        void g();   // OK
};
原生字符串标识
反斜杠’\‘其实是一个“转义(escape)”操作符(用于特殊字符),这相当令人讨厌。R"(...)"记法相比于"…”会有一点点的冗长,但为了不必使用烦琐的“转义(escape)”符号,“多一点”是必要的。
// 这个字符串是 “quoted string”
string str1 = R "("quoted string")" 

// 字符串为:"quoted string containing the usual terminator (")"
string str2 = R "***("quoted string containing the usual terminator (")")***"
右值引用
”&&”表示“右值引用”。右值引用可以绑定到右值(但不能绑定到左值):
int a ;
int f ()
{
        return 1;
}

int& r1 = a ;      // 将r1绑定到a(一个左值)
int& r2 = f ();    // 错误:f()的返回值是右值,无法绑定
int&& rr1 = f();  // OK:将rr1绑定到临时变量。这样效率高,比int rr1 = f() 少了一次拷贝构造的时间。
int&& rr2 = a;    // 错误:不能将右值引用rr2绑定到左值a
移动赋值操作背后的思想是,“赋值”不一定要通过“拷贝”来做,还可以通过把源对象简单地“偷换”给目标对象来实现。例如对于表达式s1=s2,我们可以不从s2逐字拷贝,而是直接让s1“侵占”s2内部的数据存储(译注:比如char* p),并以某种方式“删除”s1中原有的数据存储。我们如何知道源对象能否“移动”呢?我们可以这样告诉编译器:(译注:通过move()操作符)
template <class T >
void swap(T & a, T& b //“完美swap”(大多数情况下)
{
        T tmp = move(a );  // 变量a现在失效(译注:内部数据被move到tmp中了)
        a = move(b );      // 变量b现在失效(译注:内部数据被move到a中了,变量a现在“满血复活”了)
        b = move(tmp);    // 变量tmp现在失效(译注:内部数据被move到b中了,变量b现在“满血复活”了)
}
静态(编译期)断言
静态(编译期)断言由一个常量表达式及一个字符串文本构成
static_assert(expression, string); 
expression在编译期进行求值,当结果为false(即:断言失败)时,将string作为错误消息输出。例如:
static_assert(sizeof (long ) >= 8, "64 - bit code generation required for this library.");
static_assert在判断代码的编译环境方面(译注:比如判断当前编译环境是否64位)十分有用。但需要注意的是,由于static_assert在编译期进行求值,它不能对那些依赖于运行期计算的值的进行检验。
int f (int * p, int n)
{
        //错误:表达式“p == 0”不是一个常量表达式
        static_assert(p == 0, "p is not null" );
}

C++11 FAQ