Tekkaman

导航

 

深入理解C++11【2】

1、继承构造函数。

  当基类拥有多个构造函数的时候,子类不得不一一实现。

  C++98 可以使用 using 来使用基类的成员函数。

#include < iostream> using namespace std; 
struct Base { 
    void f( double i){ 
        cout << "Base:" << i << endl; 
    } 
}; 
struct Derived : Base { 
    using Base:: f; 
    void f( int i) { 
        cout << "Derived:" << i << endl; 
    } 
}; 

int main() { 
    Base b; 
    b. f( 4. 5); // Base: 4. 5 
    
    Derived d; 
    d. f( 4. 5); // Base: 4. 5 
} // 编译 选项: g++ 3- 1- 3. cpp

  C++11中,这个功能由成员函数扩展到了构造函数上。

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

struct B : A { 
    using A:: A; // 继承 构造 函数 
    // ... 
    virtual void ExtraInterface(){} 
};

  这 意味着 如果 一个 继承 构造 函数 不被 相关 代码 使用, 编译器 不 会为 其 产生 真正 的 函数 代码。

  对于 继承 构造 函数 来讲, 参数 的 默认值不会被继承 的。 事实上, 默认值 会 导致 基 类 产生 多个 构造 函数 的 版本, 这些 函数 版本 都会 被 派生 类 继承。

  如果 一旦 使用 了 继承 构造 函数, 编译器 就不 会 再为 派生 类 生成 默认 构造 函数 了,

struct A { A (int){} }; 
struct B : A{ using A:: A; }; 
B b; // B 没有 默认 构造 函数

  截至2013年,还没有编译器实现了继承构造函数。

 

2、委派构造函数。

  C++98 中编译器 不允许 在 构造 函数 中 调用 构造 函数, 即使 参数 看起来 并不 相同。以下代码会产生编译错误。

Info() { InitRest(); } 
Info( int i) { this-> Info(); type = i; }
Info( char e) { this-> Info(); name = e; }

  所以C++98的hacker喜欢使用 placement new 来实现调用其它构造函数。

Info() { InitRest(); } 
Info( int i) { new (this) Info(); type = i; } 
Info( char e) { new (this) Info(); name = e; }

  C++11 中可以使用委派构造函数来简化代码。

class Info { 
    public: 
        Info() { InitRest(); } 
        Info( int i) : Info() { type = i; } 
        Info( char e): Info() { name = e; } 
    private: 
        void InitRest() { /* 其他 初始化 */ } 
        int type {1}; 
        char name {'a'}; // ... 
}; // 编译 选项: g++ -c -std= c++ 11 3- 2- 3. cpp

  调用“ 基准 版本” 的 构造 函数 为 委派 构造 函数( delegating constructor), 而被 调用 的“ 基准 版本” 则为 目标 构造 函数( target constructor)。

  构造 函数 不能 同时“ 委派” 和 使用 初始化 列表, 所以 如果 委派 构造 函数 要给 变量 赋 初值, 初始化 代码 必须 放在 函数 体中。如下:

struct Rule1 { 
    int i; 
    Rule1( int a): i( a) {} 
    Rule1(): Rule1( 40), i( 1) {} // 无法 通过 编译 
};

   目标 构造 函数 的 执行 总是 先于 委派 构造 函数 而 造成 的。

   委派 构造 的 一个 很 实际 的 应用 就是 使用 构造模板函数 产生 目标 构造 函数,

class TDConstructed 
{
        template< class T> 
        TDConstructed( T first, T last) : l( first, last) {} 
        list< int> l; 
    public: 
        TDConstructed( vector< short> & v): TDConstructed( v. begin(), v. end()) {} 
        TDConstructed( deque< int> & d): TDConstructed( d. begin(), d. end()) {} 
}; // 编译 选项: g++ -c -std= c++ 11 3- 2- 6. cpp

   委托构造函数的异常捕获稍稍有些另类,try-catch 是在函数外:

class DCExcept { 
    public: 
        DCExcept( double d) try : DCExcept( 1, d) { 
            cout << "Run the body." << endl; // 其他 初始化 
        } 
        catch(...) { 
            cout << "caught exception." << endl; 
        }

 

3、移动语义

  C++98 中的经典问题,会产生多达分别3次的construct、destruct:

HasPtrMem GetTemp() { 
    return HasPtrMem(); 
} 
int main() { 
    HasPtrMem a = GetTemp(); 
} 
// 编译 选项: g++ 3- 3- 3. cpp -fno- elide- constructors

