Tekkaman

导航

 

深入理解C++11【3】

1、POD类型

  Plain Old Data. Plain 表示 了POD是个普通的类型。C++11将POD划分为两个基本概念的合集:

  1)平凡的(trivial)

  2)标准布局的(standard layout)

 

  一个平凡的类或结构体应该符合以下定义:

  1)拥有平凡的默认构造函数(trivial constructor)和析构函数(trivial destructor)。

    平凡 的 默认 构造 函数 就是说 构造 函数“ 什么 都 不干”。 通常 情况下, 不 定义 类 的 构造 函数, 编译器 就会 为我 们 生成 一个 平凡 的 默认 构造 函数。 而 一旦 定义 了 构造 函数, 即使 构造 函数 不 包含 参数, 函数 体 里 也没 有 任何 的 代码, 那么 该 构造 函数 也 不再 是“ 平凡” 的。

  2)拥有平凡的拷贝构造函数(trivial copy constructor)和移动构造函数(trivial move constructor)。

  3)拥有平凡的拷贝赋值运算符(trivial assignmnt operator)和移动赋值运算符(trivial move operator)。

  4)不能包含虚函数以及虚基类。

 

  C++11中可以使用 is_trivial struct来判定一个类型是否是 trivial。

  类 模板 is_ trivial 的 成员 value 可以 用于 判断 T 的 类型 是否 是一 个 平凡 的 类型。 除了 类 和 结构 体外, is_ trivial 还可 以对 内置 的 标量 类型 数据( 比如 int、 float 都 属于 平凡 类型) 及 数组 类型( 元素 是 平凡 类型 的 数组 总是 平凡 的) 进行 判断。

template < typename T> struct std:: is_ trivial;

 

  标准布局应该符合以下定义:

  1)所有非静态成员有相同的访问权限。

  2)在类或结构体继承时,满足以下两种情况之一:

    a)派生类中有非静态成员,且只有一个仅包含静态成员的基类。

    b)基类有非静态成员,而派生类没有非静态成员。

struct B1 { static int a; }; 
struct D1 : B1 { int d; }; 
struct B2 { int a; }; 
struct D2 : B2 { static int d; }; 
struct D3 : B2, B1 { static int d; }; 
struct D4 : B2 { int d; }; 
struct D5 : B2, D1 { };

    D1、 D2 和 D3 都是 标准 布局 的, 而 D4 和 D5 则 不属于 标准 布局 的。

    非 静态 成员 只要 同时 出现 在 派生 类 和 基 类 间, 其 即 不属于 标准 布局 的。 而 多重 继承 也会 导致 类型 布局 的 一些 变化, 所以 一旦 非 静态 成员 出现 在 多个 基

  3)类中第一个非静态成员的类型与其基类不同。

struct A : B{ B b; }; // 不是标准布局
struct C : B{int a; B b;} // 是标准布局

  4)没有虚函数和虚基类。

  5)所有 非 静态 数据 成员 均 符合 标准 布局 类型, 其 基 类 也 符合 标准 布局。 这是 一个 递归 的 定义, 没有 什么 好 特别 解释 的。

 

  C++11中可以使用struct模板 is_standard_layout 来判断是否是标准布局类型。

template < typename T> struct std:: is_ standard_ layout;

 

  要判定一个类型是否是 POD,可以使用 is_pod模板:

template < typename T> struct std:: is_ pod;

 

  使用 POD 有 什么 好处 呢? 我们 看 得到 的 大概 有 如下 3 点:

  1)字节赋值。memset、memcpy。

  2)与C内存布局兼容。

  3)保证了静态初始化的安全有效。

 

2、非受限联合体

  C++98中 union不能包含非POD类型。也不允许union拥有静态或引用类型的成员。

  在 新的 C++ 11 标准 中, 取消 了 联合体 对于 数据 成员 类型 的 限制。 标准 规定, 任何 非 引用 类型 都可以 成为 联合体 的 数据 成员, 这样 的 联合体 即 所谓 的 非 受限 联合体( Unrestricted Union)。

  非 受限 联合体 有一个 非 POD 的 成员, 而 该 非 POD 成员 类型 拥有 有 非 平凡 的 构造 函数, 那么 非 受限 联合体 成员 的 默认 构造 函数 将被 编译器删除。如下:

#include < string> 
using namespace std; 

union T { 
    string s; // string 有 非 平凡 的 构造 函数 
    int n; 
}; 

