【深入理解C++11【4】】
1、基于范围的 for 循环
C++98 中需要告诉编译器循环体界面范围。如for,或stl 中的for_each:
int main() { int arr[ 5] = { 1, 2, 3, 4, 5}; int * p; for (p = arr; p < arr + sizeof( arr)/ sizeof( arr[ 0]); ++ p){ *p *= 2; } for (p = arr; p < arr + sizeof( arr)/ sizeof( arr[ 0]); ++ p){ cout << *p << ' '; } }
int action1( int & e){ e *= 2; } int action2( int & e){ cout << e << ' '; } int main() { int arr[ 5] = { 1, 2, 3, 4, 5}; for_ each( arr, arr + sizeof( arr)/ sizeof( arr[ 0]), action1); for_ each( arr, arr + sizeof( arr)/ sizeof( arr[ 0]), action2); }
由 程序员 来说 明 循环 的 范围是 多余 的, 也是 容易 犯错误 的。 而 C++ 11 也 引入 了 基于 范围 的 for 循环, 就可以 很好 地 解决 了 这个 问题。
int main() { int arr[ 5] = { 1, 2, 3, 4, 5 }; for (int & e: arr) e *= 2; for (int & e: arr) cout << e << ' '; }
上棕采用了引用的形式,也可以不采用引用的形式。
for (int e: arr) cout << e << ' ';
结合之前的 auto,会更简练。
for (auto e: arr) cout << e << ' ';
下述 代码 会 报错, 因为 作为参数传递而来的数组 a 的范围不能确定, 因此 也就 不能 使用 基于 范围 循环 for 循环 对其 进行 迭代 的 操作。
int func( int a[]) { for (auto e: a) cout << e; } int main() { int arr[] = {1, 2, 3, 4, 5}; func( arr); }
2、枚举:分门别类与数值的名字。
觉的常量定义方式:
1)宏。宏 的 弱点 在于 其 定义 的 只是 预处理 阶段 的 名字, 如果 代码 中有 Male 或者 Female 的 字符串, 无论 在什么 位置 一律 将被 替换。 这 在 有的 时候 会 干扰 到 正常 的 代码,
#define Male 0 #define Female 1
2)枚举。相比于宏,会 得到 编译器 的 检查。且 不会有 干扰 正常 代码 的 尴尬。
enum { Male, Female };
3)静态常量。由于 是 静态 常量, 其 名字 作用域 也 被 很好 地 局限于 文件 内。
const static int Male = 0; const static int Female = 1;
3、有缺陷的枚举类型
C/ C++ 的 enum 有个 很“ 奇怪” 的 设定, 就是 具名( 有 名字) 的 enum 类型 的 名字, 以及 enum 的 成员 的 名字 都是 全局 可见 的。下面代码,Category 中的 General 和 Type 中的 General 都是 全局 的 名字, 因此 编译会报错。
enum Type { General, Light, Medium, Heavy }; enum Category { General, Pistol, MachineGun, Cannon };
匿名space的 enum还是会污染全局空间。如下:
namespace T { enum Type { General, Light, Medium, Heavy }; } namespace { enum Category { General = 1, Pistol, MachineGun, Cannon }; } int main() { T:: Type t = T:: Light; if (t == General) // 忘记 使用 namespace cout << "General Weapon" << endl; return 0; }
C++98 中并不会阻止不同enum 间的比较。
enum Type { General, Light, Medium, Heavy }; //enum Category { General, Pistol, MachineGun, Cannon }; // 无法 编译 通过, 重复 定义 了 General enum Category { Pistol, MachineGun, Cannon }; int main() { Type type1; if (type1 >= Pistol) cout << "It is not a pistol" << endl; }
C++98 中可以通过封闭来解决上述污染和类型比较的问题。
class Type { public: enum type { general, light, medium, heavy }; type val; public: Type( type t): val( t){} bool operator >= (const Type & t) { return val >= t. val; } static const Type General, Light, Medium, Heavy; }; const Type Type:: General( Type:: general); const Type Type:: Light( Type:: light); const Type Type:: Medium( Type:: medium); const Type Type:: Heavy( Type:: heavy); class Category { public: enum category { pistol, machineGun, cannon }; category val; public: Category( category c): val( c) {} bool operator >= (const Category & c) { return val >= c. val; } static const Category Pistol, MachineGun, Cannon; }; const Category Category:: Pistol( Category:: pistol); const Category Category:: MachineGun( Category:: machineGun); const Category Category:: Cannon( Category:: cannon); struct Killer { Killer( Type t, Category c) : type( t), category( c){} Type type; Category category; }; int main() { // 使用 类型 包装 后的 enum Killer notCool( Type:: General, Category:: MachineGun); // ... // ...其他 很多 代码... // ... if (notCool. type >= Type:: General) // 可以 通过 编译 cout << "It is not general" << endl; if (notCool. type >= Category:: Pistol) // 该 句 无法 编译 通过 cout << "It is not a pistol" << endl; // ... cout << is_ pod< Type>:: value << endl; // 0 cout << is_ pod< Category>:: value << endl; // 0 return 0; }
编译器 会 根据 数据类型 的 不同 对 enum 应用 不同 的 数据 长度。 在 我们 对 g++ 的 测试 中, 普通 的 枚举 使用 了 4 字节 的 内存, 而 当 需要 的 时候, 会 拓展 为 8 字节。
enum C { C1 = 1, C2 = 2}; enum D { D1 = 1, D2 = 2, Dbig = 0xFFFFFFF0U }; enum E { E1 = 1, E2 = 2, Ebig = 0xFFFFFFFFFLL}; int main() { cout << sizeof( C1) << endl; // 4 cout << Dbig << endl; // 编译器 输出 不同, g++: 4294967280 cout << sizeof( D1) << endl; // 4 cout << sizeof( Dbig) << endl; // 4 cout << Ebig << endl; // 68719476735 cout << sizeof( E1) << endl; // 8 return 0; }
不同的编译器,上例中 Dbig 的 输出 结果 将会 不同: 使用 Visual C++ 编译程序 的 输出 结果 为– 16, 而使 用 g++ 来 编译 输出 为 4294967280。 这是 由于 Visual C++ 总是 使用 无符号 类型 作为 枚举 的 底层 实现, 而 g++ 会 根据 枚举 的 类型 进行 变动 造成 的。
4、强类型枚举(strong-typed enum) 以及 C++ 11 对 原有枚举类型的扩展
声明 强 类型 枚举 非常 简单, 只需 要在 enum 后加 上 关键字 class。
enum class Type { General, Light, Medium, Heavy };
强类型枚举有以下几个优势:
1)强作用域,强类型枚举成员的名称不会被输出到其你作用域空间。
2)转换限制,强类型枚举成员的值不可以与整形隐式地相互转换。
3)可以 指定 底层 类型。 强 类型 枚举 默认 的 底层 类型 为 int, 但也 可以 显 式 地 指定 底层 类型。
enum class Type: char { General, Light, Medium, Heavy };
下面是一个强类型枚举的例子:
enum class Type { General, Light, Medium, Heavy }; enum class Category { General = 1, Pistol, MachineGun, Cannon }; int main() { Type t = Type:: Light; t = General; // 编译 失败, 必须 使用 强 类型 名称 if (t == Category:: General) // 编译 失败, 必须 使用 Type 中的 General cout << "General Weapon" << endl; if (t > Type:: General) // 通过 编译 cout << "Not General Weapon" << endl; if (t > 0) // 编译 失败, 无法 转换 为 int 类型 cout << "Not General Weapon" << endl; if ((int) t > 0) // 通过 编译 cout << "Not General Weapon" << endl; cout << is_ pod< Type>:: value << endl; // 1 cout << is_ pod< Category>:: value << endl; // 1 return 0; }
设置较小的基本类型可以节省空间:
enum class C : char { C1 = 1, C2 = 2}; enum class D : unsigned int { D1 = 1, D2 = 2, Dbig = 0xFFFFFFF0U }; int main() { cout << sizeof( C:: C1) << endl; // 1 cout << (unsigned int) D:: Dbig << endl; // 编译器 输出 一致, 4294967280 cout << sizeof( D:: D1) << endl; // 4 cout << sizeof( D:: Dbig) << endl; // 4 return 0; }
C++11增强了原有枚举类型:
1)可以 跟 强 类型 枚举 类 一样, 显 式 地 由 程序员 来 指定。
enum Type: char { General, Light, Medium, Heavy };
2)扩展作用域。除了增加到父作用域外,还增加到自身作用域,两者等价。
enum Type { General, Light, Medium, Heavy }; Type t1 = General; Type t2 = Type:: General;
此外, 我们 在 声明 强 类型 枚举 的 时候, 也可以 使用 关键字 enum struct。 事实上 enum struct 和 enum class 在 语法 上 没有 任何 区别( enum class 的 成员 没有 公有 私有 之分,
有 一点 比较 有趣 的 是 匿名 的 enum class。 由于 enum class 是 强 类型 作用域 的, 故匿名的 enum class 很可能什么都做不了。
enum class { General, Light, Medium, Heavy } weapon; int main() { weapon = General; // 无法 编译 通过 bool b = (weapon == weapon:: General); // 无法 编译 通过 return 0; }
5、显式内存管理。
常见内存管理问题:
1)野指针、重复释放。指向指向内存已被释放,但指针却还在被使用。
2)内存泄露。指针已经丢失,但其指向内存并未被释放。
C++98中的智能指针是 auto_ptr,不过 auto_ ptr 有 一些 缺点( 拷贝 时 返回 一个 左 值, 不能 调用 delete[] 等), 所以 在 C++ 11 标准中被废弃了。 C++ 11 标准 中 改用 unique_ ptr、 shared_ ptr 及 weak_ ptr 等 智能 指针 来自 动 回收 堆 分配 的 对象。
下面是 unique_ptr、shared_ptr 的例子:
#include < memory> #include < iostream> using namespace std; int main() { unique_ ptr< int> up1( new int( 11)); // 无法 复制 的 unique_ ptr unique_ ptr< int> up2 = up1; // 不能 通过 编译 cout << *up1 << endl; // 11 unique_ ptr< int> up3 = move( up1); // 现在 p3 是 数据 唯一 的 unique_ ptr 智能 指针 cout << *up3 << endl; // 11 cout << *up1 << endl; // 运行时 错误 up3. reset(); // 显 式 释放 内存 up1. reset(); // 不会 导致 运行时 错误 cout << *up3 << endl; // 运行时 错误 shared_ ptr< int> sp1( new int( 22)); shared_ ptr< int> sp2 = sp1; cout << *sp1 << endl; // 22 cout << *sp2 << endl; // 22 sp1. reset(); cout << *sp2 << endl; // 22 }
unique_ ptr 则是 一个 删除 了 拷贝 构造 函数、 保留 了 移动 构造 函数 的 指针 封装 类型。
shared_ptr 在实现 上 采用 了 引用 计数, 所以 一旦 一个 shared_ ptr 指针 放弃 了“ 所有权”( 失效), 其他 的 shared_ ptr 对对 象 内存 的 引用 并不 会 受到影响。
weak_ptr 可 以 指向 shared_ ptr 指针 指向 的 对象 内存, 却 并不 拥有 该 内存。 而使 用 weak_ ptr 成员 lock, 则 可 返回 其 指向 内存 的 一个 shared_ ptr 对象, 且 在 所指 对象 内存 已经 无效 时, 返回 指针 空 值( nullptr, 请 参见 7. 1 节)。 这 在 验证 share_ ptr 智能 指针 的 有效性 上 会很 有 作用。如下:
#include < memory> #include < iostream> using namespace std; void Check( weak_ ptr< int> & wp) { shared_ ptr< int> sp = wp. lock(); // 转换 为 shared_ ptr< int> if (sp != nullptr) cout << "still " << *sp << endl; else cout << "pointer is invalid." << endl; } int main() { shared_ ptr< int> sp1( new int( 22)); shared_ ptr< int> sp2 = sp1; weak_ ptr< int> wp = sp1; // 指向 shared_ ptr< int> 所指 对象 cout << *sp1 << endl; // 22 cout << *sp2 << endl; // 22 Check( wp); // still 22 sp1. reset(); cout << *sp2 << endl; // 22 Check( wp); // still 22 sp2. reset(); Check( wp); // pointer is invalid }
6、垃圾回收的分类。
垃圾回收的方式可以分为两大类:
1)基于引用计数。这种 方法 比较 难处理“ 环形 引用” 问题, 此外 由于 计数 带来 的 额外 开销 也 并不 小。
2)基于跟踪处理。跟踪 处理 的 垃圾 回收 机制 被 更为 广泛 地 应用。主要有以下几种方法:
a)标记 - 清除(Mark-Sweep)
这种 方法 的 特点 是 活的 对象 不会 被 移动, 但是 其 存在 会 出现 大量 的 内存 碎片 的 问题。
b)标记 - 整理(Mark-Compact)
这个 算法 标记 的 方法 和 标记- 清除 方法 一样, 但是 标记 完 之后, 不再 遍历 所有 对象 清扫 垃圾 了, 而是 将 活的 对象 向“ 左” 靠 齐, 这就 解决 了 内存 碎片 的 问题。
c)标记 - 拷贝(Mark-Copy)
标记– 整理 算法 的 另一种 实现。这种 算法 将 堆 空间 分为 两个 部分: From 和 To。
C++ 11 标准 也 开始 对 垃圾 回收 做了 一定 的 支持, 虽然 支持 的 程度 还 非常 有限。
7、C++ 与垃圾回收。
因为C++中可以自由移动指针,所以如果加入垃圾回收,有可能将有用的内存回收,从而导致重大内存问题。
int main() { int* p = new int; p += 10; // 移动 指针, 可能 导致 垃圾 回收 器 p -= 10; // 回收 原来 指向 的 内存 *p = 10; // 再次 使用 原本 相同 的 指针 则 可能 无效 }
下例也是一样,隐藏原始指针,可能导致内存被回收问题。
int main() { int *p = new int; int *q = (int*)( reinterpret_ cast< long long>( p) ^ 2012); // q 隐藏 了 p // 做 一些 其他 工作, 垃圾 回收 器 可能 已经 回收 了 p 指向 对象 q = (int*)( reinterpret_ cast< long long>( q) ^ 2012); // 这里 的 q == p *q = 10; }
8、C++11 与最小垃圾回收支持。
截至2013年, 几乎没有 编译器 实现 了 最小 垃圾 回收 支持, 甚至 连 get_ pointer_ safety 这个 函数 接口 都 还没 实现。
declare_ reachable() 显 式 地 通知 垃圾 回收 器 某一个 对象 应被 认为 可达 的, 即使 它的 所有 指针 都对 回收 器 不 可见。 undeclare_ reachable() 则 可以 取消 这种 可达 声明。
#include < memory> using namespace std; int main() { int *p = new int; declare_ reachable( p); // 在 p 被 隐藏 之前 声明 为 可达 的 int *q = (int*)(( long long) p ^ 2012); // 解除 可达 声明 q = undeclare_ reachable< int>(( int*)(( long long) q ^ 2012)); *q = 10; }
有的 时候 程序员 会 选择 在 一大 片 连续 的 堆 内存 上 进行 指针式 操作, 为了 让 垃圾 回收 器 不关心 该 区域, 也可以 使用 declare_ no_ pointers 及 undeclare_ no_ pointers 函数 来 告诉 垃圾 回收 器 该 内存 区域 不存在 有效 的 指针。
void declare_ no_ pointers( char *p, size_ t n) noexcept;
void undeclare_ no_ pointers( char *p, size_ t n) noexcept;
9、垃圾回收的兼容性。
C++ 11 标准 中 对 指针 的 垃圾 回收 支持 仅限 于 系统 提供 的 new 操作 符 分配 的 内存, 而 malloc 分配 的 内存 则 会被 认为 总是 可达 的, 即 无论 何时 垃圾 回收 器 都不 予 回收。 因此 使用 malloc 等 的 较老 代码 的 堆 内存 还是 必须 由 程序员 自己 控制。
10、运行时常量性与编译时常量性。
const用于保证运行期常量性,但有时候,我们需要的却是编译时的常量性,这是const关键字无法保证的。
const int GetConst() { return 1; } void Constless( int cond) { int arr[ GetConst()] = {0}; // 无法 通过 编译 enum { e1 = GetConst(), e2 }; // 无法 通过 编译 switch (cond) { case GetConst(): // 无法 通过 编译 break; default: break; } }
上述代码,我们发现, 无论 将 GetConst 的 结果 用于 需要 初始化 数组 Arr 的 声明 中, 还是 用于 匿名 枚举 中, 或 用于 switch- case 的 case 表达式 中, 编译器 都会 报告 错误。
发生 这样 错误 的 原因 如 我们 上面 提到 的 一样, 这些 语句 都 需 要的 是 编译 时期 的 常 量值。 而 const 修饰 的函数 返回 值, 只 保证 了 在 运行时 期内 其 值 是 不可以 被 更改 的。 这是 两个 完全 不同 的 概念。
enum BitSet { V0 = 1 << 0, V1 = 1 << 1, V2 = 1 << 2, VMAX = 1 << 3 }; // 重定 义 操作 符"|", 以 保证 返回 的 BitSet 值 不超过 枚举 的 最大值 const BitSet operator|( BitSet x, BitSet y) { return static_ cast< BitSet>((( int) x | y) & (VMAX - 1)); } template < int i = V0 | V1> // 无法 通过 编译 void LikeConst(){}
上述代码,我们将 V0| V1 作为 非 类型 模板 函数 的 默认 模板 参数, 则 会 导致 编译 错误。 这 同样 是由 需 要的 是 编译 时 常量 所 导致 的。
宏可以解决上述问题,但这种 简单 粗暴 的 做法 即使 有效, 也 会把 C++ 拉回“ 石器 时代”。 C++ 11 中 对 编译 时期 常量 的 回答 是 constexpr, 即 常量表达式( constant expression)。
constexpr int GetConst() { return 1; }
在 函数 表达式 前 加上 constexpr 关键字 即可。 有了 常量 表达式 这样 的 声明, 编译器 就可 以在 编译 时期 对 GetConst 表达式 进行 值 计算( evaluation), 从而 将其 视为 一个 编译 时期 的 常量。
这样一来 上述代码 数组 Arr、 匿名 枚举 的 初始化 以及 switch- case 的 case 表达式 通过 编译 都 不再 是 问题。
11、常量表达式函数。
并非 所有 的 函数 都有 资格 成为 常量 表达式 函数。 事实上, 常量 表达式 函数 的 要求 非常 严格, 总结 起来, 大概 有 以下 几点:
1)函数体只有单一的return返回语句。例如,下面的的写会编译错误:
constexpr int data() { const int i = 1; return i; }
不过 一些 不会 产生 实际 代码 的 语句 在 常量 表达式 函数 中 使用 下, 倒 不会 导致 编译器 的“ 抱怨”。
constexpr int f( int x) { static_ assert( 0 == 0, "assert fail."); return x; }
上面例子 能够 通过 编译。 而 其他 的, 比如 using 指令、 typedef 等 也 通常 不会 造成 问题。
2)函数必须返回值(不能是void函数)。例如,下面的写法,就不能通过:
constexpr void f(){}
3)在使用前必须已有定义。
constexpr int f(); int a = f(); const int b = f(); constexpr int c = f(); // 无法 通过 编译 constexpr int f() { return 1; } constexpr int d = f();
constexpr 不算重载,会导致编译错误。
constexpr int f(); int f();
4)return 返回 语句 表达式 中 不能 使用 非 常量 表达式 的 函数、 全局 数据, 且 必须 是一 个 常量 表达式。例如以下constexpr不能通过编译。
const int e(){ return 1;} constexpr int g(){ return e(); } // 编译错误 int g = 3; constexpr int h() { return g; } // 编译错误
另外,constexpr中赋值语句也是不允许的。以下会编译错误。
constexpr int k( int x) { return x = 1; }
12、常量表达式的值。
C++ 11 标准 中, constexpr 关键字 是 不能 用于 修饰 自定义 类型 的 定义 的。以下代码,无法通过编译。
constexpr struct MyType {int i; } constexpr MyType mt = {0};
正确地 做法 是, 定义 自定义 常量构造函数( constent- expression constructor)。
struct MyType { constexpr MyType( int x): i( x){} int i; }; constexpr MyType mt = {0};
常量 表达式 的 构造 函数 也有 使 用上 的 约束, 主 要的 有 以下 两点:
1)函数体必须为空。
2)初始化列表只能由常量表达式来赋值。例如,以下常量构造函数是错误的。
int f(); struct MyType {int i; constexpr MyType():i(f()){}};
常量构造函数、成员函数的区别:
struct Date { constexpr Date(int y, int m, int d): year(y), month(m),day(d){} constexpr int GetYear() { return year; } constexpr int GetMonth() { return month; } constexpr int GetDay() { return day; } private: int year; int month; int day; } constexpr Date PRCfound {1949,10,1}; constexpr int foundmont = PRCfound.GetMonth(); int main() { count << foundmonth << endl; // 10 }
C++11中,不允许常量表达式作用于 virtual 成员函数。
13、常量表达式的其他应用
常量表达式可以用于模板函数。不过由于模板中类型的不确定性,所以模板函数是否会被实例化为一个能够满足编译时常量性的版本通常也是未知的。
C++11规定,当声明为常量表达式的模板函数后,而某个该模板函数的实例化结果不满足常量表达式的需求的话,constexpr会被自动忽略。该实例化后的函数将成为一个普通函数。
struct NotLiteral{ NotLiteral(){i=5;} int i; }; NotLiteral nl; template<typename T> constexpr T ConstExp(T t){ return t; } void g(){ NotLiteral nl; NotLiteral nl1 = ConstExp(nl); constexpr NotLiteral nl2 = ConstExp(nl); // 无法通过编译 constexpr int a = ConstExp(1); }
上述代码,NotLiteral不是一个定义了常量表达式构造函数的类型,ConstExp一旦以 NotLiteral为参数的话,那么其 constexpr关键字将被忽略,所以 ConstExp<NotLiteral>函数不是一个常量表达式函数。
C++11 标准对常量表达式支持至少 512层递归。
constexpr int Fibonacci(int n){ return (n==1)?1:((n--2)?1:Fibonacci(n-1)+Fibonacci(n-2)); } int main(){ int fib[] = { Fibonacci(11), Fibonacci(12), Fibonacci(13), Fibonacci(14), Fibonacci(15), Fibonacci(16) }; for (int i : fib) cout<<i<<endl; }
在C++98中,上述常量表达式函数递归可以通过模板元编程来完成。
template<long num> struct Fibonacci{ static const long val = Fibonacci<num-1>::val + Fibonacci<num-2>::val; } template<> struct Fibonacci<2> { static const long val =1; } template<> struct Fibonacci<1> { static const long val =1; } template<> struct Fibonacci<0> { static const long val =0; }
14、变长函数和变长的模板参数
C++98 中使用 va_start、va_arg、 va_end 来实现变长函数参数功能。
double SumOfFloat(int count, ...){ va_list ap; ouble sum = 0; va_start(ap, count); for(int i = 0; i < count; i++) sum++ va_arg(ap, double); va_end(ap); return sum(); } int main(){ return printf("%f ", SumOfFloat(3,1.2f,3.4,5.6)); }
C++98要求函数模板始终具有数目确定的模板参数。
15、 变长模板:模板参数包和函数参数包
标识符前加三个点,表示该参数是变长的。Elements被称为模板参数包(template parameter pack)。
template <typename... Elements>
class tuple;
模板参数包也可以是非类型的,例如:
template<int... A> class NonTypeVariadicTemplate{}; NonTypeVariadicTemplate<1,0,2> ntvt;
解包(unpack)在尾部添加三个点。下面这个叫包扩展(pack expansion)表达式。
template<typename... A> class Template : private B<A...>{};
上面只是展开了 A...,但没有使用展开后的内容。Tunple 的实现中,展示了一种使用包括展的方法,数学归纳法(递归)。这与模板元编程,constexpr非常相似。
// 变长模板的声明 template <typename... Elements> class tuple; // 递归的偏特化的定义 template <typename Head, typename... Tail> class tuple<Head, Tail...>:private tuple<Tail...>{ Head head; }; // 边界条件 template<> class tuple<>{};
下面是一个使用非类型模板的例子。
// 声明 template <long... nums> struct Multiply; // 偏特化 template <long first, long... last> struct Multiply<first, last...>{ static const long val = first*Multiply<last...>::val; }; // 边界条件 template<> struct Multiply<>{ static const long val = 1; };
C++11中,还可以声明变长模板函数。变长模板函数的参数可以声明成为函数参数包(function parameter pack)。
template<typename... T> void f(T... args);
C++11中,标准要求函数参数包必须唯一,且是函数最后一个参数(模板参数包没有这样的要求)。
有了模板参数包、函数参数包,就可以实现C中变长函数的功能了。
void Printf(const char* s) { while(*s){ if(*s=='%'&&*++s!='%') throw runtime_error("invalid format string"); cout<<*s++; } } template<typename T, typename... Args> void Printf(const char*s, T value, Args... args) { while(*s){ if(*s=='%'&&*++s!='%'){ cout<<value; return Printf(++s, args...); } cout<<*s++; } throw runtime_error("extra arguments provided to Printf"); } int main(){ Printf("hello %s ", string("world")); // hello world }
16、变长模板:进阶
注意到前面的程序,包扩展形式如下:
template<typename... A> class T: private B<A...> {}; // 上述模板 T<X,Y> 将会解包为: class T<X,Y>:private B<X,Y> {}; template<typename... A> class T : private B<A>...{}; // 上述模板 T<X,Y> 将会解包为: class T<X,Y>:private B<X>, private B<Y>{};
也即,解包发生成继承基类模板时。B<A...> pack expasion 发生在<>内。而 B<A>... pack expasion 发生在 <>外。
类型的解包也可以发生在使用模板函数时。如下:
template<truename... T> void DummyWrapper(T... t){} template<typename T> T pr(T t){ cout << t; return t; } template<typename... A> void VTPrint(A... a){ DummyWrapper(pr(a)...); // 包扩展为 pr(1), pr(", "), ..., pr(", abc ") } int main() { VTPrint(1,", ", 1.2, ", abc "); }
C++11中引入 了新操作符“sizeof...”(sizeof后面加上了3个小点),其作用是计算参数包中的参数个数。下面是一个示例:
template<class...A> void Print(A... arg){ assert(false); // 非6参数偏特化都会默认assert(false) } // 特化6参数的版本 void Print(int a1, int a2, int a3, int a4, int a5, int a6){ cout<<a1<<", "<<a2<<", "<<a3<<", " <<a4<<", "<<a5<<", "<<a6<<endl; } template<class...A> int Vaargs(A... args){ int size = sizeof...(A); // 计算变长包的长度 switch(size){ case 0: Print(99, 99, 99, 99, 99, 99); break; case 1: Print(99,99,args...,99,99,99); break; case 2: Print(99,99,args...,99,99); break; case 3: Print(args...,99,99,99); break; case 4: Print(99,args...,99); break; case 5: Print(99,args...); break; case 6: Print(args...); break; case default: Print(0,0,0,0,0,0); } } int main(void) { Vaargs(); // 99,99,99,99,99,99 Vaargs(1); // 99,99,1,99,99,99 Vaargs(1,2); // 99,99,1,2,99,99 Vaargs(1,2,3); // 1,2,3,99,99,99 Vaargs(1,2,3,4); // 99,1,2,3,4,99 Vaargs(1,2,3,4,5); // 99,1,2,3,4,5 Vaargs(1,2,3,4,5,6); // 99,1,2,3,4,5,6 Vaargs(1,2,3,4,5,6,7); // 0,0,0,0,0,0 return 0; }
使用模板做变长模板参数包:
template<typename I, template<typename> class... B> struct Container{}; template<typename I, template<typename> class A, template<typename> class... B> struct Container<I, A,B...>{ A<I> a; Container<I,B...> b; } template<truename I> struct Container<I>{};
下述代码会编译错误,模板参数包不是变长模板类的最后一个参数。
template<class...A, class...B> struct Container{}; template<class...A, class B> struct Container{};