  输出如下:

Construct: 1 
Copy construct: 1 
Destruct: 1 
Copy construct: 2 
Destruct: 2 
Destruct: 3

  

   移动构造函数与 Copy构造函数:

HasPtrMem( const HasPtrMem & h): d( new int(* h. d)) 
{ 
    cout << "Copy construct: " << ++ n_ cptr << endl; 
} 

HasPtrMem( HasPtrMem && h): d( h. d) 
{ 
    // 移动 构造 函数 
    h. d = nullptr; // 将临 时值 的 指针 成员 置 空 
    cout << "Move construct: " << ++ n_ mvtr << endl; 
}
Construct: 1 Resource from GetTemp: 0x603010 
Move construct: 1 
Destruct: 1 
Move construct: 2 
Destruct: 2 
Resource from main: 0x603010 
Destruct: 3

 

4、左值、右值、右值引用

  1)等号左边的是“左值”。等号右边的是“右值”。

  2)可以取地址的、有名字是左值。不能取地址、没有名字的就是右值。

 

  C++11程序中,所有值必属于左值、将亡值、右值。

  右值引用是不能绑定到任何左值的,如下代码就无法通过编译:

int c; 
int && d = c;

 

  常量左值引用可以接受右值,同时也像右值引用一样将右值生命周期延长。如下第一行:

const bool & judgement = true;
const bool judgement = true;

  C++98中也常可以使用常量左值引用来减少临时对象的开销。

#include < iostream> 
using namespace std; 
struct Copyable { 
    Copyable() {} 
    Copyable( const Copyable &o) { 
        cout << "Copied" << endl; 
    } 
}; 

Copyable ReturnRvalue() { return Copyable(); } 
void AcceptVal( Copyable) {} 
void AcceptRef( const Copyable & ) {}


int main() 
{ 
    cout << "Pass by value: " << endl; 
    AcceptVal( ReturnRvalue()); // 临 时值 被 拷贝 传入 
    
    cout << "Pass by reference: " << endl; 
    AcceptRef( ReturnRvalue()); // 临 时值 被 作为 引用 传递 
} // 编译 选项: g++ 3- 3- 5. cpp -fno- elide- constructors
Pass by value: 
Copied 
Copied 
Pass by reference: 
Copied

   std::move 的作用是强制一个左值成为右值。

  因为 const T& 是万能引用,可以指向右值。所以当类实例无 MoveConstructor时,将会调用 CopyConstructor来替代。

  但 const T&& 常量右值引用没有卵用,一来MoveConstructor需要引用右值;二来如果右值不能改,使用 const T& 就可以了。所以 const T&&没有卵用。

  

  为了知道一个类型是否是引用类型,可以使用 <type_traits>头文件中的3个模板类:

  1)is_rvalue_reference

  2)is_lvalue_reference

  3)is_reference

 

5、std::move

  std::move并不能移动任何东西,它唯一的功能是将一个左值强制转化为右值引用。std::move 基本赞同于一个类型转换:

static_ cast< T&&>( lvalue);

  使用 std::move 时,我们希望转换为右值引用的这个值是一个生命期即将结束的对象。一个正确的 std::move 的使用场景。

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; }
        int * c; 
        int sz; 
}; 

class Moveable{ 
    public: 
        Moveable(): i( new int( 3)), h( 1024) {} 
        ~ Moveable() { delete i; } 
        
        Moveable( Moveable && m): i( m. i), h( move( m. h)) { // 强制 转为 右 值, 以 调用 移动 构造 函数 
            m. i = nullptr; 
        } 
        int* i; 
        HugeMem h; 
};

 

6、移动语义的其它问题。

  下述代码会使得 的 临时 变量 常 量化, 成为 一个 常量 右 值, 那么 临时 变量 的 引用 也就 无法 修改, 从而 导致 无法 实现 移动 语义。

Moveable( const Moveable &&)
const Moveable ReturnVal();

  编译器 会为 程序员 隐式 地 生成 一个( 隐式 表示 如果 不被 使用 则 不 生成) 移动 构造 函数。 不过 如果 程序员 声明 了 自定义 的 拷贝 构造 函数、 拷贝 赋值 函数、 移动 赋值 函数、 析 构 函数 中的 一个 或者 多个, 编译器 都不 会 再为 程序员 生成 默认 版本。

  声明 了 移动 构造 函数、 移动 赋值 函数、 拷贝 赋值 函数 和 析 构 函数 中的 一个 或者 多个, 编译器 也不 会 再为 程序员 生成 默认 的 拷贝 构造 函数。 所以 在 C++ 11 中, 拷贝 构造/ 赋值 和 移动 构造/ 赋值 函数 必须 同时 提供, 或者 同时 不 提供, 程序员 才能 保证 类同 时 具有 拷贝 和 移动 语义。

   只有 移动 语义 的 类型 则 非常 有趣, 因为 只有 移动 语义 表明 该 类型 的 变量 所 拥有 的 资源 只能 被 移动, 而 不能 被 拷贝。 那么 这样 的 资源 必须 是 唯一 的。 因此, 只有 移动 语义 构造 的 类型 往往 都是“ 资源 型” 的 类型, 比如说 智能 指针, 文件 流 等,

   有了移动语义,可以实现高性能的 swap 函数。