int main() { 
    T t; // 构造 失败, 因为 T 的 构造 函数 被 删除 了 
} // 编译 选项: g++ -std= c++ 11 3- 7- 4. cpp

  解决 这个 问题 的 办法 是, 由 程序员 自己 为 非 受限 联合体 定义 构造 函数。placement new将会发挥很好的作用。

#include < string> 
using namespace std; 
union T { 
    string s; 
    int n; 
    public: // 自定义 构造 函数 和 析 构 函数 
        T(){ new (&s) string; } 
        ~ T() { s.~ string(); } 
}; 

int main() { 
    T t; // 构造 析 构成 功 
} // 编译 选项: g++ -std= c++ 11 3- 7- 5. cpp

  匿名 非 受限 联合体 可以 运用于 类 的 声明 中。

 

3、用户自定义字面量 — 后缀标识操作符

struct RGBA{ 
    uint8 r; 
    uint8 g; 
    uint8 b; 
    uint8 a; 
    RGBA( uint8 R, uint8 G, uint8 B, uint8 A = 0): r( R), g( G), b( B), a( A){} 
}; 

RGBA operator "" _C( const char* col, size_ t n) 
{ 
    // 一个 长度 为 n 的 字符串 col 
    const char* p = col; 
    const char* end = col + n; 
    const char* r, *g, *b, *a; 
    r = g = b = a = nullptr; 
    for(; p != end; ++ p) { 
        if (*p == 'r') 
            r = p; 
        else if (*p == 'g') g = p;
        else if (*p == 'b') b = p; 
        else if (*p == 'a') a = p; 
    } 
    if ((r == nullptr) || (g == nullptr) || (b == nullptr)) 
        throw; 
    else if (a == nullptr) 
        return RGBA( atoi( r+ 1), atoi( g+ 1), atoi( b+ 1)); 
    else 
        return RGBA( atoi( r+ 1), atoi( g+ 1), atoi( b+ 1), atoi( b+ 1)); 
}

  

  除去 字符串 外, 后缀 声明 也可以 作用于 数值, 比如, 用户 可能 使用 60W、 120W 的 表示 方式 来 标识 功率, 用 50kg 来 表示 质量, 用 1200N 来 表示 力 等。

struct Watt{ unsigned int v; }; 

Watt operator "" _w( unsigned long long v) { 
    return {(unsigned int) v}; 
} 

int main() { 
    Watt capacity = 1024_ w; 
} // 编译 选项: g++ -std= c++ 11 3- 8- 3. cpp

 

  C++11中跟字面量“类型”密切相关的规则有以下四条:

  1)如果 字面 量 为 整型 数, 那么 字面 量 操作 符 函数 只可 接受 unsigned long long 或者 const char* 为 其 参数。

  2)如果 字面 量 为 浮点 型 数, 则 字面 量 操作 符 函数 只可 接受 long double 或者 const char* 为 参数。

  3)如果 字面 量 为 字符串, 则 字面 量 操作 符 函数 函数 只可 接受 const char*, size_ t 为 参数( 已知 长度 的 字符串)。

  4)如果 字面 量 为 字符, 则 字面 量 操作 符 函数 只可 接受 一个 char 为 参数。

 

  另外有以下几点要注意:

  1)operator"" 与 用户 自定义 后缀 之间 必须 有 空格。

  2)后缀 建议 以下 划线 开始。 不宜 使用 非 下划线 后缀 的 用户 自定义 字符串 常量, 否则 会被 编译器 警告。

 

4、内联名字空间

  namespace中的namespace会带来一个问题,如parentspace::childspace,外部使用者调用childspace内的内容的,会比较麻烦,因为需要需要前缀 parentspace::childspace,是否能让外部使用者不需要知道childspace的存在?

  C++98 中可以在parentspace中使用 using namespace child; 来解决,但是存在模板特化的问题。C++ 98 标准 不允许 在 不同 的 名字 空 间中 对 模板 进行 特 化。如下:

namespace Jim { 
    namespace Basic { 
        struct Knife{ 
            Knife() { 
                cout << "Knife in Basic." << endl; 
            } 
        }; 
        class CorkScrew{}; 
    } 
    
    namespace Toolkit{ 
        template< typename T> 
        class SwissArmyKnife{}; 
    }
    
    // ... 
    namespace Other{ 
        // ... 
    } 
    
