C++11:通用为本,专用为末(一)

2014-11-24 07:13:38 · 作者: · 浏览: 0

1 派生类可以自动获得基类的成员变量和接口(虚函数、纯虚函数),但是基类的非虚函数则无法被再被派生类使用了,所以派生类要使用基类的构造函数也需要显示声明,当基类的构造函数有多个版本的时候就需要在派生类中透传许多基类的构造函数。在C++11中派生类要使用基类的成员函数的话可以通过using声明完成。

struct A {
    A(int i) {}
    A(double d, int i) {}
    A(float f, int i, const char* c) {}
    // ...
};

struct B : A { 
    using A::A;//
    int d {0};
};

int main() {
    B b(356);   // b.d被初始化为0
}

2 委托构造:同一类中通过其它构造函数来完成构造,但是不能同时出现委托构造和初始化列表,委托构造不能形成委托环。这里假设A利用B完成构造,成A为委托构造函数,B为目标构造函数。

class Info {
public:
    Info() { InitRest(); }//InitRest是抽象出来最通用的初始化操作,注意避免该通用版本和其它构造函数冲突,如重复初始化同一对象可能不是预期的行为
    Info(int i) : Info() { type = i; }//委托构造
    Info(char e): Info() { name = e; }//Info(char e):Info(),name=e{}这样是错误的,只能把成员初始化置于函数体中

private:
    void InitRest() { /* 其它初始化 */ }
    int  type {1};
    char name {'a'};
    // ...
};
委托构造的一个实际应用就是利用构造模板函数产生目标构造函数

class TDConstructed {
    template
  
    TDConstructed(T first, T last) : 
        l(first, last) {}
    list
   
     l; public: TDConstructed(vector
    
      & v): TDConstructed(v.begin(), v.end()) {} TDConstructed(deque
     
       & d): TDConstructed(d.begin(), d.end()) {} };
     
    
   
  
在目标构造函数中若抛出异常,委托构造函数中可以捕获异常

class DCExcept {
public:
    DCExcept(double d)
        try : DCExcept(1, d) { 
            cout << "Run the body." << endl;
            // 其它初始化
        }
        catch(...) {
            cout << "caught exception." << endl;
        }
private:
    DCExcept(int i, double d){ 
        cout << "going to throw!" << endl;
        throw 0; 
    }
    int type;
    double data;
};


3 右值语义与移动语义:在函数返回一个临时对象时若编译器不采用返回值优化RVO的话会产生许多拷贝构造,g++中编译时加上-fno-elide-constructors可以关闭RVO,一个简单的例子:

#include
  
   
using namespace std;
class test{
    public:
        test():data(new int(10)){
            cout<<"constructor"<
   
    程序输出: 
    

constructor //a的构造
copy constructor //return a时的拷贝构造
destructor //a的析构
copy constructor //test b=fun()时的拷贝构造
destructor //fun()返回的临时对象的析构
destructor //b的析构

从上面的例子中可以看出产生了很多拷贝构造,如果类中持有大块内存,那么深拷贝的开销非常大,且拷贝构造几乎是透明的,不易察觉性能开销。移动语义要解决的额问题就是在临时对象one构造的时候不分配内存,即不使用拷贝构造语义。

#include
     
      
using namespace std;
class test{
    public:
        test():data(new int(10)){
            cout<<"constructor"<
       
       

程序输出:
constructor //a的构造
fun 0x1aaa010 //fun输出data地址
move constructor //a移动构造成fun返回的临时对象
destructor //a的析构
move constructor //从fun返回的临时对象移动构造为b
destructor //fun返回的临时对象的析构
main 0x1aaa010 //main中b的data地址
destructor //b的析构

从上面的输出看出:利用移动构造函数没有减少构造的次数,但是资源data没有重新开辟空间,即被重复利用了,b和a的data地址相同,这对于持有大块内存的class来说无疑是提高性能的有效方式。移动构造接受一个"右值引用"参数&&。

左值与右值:在赋值表达式中出现在等号左边的就是左值,在等号右边的是右值。还有一种观点是:可以取地址的是左值,左值是具名的; 不能取值的、不具名的就是右值。比如前面代码中函数fun返回值就是个不具名对象无法对其取地址所以是右值。

c++11中,右值由两个概念构成:将亡值、纯右值。纯右值:用于辨识临时变量和一些不跟对象关联的值,比如非引用返回的函数返回的临时变量值,一些运算表达式如1+3产生的临时变量,不跟对象关联的字面值如'c'。将亡值:跟右值引用相关的表达式,这样的表达式通常是将要被移动的对象,比如返回右值引用T&&的函数返回值、move的返回值。

C++11中所有值都必属于左值、将亡值、纯右值之一。

右值引用就是对一个右值进行引用的类型,由于右值通常不具名,所以只能通过引用的方式找到其存在。如利用前面的代码:test&& a=fun();声明一个右值引用,其值等于fun翻书返回的临时变量的值。无论是左值引用还是右值引用必须声明即初始化,即绑定一个对象,引用仅仅是个别名。右值引用是不具名(匿名)变量的别名。在test&& a=fun()中本来fun()返回后生命就终结了,而通过右值引用,该临时变量生命又和引用a一样长了,故会少一次析构和构造。通常,右值引用不能绑定到任何左值的。如下是错误的:int a; int&& b=a; 。那么左值引用是否可以绑定到右值呢?可以使用左值常引用绑定右值,如:const int& c=a;。 常量左值引用是个万能引用,它可以绑定非常量左值、常量左值、右值。而且使用右值对其初始化的时候,常量左值引用还可以像右值引用一样将右值的生命期延长。但是常量左值引用是只读的,以后都不能修改,针对移动语义将来要修改来说显然相悖的,但是可以常量左值引用减少临时对象的开销。

const bool& a=true;//使用常量左值引用将右值true续命
const bool b=true;//右值在表达式结束后销毁了
类中添加一个以右值引用为参数的构造函数就可以实现移动语义std::move,move的作用是将一个左值转为右值引用。如前面代码中:test::test(test&& one)一样。在有了移动构造函数后可以这样:

void myFun(test&& s){
     test t=std::move(s);//通过move将左值转为右值,然后对t初始化
}
如果在class test中没有声明移动构造函数,而只声明了一个以常量左值为参数的构造函数,由于常量左值引用是万能引用类型,test t=std::move(s)会调用该常量左值引用为参数的构造函数。那么move是个安全的设计,因为在没有移动构造函数的情形下,可以调用常量左值引用的构造函数,只不过此时变成了拷贝构造。

判断引用类型的方法,头文件 中有三个模板类:is_rvalue_reference, is_lvalue_reference, is_reference可以判断引用类型。使用方式: is_rvalue_reference ::value返回1表示是右值引用。

C++11中引用类型及其可以引用的值类型:

引用类型 非常量左值 常量左值 非常量右值 常量右值
非常量左值引用 Y N N N
常量左值引用 Y Y Y Y 万能引用
非常量右值引用 N N Y N 用于移动语义、完美转发
常量右值引用 N N Y Y 暂无用途
std::move并不能移动任何东西,其唯一的功能是将一个左值强制转换为右值引用,继而可以通过右值引用使用该值,以利于移动语义。等同于:

static_cast
        
         (左值)
        
被转化的那个左值并没有因为move而影响其生命期。看下面一个关于move使用的例子:

#include 
        
         
using namespace std;
class HugeMem{
public:
    HugeMem(int size): sz(size > 0   size : 1) {
        c = new int[sz];
    }
    ~HugeMem() { delete [] c; }
    HugeMem(HugeMem && hm): sz(hm.sz), c(hm.c) {
        hm.c = nullptr;//注意置原指针为空,否则会double free
    }
    int * c;
    int sz;
};
class Moveable{
public:
    Moveable():i(new int(3)), h(1024) {}
    ~Moveable() { delete i; }
    Moveable(Moveable && m)://这里有句绕口的话:可以接受右值的右值引用本身是个左值,即m可以接受一个右值引用实参,但是m是个具名变量,所以m本身是左值
        i(m.i), h(move(m.h)) {//强制转为右值,以调用移动构造函数,因为m的左值,所以必须move为右值,然后调用移动构造函数,若没有move则调用深拷贝构造函数重新开辟内存空间增大了开销
        m.i = nullptr;// 
    }
    int* i;
    HugeMem h;
};
Moveable GetTemp() { 
    Moveable tmp = Moveable(); 
    cout << hex << "Huge Mem from " << __func__ << " @" << tmp.h.c << endl; // Huge Mem from GetTemp @0x603030
    return tmp;
}
int main() {
    Moveable a(GetTemp());//GetTemp()返回一个右值,所以这里移动构造 
    cout << hex << "Huge Mem from " << __func__<< " @" << a.h.c << endl;   // Huge Mem from main @0x603030
}
        
通常使用move转换拥有堆内存、文件句柄等资源的变量为右值,再配合移动构造函数实现移动语义,像偷了原来的资源一样。即使没有移动构造函数,也可以调用万能的常量左值引用的构造函数,从而实现安全设计。移动语义一定是要修改变量的值,即要偷变量里面的资源,所以常右值引用没什么用处,因为其不允许将来修改。
默认情况下,编译器隐式地生成移动构造函数(隐式表示不适用就生成)。如果显示定义了拷贝构造函数、拷贝赋值函数、移动赋值函数、析构函数中的一个或多个,则编译器不会默认生成移动构造函数。同样,显示声明了移动构造、移动赋值、拷贝赋值、析构中的一个或 多个,编译器也不会生成默认的拷贝构造函数。默认的移动构造函数和默认的拷贝构造函数一样,只能做一些按位拷贝的工作。要实现移动语义,需要显示定义移动构造函数。当然对一些简单的、不包含任何资源的类型来说,实现移动语义与否都无关紧要,因为对于这样的类型而言,移动就是拷贝,拷贝就是移动,实例如下:

#include
        
         
using namespace std;
class test{
    public:
        test(int i=1):data(i){
            cout<<"constructor"<
         
          程序输出: 
          

constructor
move constructor
destructor
move constructor
destructor
destructor


头文件中,可以利用辅助的模板类判断类型是否可以移动,is_move_construcitble, is_trivially_move_constructible, is_nothrow_move_constructible.使用方法:is_move_constructible<类名>::value为1的话表示可移动的,为0不可移动。

利用移动语义实现的高性能的swap函数:

template
           
            
void swap(T& a,T& b){//若T中不涉及资源的时候和普通交换一样,但是当T中设计某种资源时,利用移动语义可以将资源移动而非拷贝
     T tmp(move(a));//假设T中有指针,那么这里只是把指针值交给tmp,而不是重新开辟内存
     a=move(b);
     b=move(tmp);
}//若T不支持移动,那么也和普通swap的拷贝一样
           
移动构造函数尽量不要抛出异常,用noexcept修饰。也可以用一个模板类std::move_if_noexcept替代std::move函数,该模板类在移动构造没有noexcept时采用拷贝语义,移动构造函数有noexcept时采用移动语义,从而保证安全。例子如下:

#include 
           
            
#include 
            
              using namespace std; struct Maythrow { Maythrow() {} Maythrow(const Maythrow&) { std::cout << "Maythorow copy constructor." << endl; } Maythrow(Maythrow&&) { std::cout << "Maythorow move constructor." << endl; } }; struct Nothrow { Nothrow() {} Nothrow(Nothrow&&) noexcept { std::cout << "Nothorow move constructor." << endl; } Nothrow(const Nothrow&) { std::cout << "Nothorow move constructor." << endl; } }; int main() { Maythrow m; Nothrow n; Maythrow mt = move_if_noexcept(m); // Maythorow copy constructor. Nothrow nt = move_if_noexcept(n); // Nothorow move constructor. return 0; }
            
           
RVO:编译器的返回值优化,g++中添加选项-fno-elide-constructors可以关闭返回值优化。编译器将许多临时变量的构造、析构、移动都省略了。例如:

test fun(){
  test b()l
  return b;
}
test a=fun();//这里经过RVO后,a实际使用了b的地址,任何拷贝、移动都没有了


4 完美转发是指在函数模板中,完全按照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。

引用折叠:多个引用重复组合在一起导致的最后的引用结果类系,一旦定义中出现了左值引用,引用折叠总是折叠为左值引用

template
           
            
void fun(T&& t){
    myFun(static_cast
            
             (t));//根据引用折叠原则:若T为右值引用,T&& &&为T&&,则传给myFun的是右值引用; 若T为左值引用,T& &&为T&,传给myFun的是左值引用 }//这里static_cas
             
              等价于std::move 
             
            
           

但是在C++11中,用于完美转发的却不是std::move而是forward

template
           
            
void fun(T&& t){
    myFun(forward(t));//forward和move语义差不多
}

           
一个完美转发的例子:

#include 
           
            
using namespace std;
void RunCode(int && m) { cout << "rvalue ref" << endl; }
void RunCode(int & m) { cout << "lvalue ref" << endl; }
void RunCode(const int && m) { cout << "const rvalue ref" << endl; }
void RunCode(const int & m) { cout << "const lvalue ref" << endl; }
template 
            
              void PerfectForward(T &&t) { RunCode(forward
             
              (t)); } int main() { int a; int b; const int c = 1; const int d = 0; PerfectForward(a); // lvalue ref PerfectForward(move(b)); // rvalue ref PerfectForward(c); // con