深入理解C++11【3】

时间:2023-03-08 19:11:55

深入理解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 = ): 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+ ), atoi( g+ ), atoi( b+ ));
else
return RGBA( atoi( r+ ), atoi( g+ ), atoi( b+ ), atoi( b+ ));
}

  

  除去 字符串 外, 后缀 声明 也可以 作用于 数值, 比如, 用户 可能 使用 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

  

  在 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

  

  上面对 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

  

  所谓“ 参数 关联 名称 查找”, 即 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>( ); // 调用# 1.
f< int>( ); // 调用# 2. 由于 SFINEA, 虽然 不存在 类型 int:: foo, 也不 会 发生 编译 错误
}
// 编译 选项: g++ 2- 14- 1. cpp

7、右尖括号的改进

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

template < int i>
class X{}; template < class T>
class Y{}; Y< X< > > x1; // 编译 成功
Y< X< >> 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 = ; // 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 = . 1415927f;
}; int main()
{
float radius = . 7e10;
PI pi;
auto circumference = * (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 = ;
long b = ;
float c = . 0f, d = . 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( * * * , + + + );
int m2 = Max2( * * * , + + + );
}
// 编译 选项: 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 = , y = ; // x 和 y 的 类型 均为 int 

// m 是一 个 指向 const int 类型 变量 的 指针, n 是 一个 int 类型 的 变量
const auto* m = &x, n = ; auto i = , j = . 14f; // 编译 失败 auto o = , &p = o, *q = &p; // 从左 向右 推导 // 编译 选项: g++ -std= c++ 11 4- 2- 11. cpp -c

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

#include < initializer_ list>
auto x = ;
auto x1( );
auto y {}; // 使用 初始化 列表 的
auto auto z = new auto( ); // 可以 用于 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: auto 函数 参数, 无法 通过 编译 struct str{
auto var = ; // 2: auto 非 静态 成员 变量, 无法 通过 编译
}; int main() {
char x[ ];
auto y = x;
auto z[ ] = x; // 3: auto 数组, 无法 通过 编译
// 4: auto 模板 参数( 实例 化 时), 无法 通过 编译
vector< auto> v = {};
} // 编译 选项: 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

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

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

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

int main()
{
int i;
decltype( i) j = ;
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

  

11、decltype的应用

  decltype 与 typedef/using 的全用:

using size_ t = decltype( sizeof( ));
using ptrdiff_ t = decltype(( int*) - (int*) );
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[ ]; // 匿名 的 struct 数组 int main() {
decltype( anon_ s) as;
as[ ]. 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[ ], b[ ], c[ ];
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 = ;
int arr[ ] = {};
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 = ; // int&& // 规则 3: 左 值, 推导 为 类型 的 引用
decltype( true ? i : i) var7 = i; // int&, 三元 运算符, 这里 返回 一个 i 的 左 值
decltype(( i)) var8 = i; // int&, 带 圆括号 的 左 值
decltype(++ i) var9 = i; // int&, ++ i 返回 i 的 左 值
decltype( arr[ ]) var10 = i; // int& []操作 返回 左 值
decltype(* ptr) var11 = i; // int& *操作 返回 左 值
decltype(" lval") var12 = "lval"; // const char(&)[ 9], 字符串 字面 常量 为 左 值 // 规则 4: 以上 都不 是, 推导 为本 类型
decltype( ) var13; // int, 除 字符串 外 字面 常量 为 右 值
decltype( i++) var14; // int, i++ 返回 右 值
decltype(( Func( ))) var15; // const bool, 圆括号 可以 忽略

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

#include < type_ traits>
#include < iostream>
using namespace std;
int i = ;
int arr[ ] = {};
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[ ])>:: 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 = ;
volatile int iv;
struct S { int i; };
const S a = {};
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 = ;
int & j = i;
int * p = &i;
const int k = ;
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 = ; // 冗余 的 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 = ;
auto b = 4L;
auto pi = . ;
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 + . ;
} int foo( double b)
{
return (int) b;
} template < class T>
auto Forward( T t) -> decltype( foo( t))
{
return foo( t);
} int main()
{
cout << Forward( ) << endl; // 2. 1
cout << Forward( . ) << endl; // 0
}

  以下内容是等价的。

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

  

15、

16、

17、