    // 打开 一些 内部 名字 空间 
    using namespace Basic; 
    using namespace Toolkit; 
} 

// LiLei 决定 对 该 class 进行 特 化 
namespace Jim { 
    template<> class SwissArmyKnife< Knife>{}; // 编译 失败 
} 

using namespace Jim; 

int main() 
{ 
    SwissArmyKnife< Knife> sknife; 
} // 编译 选项: g++ 3- 9- 2. cpp
View Code

  

  在 C++ 11 中, 标准 引入 了 一个 新 特性, 叫做“ 内联的名字空间”。 通过 关键字“ inline namespace” 就可以 声明 一个 内联 的 名字 空间。 内联 的 名字 空间 允许 程序员 在 父名字空间 定义 或 特 化子名字空间 的 模板。

namespace Jim { 
    inline namespace Basic { 
        struct Knife{ 
            Knife() { 
                cout << "Knife in Basic." << endl; 
            } 
        }; 
        class CorkScrew{}; 
    } 
    
    inline namespace Toolkit{ 
        template< typename T> 
        class SwissArmyKnife{}; 
    } 
    
    // ... 
    namespace Other{ 
        Knife b; // Knife in Basic 
        struct Knife{ Knife() { cout << "Knife in Other" << endl;} }; 
        Knife c; // Knife in Other 
        Basic:: Knife k; // Knife in Basic

    } 
    
    // 打开 一些 内部 名字 空间 
    using namespace Basic; 
    using namespace Toolkit; 
} 

// LiLei 决定 对 该 class 进行 特 化 
namespace Jim { 
    template<> class SwissArmyKnife< Knife>{}; // 编译 通过
} 

using namespace Jim; 

int main() 
{ 
    SwissArmyKnife< Knife> sknife; 
} // 编译 选项: g++ 3- 9- 2. cpp
View Code

  

  上面对 inline namespace 的用法是一种误用,上述用法跟将 childspace中的内容放到 parentspace没有区别。inline namespace正确的用法是用于版本管理。如:

namespace Jim { 
#if __cplusplus == 201103L 
    inline 
#endif 
    namespace cpp11{ 
        struct Knife{ Knife() { cout << "Knife in c++ 11." << endl; } }; 
        // ... 
    } 
    
#if __cplusplus < 201103L 
    inline 
#endif 
    namespace oldcpp{ 
        struct Knife{ Knife() { cout << "Knife in old c++." << endl; } }; 
        // ... 
    } 
} 

using namespace Jim; 
int main() { 
    Knife a; // Knife in c++ 11. (默认 版本) 
    cpp11:: Knife b; // Knife in c++ 11. (强制 使用 cpp11 版本) 
    oldcpp:: Knife c; // Knife in old c++. (强制 使用 oldcpp11 版本) 
} // 编译 选项: g++ -std= c++ 11 3- 9- 4. cpp
View Code

   

  所谓“ 参数 关联 名称 查找”, 即 ADL( Argument- Dependent name Lookup)。 这个 特性 允许 编译器 在 名字 空 间内 找 不到 函数 名称 的 时候, 在 参数 的 名字 空间 内 查找 函数 名字。

namespace ns_ adl{ 
    struct A{}; 
    void ADLFunc( A a){} // ADLFunc 定义 在 namespace ns_ adl 中 
} 

int main() { 
    ns_ adl:: A a; 
    ADLFunc( a); // ADLFunc 无需 声明 名字 空间 
}

 

5、模板的别名。

  C++98中只有typedef可以定义类型的别名,C++11中using也可以定义类型的别名。

#include < iostream> 
#include < type_ traits> 
using namespace std; 
using uint = unsigned int; 
typedef unsigned int UINT; 
using sint = int; 

int main() 
{
    cout << is_ same< uint, UINT>:: value << endl; // 1 
} 
// 编译 选项: g++ -std= c++ 11 3- 10- 1. cpp

  不止于此,C++11中using 还可以结合模板使用。

template< typename T> using MapString = std:: map< T, char*>; 
MapString< int> numberedString;

 

6、SFINEA,Substitution failure is not an error.

  对 重载 的 模板 的 参数 进行 展开 的 时候, 如果 展开 导致 了 一些 类型 不匹配, 编译器 并不 会 报错。如下:

struct Test { 
    typedef int foo; 
}; 

template < typename T> 
void f( typename T:: foo) {} // 第一个 模板 定义 - #1 

