c++11深入学习

委托构造函数

      在引入C++ 11之前,如果某个类有多个重载的构造函数,且这些构造函数中有一些共同的初始化逻辑,通常都需要再编写一个带参数的初始化函数,然后在这些构造函数中调用这个初始化函数。在C++ 11中,再也不用这么麻烦了。我们可以实现一个最基础的构造函数,其他构造函数都调用这个构造函数。示例代码如下:

 

 

 

 

 1 class CPerson

 2 {

 3 public:

 4  CPerson() : CPerson(0, "") { NULL; }

 5  CPerson(int nAge) : CPerson(nAge, "") { NULL; }

 6  CPerson(int nAge, const string &strName)

 7  {

 8   stringstream ss;

 9   ss << strName << "is " << nAge << "years old.";

10   m_strInfo = ss.str();

11  }

12 

13 private:

14  string m_strInfo;

15 };

 

 

统一的初始化语法

      在引入C++ 11之前,有各种不同的初始化语法。在C++ 11中,仍可以使用这些初始化语法,但也可以选择使用新引入的统一的初始化语法。统一的初始化语法用一对大括号{}表示,使用{}初始化语法还可有效地避免窄转换。示例代码如下:

 

 

 

 

1  int a{5};

2  char c{‘X‘};

3  int p[5] = {1, 2,3, 4, 5};

4  vector<int> vctTemp{1, 2, 3};

5  CPerson person{10, "Mike"};

6   int b = 5.3;                     // b赋值成5,发生了窄转换

7   int d{5.3};                      // 会提示编译错误,避免了窄转换

 

 

语法甜点4nullptr

      nullptrC++ 11中新加的一个关键字,用于标识空指针。引入nullptr后,可以解决某些函数重载时的二义性问题。示例代码如下:

 

 1 void F(int a)

 2 {

 3  cout << a << endl;

 4 }

 5 

 6 void F(char *p)

 7 {

 8  assert(p != NULL);

 9 

10  cout << p << endl;

11 }

12 

13 int main(int argc, _TCHAR* argv[])

14 {

15  int *p = nullptr;

16  int *q = NULL;

17  bool bEqual = (p == q);  // 两个指针值是相等的,bEqualtrue

18  int a = nullptr;   // 编译失败,nullptr不是转换为int

19 

20  F(0);          // C++ 98中编译失败,有二义性;在C++ 11中调用F(int)

21  F(nullptr);    // 调用F(char *)

22 

23  getchar();

24  return 0;

25 }

 

 

成员变量初始化

      与JavaC#中的用法一样,可以对成员变量进行就地初始化。示例代码如下:

 

 

1 class CPerson

2 {

3 private:

4  int m_nAge = 10;

5  string m_strName = "Mike";

6 };

 

 

默认或禁用函数

      当我们定义了自己的带参数的构造函数时,编译器将不再生成默认的构造函数,如果此时想使用默认的构造函数,则必须显式地声明并定义不带参数的构造函数。在C++ 11中,我们可以使用default关键字来表明我们希望使用默认的构造函数。类似的,当我们不想外部使用编译器自动生成的构造函数或赋值函数时,我们一般需要将其声明成protectedprivate的。在C++ 11中,我们可以使用delete关键字来表明我们不希望编译器生成默认的构造函数或赋值函数。示例代码如下:

 

 

1 class CPerson

2 {

3 public:

4  CPerson() = default;

5  CPerson(const CPerson &person) = delete;

6 };

 

 

static_assert

      静态断言static_assert由一个常量表达式和一个字符串构成。在编译期间,将计算常量表达式的值,如果为false,字符串将作为错误信息输出。示例代码如下:

 

 

1  char a = 10;

2  static_assert(sizeof(a)==4, "a is not an integer.");

 

 

模板右边双括号

      在C++ 98中,vector<vector<int>> vctTemp是一个非法的表达式,编译器会认为右边的>>是一个移位操作符,因此必须修改为vector<vector<int> > vctTemp,即在右边的两个>中间添加一个空格。在C++ 11中,这将不再是一个问题,编译器将能够识别出右边的双括号是两个模板参数列表的结

-

 