template < class T> 
void swap( T& a, T& b) 
{ 
    T tmp( move( a)); 
    a = move( b); 
    b = move( tmp); 
}

  程序员 应该 尽量 编写 不 抛出 异常 的 移动 构造 函数, 通过 为 其 添加 一个 noexcept 关键字, 可以 保证 移动 构造 函数 中 抛出 来的 异常 会 直接 调用 terminate 程序 终止 运行, 而 不是 造成 指针 悬挂 的 状态。

  一个 std:: move_ if_ noexcept 的 模板 函数 替代 move 函数。 该 函数 在 类 的 移动 构造 函数 没有 noexcept 关键字 修饰 时 返回 一个 左 值 引用 从而 使 变量 可以 使用 拷贝 语义, 而在 类 的 移动 构造 函数 有 noexcept 关键字 时, 返回 一个 右 值 引用, 从而 使 变量 可以 使用 移动 语义。如下:

  

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; 
} // 编译 选项: g++ -std= c++ 11 3- 3- 8. cpp

 

  在 本节 中 大量 的 代码 都 使用 了- fno- elide- constructors 选项 在 g++/ clang++ 中 关闭 这个 优化。如果关闭,则下述代码只会有一次 constructor、destructor:

A ReturnRvalue() { A a(); return a; } 
A b = ReturnRvalue();

 

7、完美转发

  完美 转发( perfect forwarding), 是指 在 函数 模板 中, 完全 依照 模板 的 参数 的 类型, 将 参数 传递 给 函数 模板 中 调用 的 另外 一个 函数。下面的例子中,如果传入的是一个右值,t被传给内部函数时成了一个左值,不是完美转发。

template < typename T> 
void IamForwording( T t) 
{ 
    IrunCodeActually( t); 
}

  我们希望传入左值,传出也是左值;传入右值,传出也是右值。

  C++11加入了引用折叠新规则。在C++98中,以下代码会导致编译错误。

typedef const int T; 
typedef T& TR; 
TR& v = 1;     // 该 声明 在 C++ 98 中会 导致 编译 错误

  

  

  模板类型 的 推导 规则 就比 较 简单, 当 转发 函数 的 实 参 是 类型 X 的 一个 左 值 引用, 那么 模板 参数 被 推导 为 X& 类型, 而 转发 函数 的 实 参 是 类型 X 的 一个 右 值 引 用的 话, 那么 模板 的 参数 被 推导 为 X&& 类型。

template < typename T> 
void IamForwording( T && t) { 
    IrunCodeActually( static_ cast< T &&>(t)); 
}

  上面就是完美转发。

  

8、显式类型转换

  只有一个参数的构造函数也定义了一个隐式转换,将该构造函数对应数据类型的数据转换为该类对象。

class String
{
public:
    String ( const char* p ); // 用C风格的字符串p作为初始化值
    //
}
 
String s1 = “hello”; //OK 隐式转换,等价于String s1 = String(”hello”)

  在 C++ 11 中, 标准 将 explicit 的 使用范围 扩展到 了 自定义 的 类型 转换 操作 符 上。

class ConvertTo {}; 

class Convertable 
{ 
    public: 
        explicit operator ConvertTo () const { 
            return ConvertTo(); 
        } 
}; 
        
void Func( ConvertTo ct) {} 
void test() 
{ 
    Convertable c; 
    ConvertTo ct( c); // 直接 初始化, 通过 
    ConvertTo ct2 = c; // 拷贝 构造 初始化, 编译 失败 
    ConvertTo ct3 = static_ cast< ConvertTo>( c); // 强制 转化, 通过 
    Func( c); // 拷贝 构造 初始化, 编译 失败 
} 
// 编译 选项: g++ -std= c++ 11 3- 4- 3. cpp

 

9、初始化列表

  C++98 中只允许对数组进行列表初始化,C++11扩展到了所有元素。