template < typename T> 
void f( T) {} // 第二个 模板 定义 - #2 

int main() 
{
    f< Test>( 10); // 调用# 1. 
    f< int>( 10); // 调用# 2. 由于 SFINEA, 虽然 不存在 类型 int:: foo, 也不 会 发生 编译 错误 
} 
// 编译 选项: g++ 2- 14- 1. cpp

 

7、右尖括号的改进

  在 C++ 98 中, 有 一条 需要 程序员 规避 的 规则: 如果 在 实例 化 模板 的 时候 出现 了 连续 的 两个 右 尖 括号>, 那么 它们 之间 需要 一个 空格 来 进行 分隔, 以 避免 发生 编译 时 的 错误。

template < int i> 
class X{}; 

template < class T> 
class Y{}; 

Y< X< 1> > x1; // 编译 成功 
Y< X< 2>> x2; // 编译 失败 

// 编译 选项: g++ -c 4- 1- 1. cpp

  上面代码,在 x2 的 定义 中, 编译器 会把>> 优先 解析 为 右移 符号。除去上面这个例子,在使用 *_cast 时也会有同样的问题。如:

const vector< int> v = static_cast< vector< int>>( v); //无法 通过 编译

   C++11中,这种限制被取消了。 

 

8、auto

int main() 
{ 
    double foo(); 
    auto x = 1; // x 的 类型 为 int 
    auto y = foo(); // y 的 类型 为 double
    struct m { int i; } str; 
    auto str1 = str; // str1 的 类型 是 struct m 
    auto z; // 无法 推导, 无法 通过 编译 
    z = x; 
} 
// 编译 选项: g++ -std= c++ 11 4- 2- 2. cpp

   在类型自定义类型转换情形中,auto 能够帮上大忙:

class PI { 
public: 
    double operator* (float v) { 
        return (double) val * v; // 这里 精度 被 扩展 了 
    } 
        
    const float val = 3. 1415927f; 
}; 

int main() 
{ 
    float radius = 1. 7e10; 
    PI pi; 
    auto circumference = 2 * (pi * radius); 
} 
// 编译 选项: g++ -std= c++ 11 4- 2- 5. cpp

   auto的另一特性是自适应:

  1)假设上述代码 PI 的 作者 改动 了 PI 的 定义, 比如 将 operator* 返回 值 变为 long double, 此时, main 函数 并不 需要 修改, 因为 auto 会“ 自 适应” 新的 类型。

  2)对于 不同 的 平 台上 的 代码 维护, auto 也会 带来 一些“ 泛 型” 的 好处。 这里 我们 以 strlen 函数 为例, 在 32 位 的 编译 环境 下, strlen 返回 的 为 一个 4 字节 的 整型, 而在 64 位 的 编译 环境 下, strlen 会 返回 一个 8 字节 的 整型。

    3)当 auto 应用于 模板 的 定义 中, 其“ 自 适应” 性 会 得到 更加 充分 的 体现。如下:

template< typename T1, typename T2> 
double Sum( T1 & t1, T2 & t2) 
{ 
    auto s = t1 + t2; // s 的 类型 会在 模板 实例 化 时 被 推导 出来 
    return s; 
} 

int main() 
{ 
    int a = 3; 
    long b = 5; 
    float c = 1. 0f, d = 2. 3f; 
    auto e = Sum< int ,long>( a, b); // s 的 类型 被 推导 为 long 
    auto f = Sum< float, float>( c, d); // s 的 类型 被 推导 为 float 
} 
// 编译 选项: g++ -std= c++ 11 4- 2- 7. cpp

   auto 可以提升性能:

#define Max1( a, b) ((a) > (b)) ? (a) : (b) 
#define Max2( a, b) ({ \ 
    auto _a = (a); \ 
    auto _b = (b); \ 
    (_a > _b) ? _a: _b; }) 
    
int main() 
{ 
    int m1 = Max1( 1* 2* 3* 4, 5+ 6+ 7+ 8); 
    int m2 = Max2( 1* 2* 3* 4, 5+ 6+ 7+ 8); 
} 
// 编译 选项: g++ -std= c++ 11 4- 2- 8. cpp

  前者 采用 传统 的 三元 运算符 表达式, 这 可能 会 带来 一定 的 性能 问题。 因为 a 或者 b 在 三元 运算符 中 都 出现 了 两次,