继承的构造函数

      当一个派生类的某个函数隐藏了基类中的某个同名函数时,如果我们想在派生类中导出基类中的这个同名函数,可以通过using Base::Func的方式将基类中的这个同名函数引入到派生类的作用域内。当该方法只对普通成员函数有效,不能用于构造函数。在C++ 11中,如果派生类认为基类的构造函数已经足够,则也可以使用using Base::Base的方式将基类的构造函数引入到派生类的作用域内。但需要注意的是,此时派生类中的成员变量并没有进行初始化,所以应当对这些成员变量进行就地初始化。示例代码如下:

 

 

 

 

 1 class CBase

 2 {

 3 };

 4 

 5 class CDerived : public CBase

 6 {

 7 public:

 8  using CBase::CBase;

 9  CDerived(int nData) : m_nData(nData) { NULL; }

10 

11 private:

12  int m_nData = 10;

13 };

 

 

初始化列表

      在引入C++ 11之前,只有数组能使用初始化列表。在C++ 11中,vectorlist等各种容器以及string都可以使用初始化列表了。初始化列表对应的类为initializer_listvectorlist等各种容器以及string之所以可以使用初始化列表,是因为它们重载了参数类型为initializer_list的构造函数(称为初始化列表构造函数)和赋值函数(称为初始化列表赋值函数)。下面是一些使用初始化列表的例子。

 

 

 

 

 1 void Print(const initializer_list<int> &ilData)

 2 {

 3  for (auto a : ilData)

 4  {

 5   cout << a << endl;

 6  }

 7 }

 8 

 9 int main(int argc, _TCHAR* argv[])

10 {

11  vector<int> vctNum = {1, 2, 3, 4, 5};

12  map<string, string> mapID2Name = {{"92001", "Jack"}, {"92002", "Mike"}};

13  string strText{"hello world"};

14  Print({});

15  Print({1, 2});

16  Print({1, 2, 3, 4, 5});

17 

18  getchar();

19  return 0;

20 }

 

 

 

非成员的beginend

    在C++ 03中,标准容器都提供了beginend成员函数,但对于普通数组,则只能使用不同的写法。比如:

 

 

1 vector<int> v; 

2 int a[100]; 

3 sort(v.begin(), v.end()); 

4 sort(a, a+sizeof(a)/sizeof(a[0]));

    为了统一语法,C++ 11提供了非成员的beginend函数。用法如下:

 

 

1 sort(begin(v), end(v)); 

2 sort(begin(a), end(a));


 

显式虚函数重载    

 

    在引入C++ 11之前,基类和派生类中的虚函数很容易产生错误使用的情况。比如:

 

    a、基类添加了一个虚函数,但该虚函数与派生类中的某个已有普通函数相同。

 

    b、派生类添加了一个普通函数,但该函数与基类中的某个已有虚函数相同。

 

    为了避免这些情况,在C++ 11中可以使用override来显式地表明需要进行虚函数重载。比如:

 

 

 1 class Base 

 2 {

 3     virtual void some_func(float);

 4 };

 5 

 6 class Derived : public Base

 7  {

 8     virtual void some_func(int) override;        // 将产生编译错误

 9    virtual void some_func(float) override;    // 正确

10 };

 

 

 

C++ 11中还引入了final指示符,用于防止类或接口被继承。比如:

 

 

 

 1 class  Base1 final { };

 2 class Derived1 : public Base1 { };            // 将产生编译错误

 3 class Base2

 4 {

 5     virtual void f() final;

 6 };

 7 class Derived2 : public Base2

 8 {

 9     void f();                                             // 将产生编译错误

10 };

 

 

C++ 03中,可以使用typedef给模板类指定一个新的类型名称,但却不能给类模板指定别名。比如:

 

1 template< typename first, typename second, int third>

2 class SomeType;   template< typename second>

3 typedef SomeType<OtherType, second, 5> TypedefName;  // C++ 03中是不合法的

 

 

 

无限制的union

 

    在C++ 03中,并非任意的数据类型都能做为union的成员。比方说,带有non-trivial构造函数的类型就不能是 union 的成员。在C++ 11中,移除了所有对union的使用限制,除了其成员仍然不能是引用类型这种情况。 

 

 

 

 

 1 struct point

 2 {

 3      point() {}

 4      point(int x, int y): m_x(x), m_y(y) {}

 5      int m_x, m_y;

 6 };

 7 union

 8 {

 9      int z;

10      double w;

11      point p;                     // C++ 03中不合法;在C++ 11中合法

12 };

 

C++ 11中,允许sizeof运算符作用在类型的数据成员上,而无须明确的对象。在C++ 03中,这是不允许的,会导致编译错误。比如:

1 struct SomeType { OtherType member; };

2 sizeof(SomeType::member);        // C++ 03中不合法;在C++ 11中合法

 

新的算法

 

    C++ 11中新增了一些比较实用的算法。比如all_ofany_ofnone_ofcopy_ncopy_ifiota等。参考代码如下:

 

 

1 int a[5] = {-2, -1, 0, 1, 2};

2 auto funIsPositive = [](int v){return v>0;};

3 bool bRet = all_of(a, a+5, funIsPositive);             // false

4 bRet = any_of(a, a+5, funIsPositive);                  // true

5 bRet = none_of(a, a+5, funIsPositive);                // false

6 int b[5] = {0};

7 copy_n(a, 5, b);                                                // a开始的5个元素拷贝到b

8 copy_if(a, a+5, b, funIsPositive);                        // 1, 2两个数拷贝到b

9 iota(a, a+5, 10);                                               // a中的每个元素加10

 

 

泛化的常数表达式

 

    C++ 03中本来就已经具有常数表示式的概念,比如:3+56*7等。常数表示式对编译器来说是优化的机会,编译器常在编译期运行它们并且将值存入程序中。同样地,在许多场合下,C++规范要求使用常数表示式。比如数组大小、枚举值等。

 

    然而,常数表示式总是在遇到了函数调用时就终结。比如:

1 int GetFive() { return 5; }

2 int some_value[GetFive() + 5];         // 不合法

 

 

    C++ 11引进关键字constexpr允许用户保证函数是编译期常数。比如:

1 constexpr int GetFive() { return 5; }

2 int some_value[GetFive() + 5];

 

 

 

 

 

C++ 11中引入的一个非常重要也是比较难于理解的新特性就是完美转发(Perfect Forwarding)。完美转发中有两个关键词:“转发”和“完美”。

      我们先来看第一个关键词“转发”,那么在C++中,“转发”表示什么含义呢?转发通常用在模板编程中,假设有一个函数F(a1, a2, ..., an),如果存在另一个函数G(a1, a2, ..., an),调用G相当于调用了F,则我们说函数Ga1, a2, ..., an等参数正确地转发给了函数F。再来看第二个关键词“完美”,“完美”转发是相对于其他转发方案而言的。在目前已提出的7种转发方案中,只有完美转发能够正确地实现转发的语义,其他方案都存在这样或那样的问题。下面一一进行介绍。

      转发方案一:使用非常量左值引用。考虑下面的代码。

 

 

 

 

 1 void F(int a)

 2 {

 3  cout << a << endl;

 4 }

 5 

 6 template<class A>

 7 void G(A &a)

 8 {

 9  F(a); 

10 }

 

 

 

      使用非常量左值引用时,我们可以调用F(10),但无法调用G(10),即我们无法接收非常量右值的参数。

 

 

 

      转发方案二:使用常量左值引用。考虑下面的代码。

 

 

 

 

 1 void F(int &a)

 2 {

 3  cout << a << endl;

 4 }

 5 

 6 template<class A>

 7 void G(const A &a)

 8 {

 9  F(a); 

10 }

 

 

 

      使用常量左值引用时,函数G可以接收任意类型的值作为参数,包括非常量左值、常量左值、非常量右值和常量右值。但当F的参数类型为非常量左值引用时,我们无法将一个常量左值引用转发给一个非常量左值引用。

 

 

 

      转发方案三:使用非常量左值引用 常量左值引用。考虑下面的代码。

 

 

 

 

 1 template<class A>

 2 void G(A &a)

 3 {

 4  F(a); 

 5 }

 6 

 7 template<class A>

 8 void G(const A &a)

 9 {

10  F(a); 

11 }

 

 

 

      综合前面两种方案的分析结果,可以得出这种方案相当于对函数G进行了重载,此时可以接收任意类型的值作为参数,也可以顺利地实现转发。但由于使用了常量和非常量两种形式的重载,当参数的个数N较大时,需要重载的函数会呈指数级增长(2N次方),因此这种方案实际上是不可取的。

 

 

 

      转发方案四:使用常量左值引用 + const_cast

 

 

1 template<class A>

2 void G(const A &a)