#include < vector> 
#include < map> 
using namespace std; 
int a[] = {1, 3, 5}; // C++ 98 通过, C++ 11 通过 
int b[] {2, 4, 6}; // C++ 98 失败, C++ 11 通过 
vector< int> c{ 1, 3, 5}; // C++ 98 失败, C++ 11 通过 
map< int, float> d = {{1, 1. 0f}, {2, 2. 0f} , {5, 3. 2f}}; // C++ 98 失败, C++ 11 通过 
// 编译 选项: g++ -c -std= c++ 11 3- 5- 1. cpp

  如上,列表 初始化 可以 在“{}” 花 括号 之前 使用 等号, 其 效果 与 不带 使用 等号 的 初始化 相同。

   {}和()一样,也可以用于 new操作符中。

int * i = new int( 1);
double * d = new double{ 1. 2f};

   自定义类型如需要初始化列表功能,需要提供一个构造函数,此函数使用唯一一个参数,initialize_list。

enum Gender {boy, girl}; 

class People { 
    public: 
        People( initializer_ list< pair< string, Gender>> l) 
        { 
            // initializer_ list 的 构造 函数 
            auto i = l. begin(); 
            for (;i != l. end(); ++ i) 
                data. push_ back(* i); 
        } 
        
    private: 
        vector< pair< string, Gender>> data; 
}; 

People ship2012 = {{"Garfield", boy}, {"HelloKitty", girl}}; // 编译 选项: g++ -c -std= c++ 11 3- 5- 2. cpp

   函数也可以使用初始化列表:

void Fun( initializer_ list< int> iv){ } 

int main() { 
    Fun({ 1, 2}); 
    Fun({}); // 空 列表 
} // 编译 选项: g++ -std= c++ 11 3- 5- 3. cpp

  当初始化列表 + operator[] + operator= 时,会产生强大的语法效果:

using namespace std; 

class Mydata 
{ 
    public: 
        Mydata & operator [] (initializer_ list< int> l) 
        { 
            for (auto i = l. begin(); i != l. end(); ++ i) 
                idx. push_ back(* i); 
            return *this; 
        } 
        
        Mydata & operator = (int v) 
        { 
            if (idx. empty() != true) 
            { 
                for (auto i = idx. begin(); i != idx. end(); ++ i) 
                { 
                    d. resize((* i > d. size()) ? *i : d. size()); 
                    d[* i - 1] = v; 
                } 
                
                idx. clear(); 
            } 
            return *this; 
        } 
        
        void Print() 
        { 
            for (auto i = d. begin(); i != d. end(); ++ i) 
                cout << *i << " "; cout << endl; 
        } 
    
    private: 
        vector< int> idx; // 辅助 数组, 用于 记录 index 
        vector< int> d; 
}; 

int main() 
{ 
    Mydata d; 
    d[{ 2, 3, 5}] = 7; 
    d[{ 1, 4, 5, 8}] = 4; 
    d. Print(); // 4 7 7 4 4 0 0 4 
} 
// 编译 选项: g++ -std= c++ 11 3- 5- 4. cpp
View Code

   

  初始化 列表 还可以 用于 函数 返回 的 情况。 返回 一个 初始化 列表, 通常 会 导致 构造 一个 临时 变量,\

vector< int> Func() { return {1, 3}; }

 

10、类型收窄

  在 C++ 11 中, 使用 初始化 列表 进行 初始化 的 数据 编译器 是 会 检查 其是 否 发生 类型 收 窄 的。

const int x = 1024; 
const int y = 10; 
char a = x; // 收 窄, 但可以 通过 编译 
char* b = new char( 1024); // 收 窄, 但可以 通过 编译 
char c = {x}; // 收 窄, 无法 通过 编译 
char d = {y}; // 可以 通过 编译 
unsigned char e {-1}; // 收 窄, 无法 通过 编译 
float f { 7 }; // 可以 通过 编译 
int g { 2. 0f }; // 收 窄, 无法 通过 编译 
float * h = new float{ 1e48}; // 收 窄, 无法 通过 编译 
float i = 1. 2l; // 可以 通过 编译 
// 编译 选项: clang++ -std= c++ 11 3- 5- 5. cpp

   在 C++ 11 中, 列表 初始化 是 唯一 一种 可以 防止 类型 收 窄 的 初始化 方式。 这也 是 列表 初始化 区别于 其他 初始化 方式 的 地方。

 

11、

12、

13、

 

posted on 2019-01-02 21:56  Tekkaman  阅读(435)  评论(0编辑  收藏  举报