9、auto的使用细则

  实际上 对于 a、 c、 d 三个 变量 而言, 声明 其为 auto* 或 auto 并没有 区别。 而 如果 要 使得 auto 声明 的 变量 是 另一个 变量 的 引用, 则 必须 使用 auto&。

int x; 
int * y = &x; 
double foo(); 
int & bar(); 

auto * a = &x; // int* 
auto & b = x; // int& 
auto c = y; // int* 
auto * d = y; // int* 

auto * e = &foo(); // 编译 失败, 指针 不能 指向 一个 临时 变量 
auto & f = foo(); // 编译 失败, nonconst 的 左 值 引用 不能 和 一个 临时 变量 绑 定 
auto g = bar(); // int 
auto & h = bar(); // int& 

// 编译 选项: g++ -std= c++ 11 4- 2- 9. cpp

  声明 为 auto 的 变量 并不能 从其 初始化 表达式 中“ 带走” cv 限制 符。

double foo(); 
float * bar(); 

const auto a = foo(); // a: const double 
const auto & b = foo(); // b: const double& 
volatile auto * c = bar(); // c: volatile float* 

auto d = a; // d: double 
auto & e = a; // e: const double & 
auto f = c; // f: float * 

volatile auto & g = c; // g: volatile float * & 

// 编译 选项: g++ -std= c++ 11 4- 2- 10. cpp

  上述代码中,通过 auto 声明 的 变量 d、 f 却 无法 带走 a 和 f 的 常量 性 或者 易 失 性。 这里 的 例外 还是 引用, 可以 看出, 声明 为 引用 的 变量 e、 g 都 保持 了 其 引用 的 对象 相同 的 属性(

   用 auto 来 声明 多个 变量 类型 时, 只有 第一个 变量 用于 auto 的 类型 推导。注意 const auto *:

auto x = 1, y = 2; // x 和 y 的 类型 均为 int 

// m 是一 个 指向 const int 类型 变量 的 指针, n 是 一个 int 类型 的 变量 
const auto* m = &x, n = 1; auto i = 1, j = 3. 14f; // 编译 失败

auto o = 1, &p = o, *q = &p; // 从左 向右 推导 

// 编译 选项: g++ -std= c++ 11 4- 2- 11. cpp -c

   C++ 11 新 引入 的 初始化 列表, 以及 new, 都可以 使用 auto 关键字

#include < initializer_ list> 
auto x = 1; 
auto x1( 1); 
auto y {1}; // 使用 初始化 列表 的 
auto auto z = new auto( 1); // 可以 用于 new 

// 编译 选项: g++ -std= c++ 11 -c 4- 2- 12. cpp

   禁止使用 auto 的情形:

  1)auto不能是函数形参。

  2)非静态成员变量的类型不能是auto。

  3)不能声明 auto 数组。

  4)不能在模板参数中使用 auto。

#include < vector> 
using namespace std; 
void fun( auto x = 1){} // 1: auto 函数 参数, 无法 通过 编译 

struct str{ 
    auto var = 10; // 2: auto 非 静态 成员 变量, 无法 通过 编译 
}; 

int main() { 
    char x[ 3]; 
    auto y = x; 
    auto z[ 3] = x; // 3: auto 数组, 无法 通过 编译 
    // 4: auto 模板 参数( 实例 化 时), 无法 通过 编译 
    vector< auto> v = {1}; 
} 

// 编译 选项: g++ -std= c++ 11 4- 2- 13. cpp

 

10、typeid 与 decltype

  C++ 98 对 动态 类型 支持 就是 C++ 中的 运行时 类型 识别( RTTI)。

  RTTI 的 机制 是 为 每个 类型 产生 一个 type_info 类型的数据, 程序员 可以 在 程序 中 使用 typeid 随时 查询 一个 变量 的 类型, typeid 就会 返回 变量 相应 的 type_info 数据。 而 type_info 的 name 成员 函数 可以 返回 类型 的 名字。

  而在 C++ 11 中, 又 增加 了 hash_code 这个成员函数, 返回该类型唯一的哈希值, 以供 程序员 对 变量 的 类型 随时 进行 比较。

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

class White{}; 
class Black{}; 

int main() 
{ 
    White a; 
    Black b; 
    cout << typeid( a). name() << endl; // 5White 
    cout << typeid( b). name() << endl; // 5Black 
    White c; 
    bool a_ b_ sametype = (typeid( a). hash_ code() == typeid( b). hash_ code()); 
    bool a_ c_ sametype = (typeid( a). hash_ code() == typeid( c). hash_ code()); 
    cout << "Same type? " << endl; cout << "A and B? " << (int) a_ b_ sametype << endl; // 0 
    cout << "A and C? " << (int) a_ c_ sametype << endl; // 1 
} 