3 {

4  F(const_cast<A &>(a)); 

5 }

 

      这种方案克服了方案二的缺点,现在可以将常量左值引用转发给非常量左值引用了。但这又带来了新的问题,假如F的参数是一个非常量左值引用,则调用G后,我们可以通过F来修改传入的常量左值和常量右值了,而这是非常危险的。

 

 

 

      转发方案五:非常量左值引用 修改的参数推导规则。

      这种方案与方案一类似,但需要修改现有的参数推导规则,即传递一个非常量右值给模板类型时,将它推导成常量右值,这样就解决了方案一中无法接收非常量右值的参数的问题。但由于修改了现有的参数推导规则,因此会导致已有代码的语义发生改变。考虑下面的代码。

 

 

 

 

 1 template<class A>

 2 void F(A &a)

 3 {

 4  cout << "void F(A& a)" << endl;

 5 }

 6 

 7 void F(const long &a)

 8 {

 9  cout << "void F(const long &a)" << endl;

10 }

 

 

 

      在未修改参数推导规则前,调用F(10)会选择第二个重载函数,但修改后,却会调用第一个重载函数,这就给C++带来了兼容性的问题。

 

 

 

      转发方案六:右值引用。考虑下面的代码。

 

 

1 template<class A>

2 void G(A &&a)

3 {

4  F(a); 

5 }

 

      在这种方案中,G将无法接收左值,因为不能将一个左值传递给一个右值引用。另外,当传递非常量右值时也会存在问题,因为此时a本身是一个左值,这样当F的参数是一个非常量左值引用时,我们就可以来修改传入的非常量右值了。

 

 

 

      转发方案七:右值引用 修改的参数推导规则。

      要理解修改后的参数推导规则,我们先来看一下引用叠加规则:

           1T& + & = T&

           2T& + && = T&

           3T&& + & = T&

           4TT&& + && = T&&

      修改后的针对右值引用的参数推导规则为:若函数模板的模板参数为A,模板函数的形参为A&&,则可分为两种情况讨论:

           1、若实参为T&,则模板参数A应被推导为引用类型T&。(由引用叠加规则第2T& + && = T&A&&=T&,可得出A=T&

           2、若实参为T&&,则模板参数A应被推导为非引用类型T。(由引用叠加规则第4TT&& + && = T&&A&&=T&&,可得出A=TT&&,强制规定A=T

      应用了新的参数推导规则后,考虑下面的代码。

 

 

1 template<class A>

2 void G(A &&a)

3 {

4  F(static_cast<A &&>(a)); 

5 }

 

      当传给G一个左值(类型为T)时,由于模板是一个引用类型,因此它被隐式装换为左值引用类型T&,根据推导规则1,模板参数A被推导为T&。这样,在G内部调用F(static_cast<A &&>(a))时,static_cast<A &&>(a)等同于static_cast<T& &&>(a),根据引用叠加规则第2点,即为static_cast<T&>(a),这样转发给F的还是一个左值。

      当传给G一个右值(类型为T)时,由于模板是一个引用类型,因此它被隐式装换为右值引用类型T&&,根据推导规则2,模板参数A被推导为T。这样,在G内部调用F(static_cast<A &&>(a))时,static_cast<A &&>(a)等同于static_cast<T&&>(a),这样转发给F的还是一个右值(不具名右值引用是右值)。

      可见,使用该方案后,左值和右值都能正确地进行转发,并且不会带来其他问题。另外,C++ 11为了方便转发的实现,提供了一个函数模板forward,用于参数的完美转发。使用forward后的代码可简化为:

 

 

1 template<class A>

2 void G(A &&a)

3 {

4  F(forward<A>(a)); 

5 }

 

      为了便于进行各种转发方案的比较,下面以表格的形式列出了各自的特性。


转发方案非常量左值常量左值非常量右值常量右值修改语言已知问题

 

一、非常量左值引用非常量左值常量左值无法转发常量左值否无法接收非常量右值的参数

二、常量左值引用常量左值常量左值常量左值常量左值否无法将常量左值引用转发给非常量左值引用

三、非常量左值引用 常量左值引用非常量左值常量左值常量左值常量左值否重载函数过多,实际编码不可行

四、常量左值引用 + const_cast非常量左值非常量左值非常量左值非常量左值否可修改常量左值和常量右值,不安全

五、非常量左值引用 修改的参数推导规则非常量左值常量左值常量左值常量左值是会导致兼容性问题,且不支持移动语义

六、右值引用无法转发无法转发非常量左值常量左值是可修改非常量右值,不安全

七、右值引用 修改的参数推导规则非常量左值常量左值非常量右值常量右值是暂无,故简称为完美转发

 


wode 下载 :

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。