// 编译 选项: g++ -std= c++ 11 4- 3- 1. cpp
View Code

  上面代码中,5 这样 的 前缀 是 g++ 这类 编译器 输出 的 名字, 其他 编译器 可能 会 打印 出 其他 的 名字, 这个 标准 并没有 明确 规定),

   由于 RTTI 会 带来 一些 运行时 的 开销, 所以 一些 编译器 会 让 用户 选择 性地 关闭 该 特性( 比如 XL C/ C++ 编译器 的- qnortti, GCC 的 选项- fno- rttion, 或者 微软 编译器 选项/ GR-)。

 

  decltype 总是 以 一个 普通 的 表达式 为 参数, 返回 该 表达式 的 类型。decltype的demo:

int main() 
{ 
    int i; 
    decltype( i) j = 0; 
    cout << typeid( j). name() << endl; // 打 印出" i", g++ 表示 int 
    float a; 
    double b; 
    decltype( a + b) c; 
    cout << typeid( c). name() << endl; // 打 印出" d", g++ 表示 double 
} 
// 编译 选项: g++ -std= c++ 11 4- 3- 2. cpp
View Code

   

11、decltype的应用

   decltype 与 typedef/using 的全用:

using size_ t = decltype( sizeof( 0)); 
using ptrdiff_ t = decltype(( int*) 0 - (int*) 0); 
using nullptr_ t = decltype( nullptr);

   decltype 可以用于获取匿名类型的类型。这是C++98所做不到的。

enum class{ K1, K2, K3} anon_ e; // 匿名 的 强 类型 枚举 

union { 
    decltype( anon_ e) key; 
    char* name; 
}anon_ u; // 匿名 的 union 联合体 

struct { 
    int d; 
    decltype( anon_ u) id; 
}anon_ s[ 100]; // 匿名 的 struct 数组 

int main() { 
    decltype( anon_ s) as; 
    as[ 0]. id. key = decltype( anon_ e):: K1; // 引用 匿名 强 类型 枚举 中的 值 
} 

// 编译 选项: g++ -std= c++ 11 4- 3- 4. cpp

   与模板特化结合:

template< typename T1, typename T2> 

void Sum( T1 & t1, T2 & t2, decltype( t1 + t2) & s) 
{ 
    s = t1 + t2; 
} 

void Sum( int a[], int b[], int c[])
{ 
    // 数组 版本 
} 

int main() 
{ 
    int a[ 5], b[ 5], c[ 5]; 
    Sum( a, b, c); // 选择 数组 版本 
    int d, e, f; 
    Sum( d, e, f); // 选择 模板 的 实例 化 版本 
} 
// 编译 选项: g++ -std= c++ 11 4- 3- 6. cpp

  decltype 只能 接受 表达式 做 参数, 像 函数 名 做 参数 的 表达式 decltype( hash) 是 无法 通过 编译 的。

int hash( char*); 
map< char*, decltype( hash)> dict_ key; // 无法 通过 编译 
map< char*, decltype( hash( nullptr))> dict_ key1;

  std::result_of 可以用于推导函数返回值类型:

typedef double (*func)(); 
int main() 
{ 
    result_ of< func()>:: type f; // 由 func() 推导 其 结果 类型 
} 
// 编译 选项: g++ -std= c++ 11 4- 3- 8. cpp

 

11、decltype 推导四规则

  decltype 可以带双括号。如下:

int i; 
decltype( i) a; // a: 
int decltype(( i)) b; // b: int &, 无法 编译 通过

   推导四规则如下:

  1)最常见的情形,如前文所应用。如果 e 是一 个 没有带括号的标记符表达式( id- expression) 或者 类成员访问表达式, 那么 decltype( e) 就是 e 所 命名 的 实体 的 类型。 此外, 如果 e 是 一个 被重载的函数, 则 会 导致 编译 时 错误。

  2)否则, 假设 e 的 类型 是 T, 如果 e 是 一个 将 亡 值( xvalue), 那么 decltype( e) 为 T&&。

  3)否则, 假设 e 的 类型 是 T, 如果 e 是 一个 左 值, 则 decltype( e) 为 T&。

  4)否则, 假设 e 的 类型 是 T, 则 decltype( e) 为 T。

int i = 4; 
int arr[ 5] = {0}; 
int *ptr = arr;

struct S { double d; } s; 
void Overloaded( int); 
void Overloaded( char); // 重载 的 函数 
int && RvalRef(); 
const bool Func( int); 

// 规则 1: 单个 标记 符 表达式 以及 访问 类 成员, 推导 为本 类型 
decltype( arr) var1; // int[ 5], 标记 符 表达式 
decltype( ptr) var2; // int*, 标记 符 表达式 
decltype( s. d) var4; // double, 成员 访问 表达式 
decltype( Overloaded) var5; // 无法 通过 编译, 是个 重载 的 函数 

// 规则 2: 将 亡 值, 推导 为 类型 的 右 值 引用 
decltype( RvalRef()) var6 = 1; // int&& 

// 规则 3: 左 值, 推导 为 类型 的 引用 
decltype( true ? i : i) var7 = i; // int&, 三元 运算符, 这里 返回 一个 i 的 左 值 
decltype(( i)) var8 = i; // int&, 带 圆括号 的 左 值 
decltype(++ i) var9 = i; // int&, ++ i 返回 i 的 左 值 
decltype( arr[ 3]) var10 = i; // int& []操作 返回 左 值 
decltype(* ptr) var11 = i; // int& *操作 返回 左 值 
decltype(" lval") var12 = "lval"; // const char(&)[ 9], 字符串 字面 常量 为 左 值 

// 规则 4: 以上 都不 是, 推导 为本 类型 
decltype( 1) var13; // int, 除 字符串 外 字面 常量 为 右 值 
decltype( i++) var14; // int, i++ 返回 右 值 
decltype(( Func( 1))) var15; // const bool, 圆括号 可以 忽略 
View Code

 

   可以使用 is_rvalue_reference、is_lvalue_reference 来判断是否是左右值引用。

#include < type_ traits> 
#include < iostream> 
using namespace std; 
int i = 4; 
int arr[ 5] = {0}; 
int *ptr = arr; 
int && RvalRef(); 
int main()
{ 
    cout << is_ rvalue_ reference< decltype( RvalRef())>:: value << endl; // 1 
    cout << is_ lvalue_ reference< decltype( true ? i : i)>:: value << endl; // 1 
    cout << is_ lvalue_ reference< decltype(( i))>:: value << endl; // 1 
    cout << is_ lvalue_ reference< decltype(++ i)>:: value << endl; // 1 
    cout << is_ lvalue_ reference< decltype( arr[ 3])>:: value << endl; // 1 
    cout << is_ lvalue_ reference< decltype(* ptr)>:: value << endl; // 1 
    cout << is_ lvalue_ reference< decltype(" lval")>:: value << endl; // 1 
    cout << is_ lvalue_ reference< decltype( i++)>:: value << endl; // 0 
    cout << is_ rvalue_ reference< decltype( i++)>:: value << endl; // 0 
} 

 

12、cv限制符的继承与冗余符号

  与 auto 类型 推导 时不 能“ 带走” cv 限制 符 不同, decltype 是 能够“ 带走” 表达式 的 cv 限制 符 的。通过 is_const、is_volatile 可以判定 const、volatile 性。

using namespace std; 
const int ic = 0; 
volatile int iv; 
struct S { int i; }; 
const S a = {0}; 
volatile S b; 
volatile S* p = &b; 
int main() 
{ 
    cout << is_ const< decltype( ic)>:: value << endl; // 1 
    cout << is_ volatile< decltype( iv)>:: value << endl; // 1 
    cout << is_ const< decltype( a)>:: value << endl; // 1 
    cout << is_ volatile< decltype( b)>:: value << endl; // 1 
    cout << is_ const< decltype( a. i)>:: value << endl; // 0, 成员 不是 const 
    cout << is_ volatile< decltype( p-> i)>:: value << endl; // 0, 成员 不是 volatile 
}

   进行 类型 定义 时, 也会 允许 一些 冗余 的 符号。 比如 cv 限制 符 以及 引用,符号&, 通常 情况下, 如果 推 导出 的 类型 已经 有了 这些 属性, 冗余 的 符号 则 会被 忽略。

int i = 1; 
int & j = i; 
int * p = &i; 
const int k = 1; 
int main() 
{ 
    decltype( i) & var1 = i; 
    decltype( j) & var2 = i; // 冗余 的&, 被 忽略 
    cout << is_ lvalue_ reference< decltype( var1)>:: value << endl;// 1, 是 左 值 引用 
    cout << is_ rvalue_ reference< decltype( var2)>:: value << endl;// 0, 不是 右 值 引用 
    cout << is_ lvalue_ reference< decltype( var2)>:: value << endl;// 1, 是 左 值 引用 
    decltype( p)* var3 = &i; // 无法 通过 编译 
    decltype( p)* var3 = &p; // var3 的 类型 是 int**
    auto* v3 = p; // v3 的 类型 是 
    int* v3 = &i; 
    const decltype( k) var4 = 1; // 冗余 的 const, 被 忽略 
}

   这里 特别 要 注意 的 是 decltype( p)* 的 情况。 可以 看到, 在 定义 var3 变量 的 时候, 由于 p 的 类型 是 int*, 因此 var3 被 定义 为了 int** 类型。 这 跟 auto 声明 中,* 也可以 是 冗余 的 不同。 在 decltype 后的* 号, 并不 会被 编译器 忽略。

 

13、追踪返回类型的引入

  C++ 11 引入 新语 法— 追踪 返回 类型。

template< typename T1, typename T2> 
auto Sum( T1 & t1, T2 & t2) -> decltype( t1 + t2)
{ 
    return t1 + t2;
}

  我们 把 函数 的 返回 值 移至 参数 声明 之后, 复合 符号-> decltype( t1+ t2) 被称为 追踪 返回 类型。 而 原本 函数 返回 值 的 位置 由 auto 关键字 占据。 这样, 我们 就可以 让 编译器 来 推导 Sum 函数 模板 的 返回 类型 了。 而 auto 占位符 和-> return_ type 也就是 构成 追踪 返回 类型 函数 的 两个 基本 元素。

 

14、使用追踪返回类型的函数

  使用追踪返回类型的时候,可以不必写明作用域。如下,InnerType不必写明其作用域。

class OuterType
{ 
    struct InnerType { int i; }; 
    InnerType GetInner(); 
    InnerType it; 
}; 
// 可以 不 写 OuterType:: InnerType 
auto OuterType:: GetInner() -> InnerType { return it; }

  返回 类型 后置, 使 模板 中的 一些 类型 推导 就成 为了 可能。如下:

template< typename T1, typename T2> 
auto Sum( const T1 & t1, const T2 & t2) -> decltype( t1 + t2)
{ 
    return t1 + t2; 
} 

template < typename T1, typename T2> 
auto Mul( const T1 & t1, const T2 & t2) -> decltype( t1 * t2)
{ 
    return t1 * t2; 
} 

int main() 
{ 
    auto a = 3; 
    auto b = 4L; 
    auto pi = 3. 14; 
    auto c = Mul( Sum( a, b), pi); 
    cout << c << endl; // 21. 98 
}

  追踪 返回 类型 的 另一个 优势 是 简化 函数 的 定义, 提高 代码 的 可读性。

#include < type_ traits> 
#include < iostream> 
using namespace std; 

// 有的 时候, 你会 发现 这是 面试 题 
int (*(*pf())())() { return nullptr; } 

// auto (*)() -> int(*) () 一个 返回 函数 指针 的 函数( 假设 为 a 函数) 
// auto pf1() -> auto (*)() -> int (*)() 一个 返回 a 函数 的 指针 的 函数 
auto pf1() -> auto (*)() -> int (*)() { return nullptr; } 

int main() 
{ 
    cout << is_ same< decltype( pf), decltype( pf1)>:: value << endl; // 1 
}

   使用  追踪 返回 类型, 可以 实现 参数 和 返回 类型 不 同时 的 转发。

double foo( int a) 
{ 
    return (double) a + 0. 1; 
} 

int foo( double b) 
{ 
    return (int) b; 
} 

template < class T> 
auto Forward( T t) -> decltype( foo( t))
{ 
    return foo( t); 
} 

int main()
{ 
    cout << Forward( 2) << endl; // 2. 1 
    cout << Forward( 0. 5) << endl; // 0 
}

   以下内容是等价的。

auto (*fp)() -> int;
int (*fp)();

auto (&fr)()->int;
int (&fr)();

 

   

15、

 

16、

17、

posted on 2019-01-04 14:42  Tekkaman  阅读(463)  评论(0编辑  收藏  举报