diff --git a/ReadMe.md b/ReadMe.md index 10af4f0..8a85d9b 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1,11 +1,55 @@ - -# C++ Template 进阶指南 - -## 0. 前言 - -### 0.1 C++另类简介:比你用的复杂,但比你想的简单 - -C++似乎从他为世人所知的那天开始便成为天然的话题性编程语言。在它在周围有着形形色色的赞美与贬低之词。当我在微博上透露欲写此文的意愿时,也收到了很多褒贬不一的评论。作为一门语言,能拥有这么多使用并恨着它、使用并畏惧它的用户,也算是语言丛林里的奇观了。 + C++ Template 进阶指南 + ================= + +章节目录由VSCode插件[Markdown All in One](https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one)生成。 +- [1. 前言](#1-前言) + - [1.1. C++另类简介:比你用的复杂,但比你想的简单](#11-c另类简介比你用的复杂但比你想的简单) + - [1.2. 适宜读者群](#12-适宜读者群) + - [1.3. 版权](#13-版权) + - [1.4. 推荐编译环境](#14-推荐编译环境) + - [1.5. 体例](#15-体例) + - [1.5.1. 示例代码](#151-示例代码) + - [1.5.2. 引用](#152-引用) + - [1.6. 意见、建议、喷、补遗、写作计划](#16-意见建议喷补遗写作计划) +- [2. Template的基本语法](#2-template的基本语法) + - [2.1. 什么是模板(Template)](#21-什么是模板template) + - [2.2. 类模板 (Class Template) 的基本语法](#22-类模板-class-template-的基本语法) + - [2.2.1. "模板类"还是"类模板"](#221-模板类还是类模板) + - [2.2.2. Class Template的与成员变量定义](#222-class-template的与成员变量定义) + - [2.2.3. 模板的使用](#223-模板的使用) + - [2.2.4. 类模板的成员函数定义](#224-类模板的成员函数定义) + - [2.3. 函数模板 (Function Template) 入门](#23-函数模板-function-template-入门) + - [2.3.1. 函数模板的声明和定义](#231-函数模板的声明和定义) + - [2.3.2. 函数模板的使用](#232-函数模板的使用) + - [2.4. 整型也可是Template参数](#24-整型也可是template参数) + - [2.5. 模板形式与功能是统一的](#25-模板形式与功能是统一的) +- [3. 模板元编程基础](#3-模板元编程基础) + - [3.1. 编程,元编程,模板元编程](#31-编程元编程模板元编程) + - [3.2. 模板世界的If-Then-Else:类模板的特化与偏特化](#32-模板世界的if-then-else类模板的特化与偏特化) + - [3.2.1. 根据类型执行代码](#321-根据类型执行代码) + - [3.2.2. 特化](#322-特化) + - [3.2.3. 特化:一些其它问题](#323-特化一些其它问题) + - [3.3. 即用即推导](#33-即用即推导) + - [3.3.1. 视若无睹的语法错误](#331-视若无睹的语法错误) + - [3.3.2. 名称查找:I am who I am](#332-名称查找i-am-who-i-am) + - [3.3.3. "多余的" typename 关键字](#333-多余的--typename-关键字) + - [3.4. 本章小结](#34-本章小结) +- [4. 深入理解特化与偏特化](#4-深入理解特化与偏特化) + - [4.1. 正确的理解偏特化](#41-正确的理解偏特化) + - [4.1.1. 偏特化与函数重载的比较](#411-偏特化与函数重载的比较) + - [4.1.2. 不定长的模板参数](#412-不定长的模板参数) + - [4.1.3. 模板的默认实参](#413-模板的默认实参) + - [4.2. 后悔药:SFINAE](#42-后悔药sfinae) + - [4.3. Concept "概念":对模板参数约束的直接描述](#43-concept-概念对模板参数约束的直接描述) + - [4.3.1. "概念" 解决了什么问题](#431-概念-解决了什么问题) + - [4.3.2. "概念"入门](#432-概念入门) +- [5. 未完成章节](#5-未完成章节) + +# 1. 前言 + +## 1.1. C++另类简介:比你用的复杂,但比你想的简单 + +C++似乎从它为世人所知的那天开始便成为天然的话题性编程语言。在它在周围有着形形色色的赞美与贬低之词。当我在微博上透露欲写此文的意愿时,也收到了很多褒贬不一的评论。作为一门语言,能拥有这么多使用并恨着它、使用并畏惧它的用户,也算是语言丛林里的奇观了。 C++之所以变成一门层次丰富、结构多变、语法繁冗的语言,是有着多层次的原因的。Bjarne在《The Design and Evolution of C++》一书中,详细的解释了C++为什么会变成如今(C++98/03)的模样。这本书也是我和陈梓瀚一直对各位已经入门的新手强烈推荐的一本书。通过它你多少可以明白,C++的诸多语法要素之所以变成如今的模样,实属迫不得已。 @@ -19,9 +63,9 @@ C++之所以变成一门层次丰富、结构多变、语法繁冗的语言, 2002年出版的另一本书《C++ Templates》,可以说是在Template方面的集大成之作。它详细阐述了模板的语法、提供了和模板有关的语言细节信息,举了很多有代表性例子。但是对于模板新手来说,这本书细节如此丰富,让他们随随便便就打了退堂鼓缴械投降。 -本文的写作初衷,就是通过"编程语言"的视角,介绍一个简单、清晰的"模板语言"。我会尽可能的将模板的诸多要素连串起来,用一些简单的例子帮助读者学习这门"语言",让读者在编写、阅读模板代码的时候,能像 `if(exp) { dosomething(); }`一样的信手拈来,让"模板元编程"技术成为读者牢固掌握、可举一反三的有用技能。 +本文的写作初衷,就是通过"编程语言"的视角,介绍一个简单、清晰的"模板语言"。我会尽可能地将模板的诸多要素连串起来,用一些简单的例子帮助读者学习这门"语言",让读者在编写、阅读模板代码的时候,能像 `if(exp) { dosomething(); }`一样的信手拈来,让"模板元编程"技术成为读者牢固掌握、可举一反三的有用技能。 -### 0.2 适宜读者群 +## 1.2. 适宜读者群 因为本文并不是用于C++入门,例子中也多少会牵涉一些其它知识,因此如果读者能够具备以下条件,会读起来更加轻松: @@ -29,23 +73,22 @@ C++之所以变成一门层次丰富、结构多变、语法繁冗的语言, * 使用过STL; * 熟悉一些常用的算法,以及递归等程序设计方法。 -此外,尽管第一章会介绍一些Template的基本语法,但是还是会略显单薄。因此也希望读者能对C++ Template最基本语法形式有所了解和掌握;如果会编写基本的模板函数和模板类那就更好了。 +此外,尽管第一章会介绍一些Template的基本语法,但是还是会略显单薄。因此也希望读者能对C++ Template最基本语法形式有所了解和掌握;如果会编写基本的函数模板和类模板那就更好了。 诚如上节所述,本文并不是《C++ Templates》的简单重复,与《Modern C++ Design》交叠更少。从知识结构上,我建议大家可以先读本文,再阅读《C++ Templates》获取更丰富的语法与实现细节,以更进一步;《Modern C++ Design》除了元编程之外,还有很多的泛型编程示例,原则上泛型编程的部分与我所述的内容交叉不大,读者在读完1-3章了解模板的基本规则之后便可阅读《MCD》的相应章节;元编程部分(如Typelist)建议在阅读完本文之后再行阅读,或许会更易理解。 -### 0.3 版权 +## 1.3. 版权 本文是随写随即同步到Github上,因此在行文中难免会遗漏引用。本文绝大部分内容应是直接承出我笔,但是也不定会有他山之石。所有指涉内容我会尽量以引号框记,或在上下文和边角注记中标示,如有遗漏烦请不吝指出。 全文所有为我所撰写的部分,作者均保留所有版权。如果有需要转帖或引用,还请注明出处并告知于我。 -### 0.4 推荐编译环境 +## 1.4. 推荐编译环境 C++编译器众多,且对模板的支持可能存在细微差别。如果没有特别强调,本书行文过程中,使用了下列编译器来测试文中提供的代码和示例: -* Clang 3.7 (x86) -* Visual Studio 2015 Update 3 -* GCC 7 (x86, snapshot) +* Clang 14.0.3; 15.0 (amd64) +* Visual Studio 2022 19.2+ (amd64) 此外,部分复杂实例我们还在文中提供了在线的编译器预览以方便大家阅读和测试。在线编译器参见: [`gcc.godbolt.org`](https://gcc.godbolt.org/)。 @@ -55,9 +98,9 @@ C++编译器众多,且对模板的支持可能存在细微差别。如果没 |---|---| | std::decay_t | C++ 14 | -### 0.5 体例 +## 1.5. 体例 -#### 0.5.1 示例代码 +### 1.5.1. 示例代码 ```C++ void SampleCode() { @@ -65,7 +108,7 @@ void SampleCode() { } ``` -#### 0.5.2 引用 +### 1.5.2. 引用 引用自C++标准: @@ -76,7 +119,7 @@ void SampleCode() { > 《书名》 > 这是一段引用或翻译自其他图书的文字 -### 0.6 意见、建议、喷、补遗、写作计划 +## 1.6. 意见、建议、喷、补遗、写作计划 * 需增加: * 模板的使用动机。 @@ -89,23 +132,27 @@ void SampleCode() { * 比较模板和函数的差异性 * 蓝色:C++14 Return type deduction for normal functions 的分析 -## 1. Template的基本语法 +# 2. Template的基本语法 + +## 2.1. 什么是模板(Template) + +## 2.2. 类模板 (Class Template) 的基本语法 -### 1.1 Template Class基本语法 +### 2.2.1. "模板类"还是"类模板" -#### 1.1.1 Template Class的与成员变量定义 -我们来回顾一下最基本的Template Class声明和定义形式: +### 2.2.2. Class Template的与成员变量定义 +我们来回顾一下最基本的Class Template声明和定义形式: -Template Class声明: +Class Template声明: ```C++ template class ClassA; ``` -Template Class定义: +Class Template定义: ```C++ template class ClassA { - T member; + T member; }; ``` @@ -115,35 +162,35 @@ template class ClassA void foo(int a); ``` -`T`则可以类比为函数形参`a`,这里的"模板形参"`T`,也同函数形参一样取成任何你想要的名字;`typename`则类似于例子中函数参数类型`int`,它表示模板参数中的`T`将匹配一个类型。除了 `typename` 之外,我们再后面还要讲到,整型也可以作为模板的参数。 +`T`则可以类比为函数形参`a`,这里的"模板形参"`T`,也同函数形参一样取成任何你想要的名字;`typename`则类似于例子中函数参数类型`int`,它表示模板参数中的`T`将匹配一个类型。除了 `typename` 之外,我们在后面还要讲到,整型也可以作为模板的参数。 在定义完模板参数之后,便可以定义你所需要的类。不过在定义类的时候,除了一般类可以使用的类型外,你还可以使用在模板参数中使用的类型 `T`。可以说,这个 `T`是模板的精髓,因为你可以通过指定模板实参,将T替换成你所需要的类型。 -例如我们用`ClassA`来实例化模板类ClassA,那么`ClassA`可以等同于以下的定义: +例如我们用`ClassA`来实例化类模板ClassA,那么`ClassA`可以等同于以下的定义: ``` C++ // 注意:这并不是有效的C++语法,只是为了说明模板的作用 typedef class { - int member; + int member; } ClassA; ``` -可以看出,通过模板参数替换类型,可以获得很多形式相同的新类型,有效减少了代码量。这种用法,我们称之为"泛型"(Generic Programming),它最常见的应用,即是STL中的容器模板类。 +可以看出,通过模板参数替换类型,可以获得很多形式相同的新类型,有效减少了代码量。这种用法,我们称之为"泛型"(Generic Programming),它最常见的应用,即是STL中的容器类模板。 -#### 1.1.2 模板的使用 +### 2.2.3. 模板的使用 -对于C++来说,类型最重要的作用之一就是用它去产生一个变量。例如我们定义了一个动态数组(列表)的模板类`vector`,它对于任意的元素类型都具有push_back和clear的操作,我们便可以如下定义这个类: +对于C++来说,类型最重要的作用之一就是用它去产生一个变量。例如我们定义了一个动态数组(列表)的类模板`vector`,它对于任意的元素类型都具有push_back和clear的操作,我们便可以如下定义这个类: ```C++ template class vector { public: - void push_back(T const&); - void clear(); + void push_back(T const&); + void clear(); private: - T* elements; + T* elements; }; ``` @@ -161,17 +208,17 @@ intArray.push_back(5); floatArray.push_back(3.0f); ``` -变量定义的过程可以分成两步来看:第一步,`vector`将`int`绑定到模板类`vector`上,获得了一个"普通的类`vector`";第二步通过"vector"定义了一个变量。 -与"普通的类"不同,模板类是不能直接用来定义变量的。例如 +变量定义的过程可以分成两步来看:第一步,`vector`将`int`绑定到类模板`vector`上,获得了一个"普通的类`vector`";第二步通过"vector"定义了一个变量。 +与"普通的类"不同,类模板是不能直接用来定义变量的 —— 毕竟它的名字是"模板"而不是"类"。例如: ```C++ vector unknownVector; // 错误示例 ``` -这样就是错误的。我们把通过类型绑定将模板类变成"普通的类"的过程,称之为模板实例化(Template Instantiate)。实例化的语法是: +这样就是错误的。我们把通过类型绑定将类模板变成"普通的类"的过程,称之为模板实例化(Template Instantiate)。实例化的语法是: ``` -模板名 < 模板实参1 [,模板实参2,...]> +模板名 < [模板实参1,模板实参2,...]> ``` 看几个例子: @@ -190,9 +237,9 @@ ClassB 当然,在实例化过程中,被绑定到模板参数上的类型(即模板实参)需要与模板形参正确匹配。 就如同函数一样,如果没有提供足够并匹配的参数,模板便不能正确的实例化。 -#### 1.1.3 模板类的成员函数定义 +### 2.2.4. 类模板的成员函数定义 -由于C++11正式废弃"模板导出"这一特性,因此在模板类的变量在调用成员函数的时候,需要看到完整的成员函数定义。因此现在的模板类中的成员函数,通常都是以内联的方式实现。 +由于C++11正式废弃"模板导出"这一特性,因此在类模板的变量在调用成员函数的时候,需要看到完整的成员函数定义。因此现在的类模板中的成员函数,通常都是以内联的方式实现。 例如: ``` C++ @@ -217,15 +264,15 @@ template class vector { public: - void clear(); // 注意这里只有声明 + void clear(); // 注意这里只有声明 private: T* elements; }; template -void vector::clear() // 函数的实现放在这里 +void vector::clear() // 函数的实现放在这里 { - // Function body + // Function body } ``` @@ -253,11 +300,11 @@ void vector /*看起来像偏特化*/ ::clear() // 函数的实现放在这 } ``` -### 1.2 Template Function的基本语法 +## 2.3. 函数模板 (Function Template) 入门 -#### 1.2.1 Template Function的声明和定义 +### 2.3.1. 函数模板的声明和定义 -模板函数的语法与模板类基本相同,也是以关键字`template`和模板参数列表作为声明与定义的开始。模板参数列表中的类型,可以出现在参数、返回值以及函数体中。比方说下面几个例子 +函数模板的语法与类模板基本相同,也是以关键字`template`和模板参数列表作为声明与定义的开始。模板参数列表中的类型,可以出现在参数、返回值以及函数体中。比方说下面几个例子 ```C++ template void foo(T const& v); @@ -295,11 +342,11 @@ template void foo() 举个例子:generic typed function ‘add’ ``` -在我遇到的朋友中,即便如此对他解释了模板,即便他了解了模板,也仍然会对模板产生畏难情绪。毕竟从形式上来说,模板类和模板函数都要较非模板的版本更加复杂,阅读代码所需要理解的内容也有所增多。 +在我遇到的朋友中,即便如此对他解释了模板,即便他了解了模板,也仍然会对模板产生畏难情绪。毕竟从形式上来说,模板化的类和模板化的函数都要较非模板的版本更加复杂,阅读代码所需要理解的内容也有所增多。 如何才能克服这一问题,最终视模板如平坦代码呢? -答案只有一个:无他,唯手熟尔。 +答案只有一个:**无他,唯手熟尔**。 在学习模板的时候,要反复做以下的思考和练习: @@ -309,7 +356,7 @@ template void foo() 3. 把解决方案用代码写出来。 - 4. 如果失败了,找到原因。是知识有盲点(例如不知道怎么将 `T&` 转化成 `T`),还是不可行(比如试图利用浮点常量特化模板类,但实际上这样做是不可行的)? + 4. 如果失败了,找到原因。是知识有盲点(例如不知道怎么将 `T&` 转化成 `T`),还是不可行(比如试图利用浮点常量特化类模板,但实际上这样做是不可行的)? 通过重复以上的练习,应该可以对模板的语法和含义都有所掌握。如果提出问题本身有困难,或许下面这个经典案例可以作为你思考的开始: @@ -319,7 +366,7 @@ template void foo() 当然和"设计模式"一样,模板在实际应用中,也会有一些固定的需求和解决方案。比较常见的场景包括:泛型(最基本的用法)、通过类型获得相应的信息(型别萃取)、编译期间的计算、类型间的推导和变换(从一个类型变换成另外一个类型,比如boost::function)。这些本文在以后的章节中会陆续介绍。 -#### 1.2.2 模板函数的使用 +### 2.3.2. 函数模板的使用 我们先来看一个简单的函数模板,两个数相加: @@ -365,7 +412,7 @@ int b = 3; int result = Add(a, b); ``` -编译器会心领神会的将 `Add` 变成 `Add`。但是编译器不能面对模棱两可的答案。比如你这么写的话呢? +编译器会心领神会地将 `Add` 变成 `Add`。但是编译器不能面对模棱两可的答案。比如你这么写的话呢? ``` C++ int a = 5; @@ -380,7 +427,7 @@ int result = Add(a, b); error C2782: 'T _1_2_2::Add(T,T)' : template parameter 'T' is ambiguous ``` -好吧,"ambigous",这个提示再明确不过了。 +好吧,"ambiguous",这个提示再明确不过了。 不过,只要你别逼得编译器精神分裂的话,编译器其实是非常聪明的,它可以从很多的蛛丝马迹中,猜测到你真正的意图,有如下面的例子: @@ -390,7 +437,7 @@ template class A {}; template T foo( A v ); A v; -foo(v); // 它能准确的猜到 T 是 int. +foo(v); // 它能准确地猜到 T 是 int. ``` 咦,编译器居然绕过了A这个外套,猜到了 `T` 匹配的是 `int`。编译器是怎么完成这一"魔法"的,我们暂且不表,2.2节时再和盘托出。 @@ -418,7 +465,7 @@ int b = GetValue(1); 嗯,是不是so easy啊?嗯,你又信心满满的做了一个练习: -你要写一个模板函数叫 `c_style_cast`,顾名思义,执行的是C风格的转换。然后出于方便起见,你希望它能和 `static_cast` 这样的内置转换有同样的写法。于是你写了一个use case。 +你要写一个函数模板叫 `c_style_cast`,顾名思义,执行的是C风格的转换。然后出于方便起见,你希望它能和 `static_cast` 这样的内置转换有同样的写法。于是你写了一个use case。 ``` C++ DstT dest = c_style_cast(src); @@ -452,7 +499,7 @@ float i = c_style_cast(v); 嗯,很顺利的通过了。难道C++不能支持让参数推导一部分模板参数吗? -当然是可以的。只不过在部分推导、部分指定的情况下,编译器对模板参数的顺序是有限制的:先写需要指定的模板参数,再把能推导出来的模板参数放在后面。 +当然是可以的。只不过在部分推导、部分指定的情况下,编译器对模板参数的顺序是有限制的:**先写需要指定的模板参数,再把能推导出来的模板参数放在后面**。 在这个例子中,能推导出来的是 `SrcT`,需要指定的是 `DstT`。把函数模板写成下面这样就可以了: @@ -466,9 +513,9 @@ int v = 0; float i = c_style_cast(v); // 形象地说,DstT会先把你指定的参数吃掉,剩下的就交给编译器从函数参数列表中推导啦。 ``` -### 1.3 整型也可是Template参数 +## 2.4. 整型也可是Template参数 -模板参数除了类型外(包括基本类型、结构、类类型等),也可以是一个整型数(Integral Number)。这里的整型数比较宽泛,包括布尔、不同位数、有无符号的整型,甚至包括指针。我们将整型的模板参数和类型作为模板参数来做一个对比: +模板参数除了类型外(包括基本类型、结构、类类型等),也可以是一个整型数(Integral Number)。这里的整型数比较宽泛,包括布尔型,不同位数、有无符号的整型,甚至包括指针。我们将整型的模板参数和类型作为模板参数来做一个对比: ``` C++ template class TemplateWithType; @@ -499,7 +546,7 @@ class IntArrayWithSize16 IntArrayWithSize16 arr; ``` -其中有一点要注意的是,因为模板的匹配是在编译的时候完成的,所以实例化模板的时候所使用的参数,也必须要在编译期就能确定。例如以下的例子编译器就会报错: +其中有一点需要注意,因为模板的匹配是在编译的时候完成的,所以实例化模板的时候所使用的参数,也必须要在编译期就能确定。例如以下的例子编译器就会报错: ``` C++ template class A {}; @@ -536,9 +583,9 @@ void foo() { A<5> a; B<7, A<5>, nullptr> b; // 模板参数可以是一个无符号八位整数,可以是模板生成的类;可以是一个指针。 - C c; // 模板参数可以是一个bool类型的常量,甚至可以是一个函数指针。 - D<&a<3>::foo> d; // 丧心病狂啊!它还能是一个成员函数指针! - int x = Add<3>(5); // x == 8。因为整型模板参数无法从函数参数获得,所以只能是手工指定啦。 + C c; // 模板参数可以是一个bool类型的常量,甚至可以是一个函数指针。 + D<&a<3>::foo> d; // 丧心病狂啊!它还能是一个成员函数指针! + int x = Add<3>(5); // x == 8。因为整型模板参数无法从函数参数获得,所以只能是手工指定啦。 } template class E {}; // ERROR: 别闹!早说过只能是整数类型的啦! @@ -546,20 +593,20 @@ template class E {}; // ERROR: 别闹!早说过只能是整数类型 当然,除了单纯的用作常数之外,整型参数还有一些其它的用途。这些"其它"用途最重要的一点是让类型也可以像整数一样运算。《Modern C++ Design》给我们展示了很多这方面的例子。不过你不用急着去阅读那本天书,我们会在做好足够的知识铺垫后,让你轻松学会这些招数。 -### 1.4 模板形式与功能是统一的 +## 2.5. 模板形式与功能是统一的 第一章走马观花的带着大家复习了一下C++ Template的基本语法形式,也解释了包括 `typename` 在内,类/函数模板写法中各个语法元素的含义。形式是功能的外在体现,介绍它们也是为了让大家能理解到,模板之所以写成这种形式是有必要的,而不是语言的垃圾成分。 从下一章开始,我们便进入了更加复杂和丰富的世界:讨论模板的匹配规则。其中有令人望而生畏的特化与偏特化。但是,请相信我们在序言中所提到的:将模板作为一门语言来看待,它会变得有趣而简单。 -## 2. 模板元编程基础 -### 2.1 编程,元编程,模板元编程 +# 3. 模板元编程基础 +## 3.1. 编程,元编程,模板元编程 技术的学习是一个登山的过程。第一章是最为平坦的山脚道路。而从这一章开始,则是正式的爬坡。无论是我写作还是你阅读,都需要付出比第一章更多的代价。那么问题就是,付出更多的精力学习模板是否值得? -这个问题很功利,但是一阵见血。因为技术的根本目的在于解决需求。那C++的模板能做什么? +这个问题很功利,但是一针见血。因为技术的根本目的在于解决需求。那C++的模板能做什么? -一个高(树)大(新)上(蜂)的回答是,C++里面的模板,犹如C中的宏、C#和Java中的自省(restropection)和反射(reflection)一样,是一个改变语言内涵,拓展语言外延的存在。 +一个高(树)大(新)上(风)的回答是,C++里面的模板,犹如C中的宏、C和Java中的自省(restropection)和反射(reflection),是一个改变语言内涵,拓展语言外延的存在。 程序最根本的目的是什么?复现真实世界或人所构想的规律,减少重复工作的成本,或通过提升规模完成人所不能及之事。但是世间之事万千,有限的程序如何重现复杂的世界呢? @@ -579,11 +626,11 @@ template class E {}; // ERROR: 别闹!早说过只能是整数类型 class StackInt { public: - void push(Int v); - Int pop(); - Int Find(Int x) + void push(int v); + int pop(); + int Find(int x) { - for(Int i = 1; i <= size; ) + for(int i = 0; i < size; ++i) { if(data[i] == x) { return i; } } @@ -598,11 +645,11 @@ public: class StackFloat { public: - void push(Float v); - Float pop(); - Int Find(Float x) + void push(float v); + float pop(); + int Find(float x) { - for(Int i = 1; i <= size; ) + for(int i = 0; i < size; ++i) { if(data[i] == x) { return i; } } @@ -634,9 +681,9 @@ class Stack public: void push(T v); T pop(); - Int Find(T x) + int Find(T x) { - for(Int i = 0; i <= size; ++i) + for(int i = 0; i < size; ++i) { if(data[i] == x) { return i; } } @@ -648,13 +695,13 @@ typedef Stack StackInt; typedef Stack StackFloat; ``` -通过模板,我们可以将形形色色的堆栈代码分为两个部分,一个部分是不变的接口,以及近乎相同的实现;另外一部分是元素的类型,它们是需要变化的。因此同函数类似,需要变化的部分,由模板参数来反应;不变的部分,则是模板内的代码。可以看到,使用模板的代码,要比不使用模板的代码简洁许多。 +通过模板,我们可以将形形色色的堆栈代码分为两个部分,一个部分是不变的接口,以及近乎相同的实现;另外一部分是元素的类型,它们是需要变化的。因此同函数类似,需要变化的部分,由模板参数来反映;不变的部分,则是模板内的代码。可以看到,使用模板的代码,要比不使用模板的代码简洁许多。 -如果元编程中所有的变化的量(或者说元编程的参数),都是类型,那么这样的编程,我们有个特定的称呼,叫"泛型"。 +如果元编程中所有变化的量(或者说元编程的参数),都是类型,那么这样的编程,我们有个特定的称呼,叫"泛型"。 但是你会问,模板的发明,仅仅是为了做和宏几乎一样的替换工作吗?可以说是,也可以说不是。一方面,很多时候模板就是为了替换类型,这个时候作用上其实和宏没什么区别。只是宏是基于文本的替换,被替换的文本本身没有任何语义。只有替换完成,编译器才能进行接下来的处理。而模板会在分析模板时以及实例化模板时时候都会进行检查,而且源代码中也能与调试符号一一对应,所以无论是编译时还是运行时,排错都相对简单。 -但是模板也和宏有很大的不同,否则此文也就不能成立了。模板最大的不同在于它是"可以运算"的。我们来举一个例子,不过可能有点牵强。考虑我们要写一个向量逐分量乘法。只不过这个向量,它非常的大。所以为了保证速度,我们需要使用SIMD指令进行加速。假设我们有以下指令可以使用: +但是模板和宏也有很大的不同,否则此文也就不能成立了。模板最大的不同在于它是"可以运算"的。我们来举一个例子,不过可能有点牵强。考虑我们要写一个向量逐分量乘法。只不过这个向量,它非常的大。所以为了保证速度,我们需要使用SIMD指令进行加速。假设我们有以下指令可以使用: ``` Int8,16: N/A @@ -668,11 +715,11 @@ Float : VInt64Mul(floatx2, floatx2) for(v4a, v4b : vectorsA, vectorsB) { if type is Int8, Int16 - VInt32Mul( ConvertToInt32(v4a), ConvertToInt32(v4b) ) - elif type is Int32 - VInt32Mul( v4a, v4b ) - elif type is Float - ... + VInt32Mul( ConvertToInt32(v4a), ConvertToInt32(v4b) ) + elif type is Int32 + VInt32Mul( v4a, v4b ) + elif type is Float + ... } ``` @@ -700,9 +747,9 @@ for(v4a, v4b : vectorsA, vectorsB) 好吧,我承认这个例子还是太牵强了。不过相信我,在你阅读完第二章和第三章之后,你会将这些特性自如地运用到你的程序之中。你的程序将会变成体现模板"可运算"威力的最好例子。 -### 2.2 模板世界的If-Then-Else:类模板的特化与偏特化 +## 3.2. 模板世界的If-Then-Else:类模板的特化与偏特化 -#### 2.2.1 根据类型执行代码 +### 3.2.1. 根据类型执行代码 前一节的示例提出了一个要求:需要做出根据类型执行不同代码。要达成这一目的,模板并不是唯一的途径。比如之前我们所说的重载。如果把眼界放宽一些,虚函数也是根据类型执行代码的例子。此外,在C语言时代,也会有一些技法来达到这个目的,比如下面这个例子,我们需要对两个浮点做加法, 或者对两个整数做乘法: ``` C @@ -736,7 +783,7 @@ Variant addFloatOrMulInt(Variant const* a, Variant const* b) 更常见的是 `void*`: ``` C++ -#define BIN_OP(type, a, op, b, result) (*(type *)(result)) = (*(type const *)(a)) op (*(type const*)(b)) +define BIN_OP(type, a, op, b, result) (*(type *)(result)) = (*(type const *)(a)) op (*(type const*)(b)) void doDiv(void* out, void const* data0, void const* data1, DATA_TYPE type) { if(type == TYPE_INT) @@ -791,7 +838,7 @@ int main() } ``` -这点限制也粉碎了妄图用模板来包办工厂(Factory)甚至是反射的梦想。尽管在《Modern C++ Design》中(别问我为什么老举这本书,因为《C++ Templates》和《Generic Programming》我只是囫囵吞枣读过,基本不记得了)大量运用模板来简化工厂方法;同时C++11和14中的一些机制如Variadic Template更是让这一问题的解决更加彻底。但无论如何,直到C++11/14,光靠模板你就是写不出依靠类名或者ID变量产生类型实例的代码。 +这点限制也粉碎了妄图用模板来包办工厂(Factory)甚至是反射的梦想。尽管在《Modern C++ Design》中(别问我为什么老举这本书,因为《C++ Templates》和《Generic Programming》我只是囫囵吞枣读过,基本不记得了)大量运用模板来简化工厂方法;同时C++11/14中的一些机制如Variadic Template更是让这一问题的解决更加彻底。但无论如何,直到C++11/14,光靠模板你就是写不出依靠类名或者ID变量产生类型实例的代码。 所以说,从能力上来看,模板能做的事情都是编译期完成的。编译期完成的意思就是,当你编译一个程序的时候,所有的量就都已经确定了。比如下面的这个例子: @@ -807,7 +854,7 @@ Variant result = addFloatOrMulInt(aVar, bVar); 在模板代码中,这个"合适的机制"就是指"特化"和"部分特化(Partial Specialization)",后者也叫"偏特化"。 -#### 2.2.2 特化 +### 3.2.2. 特化 我的高中物理老师对我说过一句令我受用至今的话:把自己能做的事情做好。编写模板程序也是一样。当你试图用模板解决问题之前,先撇开那些复杂的语法要素,用最直观的方式表达你的需求: @@ -877,7 +924,7 @@ void foo() z = AddFloatOrMulInt::Do(x, y); // z = x * y; } ``` -也许你不明白为什么要改写成现在这个样子。看不懂不怪你,怪我讲的不好。但是你别急,先看看这样改写以后能不能跟我们的目标接近一点。如果我们把 `AddFloatOrMulInt::Do` 看作一个普通的函数,那么我们可以写两个实现出来: +也许你不明白为什么要改写成现在这个样子。看不懂不怪你,怪我讲得不好。但是你别急,先看看这样改写以后能不能跟我们的目标接近一点。如果我们把 `AddFloatOrMulInt::Do` 看作一个普通的函数,那么我们可以写两个实现出来: ``` C++ float AddFloatOrMulInt::Do(float a, float b) @@ -979,7 +1026,7 @@ void foo() template class AddFloatOrMulInt; // 但是这个类,是给T是Int的时候用的,于是我们写作 -class AddFloatOrMulInt +class AddFloatOrMulInt; // 当然,这里编译是通不过的。 // 但是它又不是个普通类,而是类模板的一个特化(特例)。 @@ -992,7 +1039,7 @@ template class AddFloatOrMulInt; template class AddFloatOrMulInt { // ... 针对Int的实现 ... -} +}; // Bingo! ``` @@ -1026,7 +1073,7 @@ void PrintID() 如果你体味到了这一点,那么恭喜你,你的模板元编程已经开悟了。 -#### 2.2.3 特化:一些其它问题 +### 3.2.3. 特化:一些其它问题 在上一节结束之后,你一定做了许多的练习。我们再来做三个练习。第一,给`float`一个ID;第二,给`void*`一个ID;第三,给任意类型的指针一个ID。先来做第一个: @@ -1037,15 +1084,17 @@ void PrintID() template class TypeToID { +public: static int const ID = 0xF10A7; }; ``` -嗯, 这个你已经了然于心了。那么`void*`呢?你想了想,这已经是一个复合类型了。不错你还是战战兢兢的写了下来: +嗯, 这个你已经了然于心了。那么`void*`呢?你想了想,这已经是一个复合类型了。不错你还是战战兢兢地写了下来: ``` C++ template class TypeToID { +public: static int const ID = 0x401d; }; @@ -1060,13 +1109,13 @@ void PrintID() ``` C++ class ClassB {}; -template class TypeToID; // 函数的TypeID -template class TypeToID; // 数组的TypeID +template class TypeToID; // 函数的TypeID +template class TypeToID; // 数组的TypeID template class TypeToID; // 这是以数组为参数的函数的TypeID template class TypeToID; // 我也不知道这是什么了,自己看着办吧。 ``` -甚至连 `const` 和 `volatile` 都能装进去 +甚至连 `const` 和 `volatile` 都能装进去: ``` C++ template class TypeToID; @@ -1083,7 +1132,7 @@ void PrintID() 嗯,它输出的是-1。我们顺藤摸瓜会看到, `TypeToID`的类模板"原型"的ID是值就是-1。通过这个例子可以知道,当模板实例化时提供的模板参数不能匹配到任何的特化形式的时候,它就会去匹配类模板的"原型"形式。 -不过这里有一个问题要厘清一下。和继承不同,类模板的"原型"和它的特化类在实现上是没有关系的,并不是在类模板中写了 `ID` 这个Member,那所有的特化就必须要加入 `ID` 这个Member,或者特化就自动有了这个成员。完全没这回事。我们把类模板改成以下形式,或许能看的更清楚一点: +不过这里有一个问题要理清一下。和继承不同,类模板的"原型"和它的特化类在实现上是没有关系的,并不是在类模板中写了 `ID` 这个Member,那所有的特化就必须要加入 `ID` 这个Member,或者特化就自动有了这个成员。完全没这回事。我们把类模板改成以下形式,或许能看的更清楚一点: ``` C++ template class TypeToID @@ -1100,9 +1149,9 @@ public: void PrintID() { - cout << "ID of float: " << TypeToID::ID << endl; // Print "1" + cout << "ID of float: " << TypeToID::ID << endl; // Print "1" cout << "NotID of float: " << TypeToID::NotID << endl; // Error! TypeToID使用的特化的类,这个类的实现没有NotID这个成员。 - cout << "ID of double: " << TypeToID::ID << endl; // Error! TypeToID是由模板类实例化出来的,它只有NotID,没有ID这个成员。 + cout << "ID of double: " << TypeToID::ID << endl; // Error! TypeToID是由类模板实例化出来的,它只有NotID,没有ID这个成员。 } ``` @@ -1138,13 +1187,13 @@ void copy(void* dst, void const* src, size_t elemSize, size_t elemCount, void (* template ``` -接下来,我们要写函数原型: +接下来,我们要写函数原型: ``` C++ void copy(?? dest, ?? src, size_t elemCount); ``` -这里的 `??` 要怎么写呢?既然我们有了模板类型参数T,那我们不如就按照经验,写 `T*` 看看。 +这里的 `??` 要怎么写呢?既然我们有了模板类型参数T,那我们不如就按照经验,写 `T*` 看看。 ``` C++ template @@ -1175,11 +1224,11 @@ template // 嗯,需要一个T class TypeToID // 我要对所有的指针类型特化,所以这里就写T* { public: - static int const ID = 0x80000000; // 用最高位表示它是一个指针 + static int const ID = 0x80000000; // 用最高位表示它是一个指针 }; ``` -最后写个例子来测试一下,看看我们的 `T*` 能不能搞定 `float*` +最后写个例子来测试一下,看看我们的 `T*` 能不能搞定 `float*`: ``` C++ void PrintID() @@ -1205,7 +1254,7 @@ public: void PrintID() { - cout << "ID of float*: " << TypeToID< TypeToID::SameAsT>::ID << endl; + cout << "ID of float*: " << TypeToID< TypeToID::SameAsT>::ID << endl; } ``` @@ -1221,14 +1270,15 @@ OK,猜出来了吗,T是`float`。为什么呢?因为你用 `float *` 匹 template class RemovePointer { - // 啥都不干,你要放一个不是指针的类型进来,我就让你死的难看。 +public: + typedef T Result; // 如果放进来的不是一个指针,那么它就是我们要的结果。 }; template class RemovePointer // 祖传牛皮藓,专治各类指针 { public: - typedef T Result; + typedef T Result; // 正如我们刚刚讲的,去掉一层指针,把 T* 这里的 T 取出来。 }; void Foo() @@ -1238,7 +1288,23 @@ void Foo() } ``` -OK,如果这个时候,我需要给 `int*` 提供一个更加特殊的特化,那么我还得都多提供一个: +当然啦,这里我们实现的不算是真正的 `RemovePointer`,因为我们只去掉了一层指针。而如果传进来的是类似 `RemovePointer` 这样的东西呢?是的没错,去掉一层之后还是一个指针。`RemovePointer::Result` 应该是一个 `int*`,要怎么才能实现我们想要的呢?聪明的你一定能想到:只要像剥洋葱一样,一层一层一层地剥开,不就好了吗!相应地我们应该怎么实现呢?可以把 `RemovePointer` 的特化版本改成这样(当然如果有一些不明白的地方你可以暂时跳过,接着往下看,很快就会明白的): + +``` C++ +template +class RemovePointer +{ +public: + // 如果是传进来的是一个指针,我们就剥夺一层,直到指针形式不存在为止。 + // 例如 RemovePointer,Result 是 RemovePointer::Result, + // 而 RemovePointer::Result 又是 int,最终就变成了我们想要的 int,其它也是类似。 + typedef typename RemovePointer::Result Result; +}; +``` + +是的没错,这便是我们想要的 `RemovePointer` 的样子。类似的你还可以试着实现 `RemoveConst`, `AddPointer` 之类的东西。 + +OK,回到我们之前的话题,如果这个时候,我需要给 `int*` 提供一个更加特殊的特化,那么我还得多提供一个: ``` C++ // ... @@ -1254,7 +1320,7 @@ public: }; template // 嗯,int* 已经是个具体的不能再具体的类型了,所以模板不需要额外的类型参数了 -class TypeToID // 嗯,对int*的特化。在这里呢,要把int*整体看作一个类型。 +class TypeToID // 嗯,对int*的特化。在这里呢,要把int*整体看作一个类型 { public: static int const ID = 0x12345678; // 给一个缺心眼的ID @@ -1268,13 +1334,13 @@ void PrintID() 嗯,这个时候它会输出0x12345678的十进制(大概?)。 可能会有较真的人说,`int*` 去匹配 `T` 或者 `T*`,也是合法的。就和你说22岁以上能结婚,那24岁当然也能结婚一样。 -那为什么 `int*` 就会找 `int*`,`float *`因为没有合适的特化就去找 `T*`,更一般的就去找 `T` 呢?废话,有专门为你准备的东西的不用,人干事?这就是直觉。 +那为什么 `int*` 就会找 `int*`,`float *`因为没有合适的特化就去找 `T*`,更一般的就去找 `T` 呢?废话,有专门为你准备的东西你不用,非要自己找事?这就是直觉。 但是呢,直觉对付更加复杂的问题还是没用的(也不是没用,主要是你没这个直觉了)。我们要把这个直觉,转换成合理的规则——即模板的匹配规则。 -当然,这个匹配规则是对复杂问题用的,所以我们会到实在一眼看不出来的时候才会动用它。一开始我们只要把握:模板是从最特殊到最一般形式进行匹配就可以了。 +当然,这个匹配规则是对复杂问题用的,所以我们会到实在一眼看不出来的时候才会动用它。一开始我们只要把握:**模板是从最特殊到最一般形式进行匹配的** 就可以了。 -### 2.3 即用即推导 +## 3.3. 即用即推导 -#### 2.3.1 视若无睹的语法错误 +### 3.3.1. 视若无睹的语法错误 这一节我们将讲述模板一个非常重要的行为特点:那就是什么时候编译器会对模板进行推导,推导到什么程度。 这一知识,对于理解模板的编译期行为、以及修正模板编译错误都非常重要。 @@ -1286,9 +1352,9 @@ template struct X {}; template struct Y { - typedef X ReboundType; // 类型定义1 + typedef X ReboundType; // 类型定义1 typedef typename X::MemberType MemberType; // 类型定义2 - typedef UnknownType MemberType3; // 类型定义3 + typedef UnknownType MemberType3; // 类型定义3 void foo() { @@ -1310,10 +1376,10 @@ template struct Y 这时我们就需要请出C++11标准 —— 中的某些概念了。这是我们到目前为止第一次参阅标准。我希望能尽量减少直接参阅标准的次数,因此即便是极为复杂的模板匹配决议我都暂时没有引入标准中的描述。 然而,Template引入的"双阶段名称查找(Two phase name lookup)"堪称是C++中最黑暗的角落 —— 这是LLVM的团队自己在博客上说的 —— 因此在这里,我们还是有必要去了解标准中是如何规定的。 -#### 2.3.2 名称查找:I am who I am +### 3.3.2. 名称查找:I am who I am 在C++标准中对于"名称查找(name lookup)"这个高大上的名词的诠释,主要集中出现在三处。第一处是3.4节,标题名就叫"Name Lookup";第二处在10.2节,继承关系中的名称查找;第三处在14.6节,名称解析(name resolution)。 -名称查找/名称解析,是编译器的基石。对编译原理稍有了解的人,都知道"符号表"的存在即重要意义。考虑一段最基本的C代码: +名称查找/名称解析,是编译器的基石。对编译原理稍有了解的人,都知道"符号表"的存在及重要意义。考虑一段最基本的C代码: ``` C int a = 0; int b; @@ -1323,9 +1389,9 @@ printf("Result: %d", b); 在这段代码中,所有出现的符号可以分为以下几类: * `int`:类型标识符,代表整型; -* `a`,`b`,`printf`:变量名或函数名; -* `=`,`+`,`*`:运算符; -* `,`,`;`,`(`,`)`:分隔符; +* `a`, `b`, `printf`:变量名或函数名; +* `=`, `+`, `*`:运算符; +* `,`, `;`, `(`, `)`:分隔符; 那么,编译器怎么知道`int`就是整数类型,`b=(a+1)*2`中的`a`和`b`就是整型变量呢?这就是名称查找/名称解析的作用:它告诉编译器,这个标识符(identifer)是在哪里被声明或定义的,它究竟是什么意思。 @@ -1425,8 +1491,8 @@ template struct X { 接下来,我们就来解决2.3.1节中留下的几个问题。 -先看第四个问题。为什么MSVC中,模板函数的定义内不管填什么编译器都不报错?因为MSVC在分析模板中成员函数定义时没有做任何事情。至于为啥连"大王叫我来巡山"都能过得去,这是C++语法/语义分析的特殊性导致的。 -C++是个非常复杂的语言,以至于它的编译器,不可能通过词法-语法-语义多趟分析清晰分割。因为它的语义将会直接干扰到语法: +先看第四个问题。为什么MSVC中,函数模板的定义内不管填什么编译器都不报错?因为MSVC在分析模板中成员函数定义时没有做任何事情。至于为啥连"大王叫我来巡山"都能过得去,这是C++语法/语义分析的特殊性导致的。 +C++是个非常复杂的语言,以至于它的编译器,不可能通过词法-语法-语义多趟分析清晰分割,因为它的语义将会直接干扰到语法: ```C++ void foo(){ @@ -1444,7 +1510,7 @@ void foo(){ // ----------- X.h ------------ template struct X { - // 实现代码 + // 实现代码 }; // ---------- X.cpp ----------- @@ -1458,7 +1524,7 @@ X xf; 此时如果X中有一些与模板参数无关的错误,如果名称查找/语义分析在两个阶段完成,那么这些错误会很早、且唯一的被提示出来;但是如果一切都在实例化时处理,那么可能会导致不同的实例化过程提示同样的错误。而模板在运用过程中,往往会产生很多实例,此时便会大量报告同样的错误。 当然,MSVC并不会真的这么做。根据推测,最终他们是合并了相同的错误。因为即便对于模板参数相关的编译错误,也只能看到最后一次实例化的错误信息: -``` +```C++ template struct X {}; template struct Y @@ -1545,7 +1611,7 @@ error: variable has incomplete type 'A' 1 error generated. ``` -符合标准的写法需要将模板类的定义,和模板函数的定义分离开: +符合标准的写法需要将类模板的定义,和函数模板的定义分离开: > TODO 此处例子不够恰当,并且描述有歧义。需要在未来版本中修订。 @@ -1568,11 +1634,11 @@ void main() { } ``` -但是其实我们知道,`foo`要到实例化之后,才需要真正的做语义分析。在MSVC上,因为函数实现就是到模板实例化时才处理的,所以这个例子是完全正常工作的。因此在上面这个例子中,MSVC的实现要比标准更加易于写和维护,是不是有点写Java/C#那种声明实现都在同一处的清爽感觉了呢! +但是其实我们知道,`foo`要到实例化之后,才需要真正的做语义分析。在MSVC上,因为函数实现就是到模板实例化时才处理的,所以这个例子是完全正常工作的。因此在上面这个例子中,MSVC的实现要比标准更加易于写和维护,是不是有点写Java/C那种声明实现都在同一处的清爽感觉了呢! 扩展阅读: [The Dreaded Two-Phase Name Lookup][2] -#### 2.3.3 "多余的" typename 关键字 +### 3.3.3. "多余的" typename 关键字 到了这里,2.3.1 中提到的四个问题,还有三个没有解决: @@ -1613,7 +1679,24 @@ template struct Y 事实上,标准对`typename`的使用规定极为复杂,也算是整个模板中的难点之一。如果想了解所有的标准,需要阅读标准14.6节下2-7条,以及14.6.2.1第一条中对于`current instantiation`的解释。 -简单来说,如果编译器能在出现的时候知道它的类型,那么就不需要`typename`,如果必须要到实例化的时候才能知道它是不是合法,那么定义的时候就把这个名称作为变量而不是类型。 +简单来说,如果编译器能在出现的时候知道它是一个类型,那么就不需要`typename`,如果必须要到实例化的时候才能知道它是不是合法,那么定义的时候就把这个名称作为变量而不是类型。 + +我们用一行代码来说明这个问题: + +```C++ +a * b; +``` + +在没有模板的情况下,这个语句有两种可能的意思:如果`a`是一个类型,这就是定义了一个指针`b`,它拥有类型`a*`;如果`a`是一个对象或引用,这就是计算一个表达式`a*b`,虽然结果并没有保存下来。可是如果上面的`a`是模板参数的成员,会发生什么呢? + +```C++ +template void meow() +{ + T::a * b; // 这是指针定义还是表达式语句? +} +``` + +编译器对模板进行语法检查的时候,必须要知道上面那一行到底是个什么——这当然可以推迟到实例化的时候进行(比如VC,这也是上面说过VC可以不加`typename`的原因),不过那是另一个故事了——显然在模板定义的时候,编译器并不能妄断。因此,C++标准规定,在没有`typename`约束的情况下认为这里`T::a`不是类型,因此`T::a * b;` 会被当作表达式语句(例如乘法);而为了告诉编译器这是一个指针的定义,我们必须在`T::a`之前加上`typename`关键字,告诉编译器`T::a`是一个类型,这样整个语句才能符合指针定义的语法。 在这里,我举几个例子帮助大家理解`typename`的用法,这几个例子已经足以涵盖日常使用[(预览)][3]: @@ -1621,32 +1704,32 @@ template struct Y struct A; template struct B; template struct X { - typedef X _A; // 编译器当然知道 X 是一个类型。 - typedef X _B; // X 等价于 X 的缩写 - typedef T _C; // T 不是一个类型还玩毛 + typedef X TA; // 编译器当然知道 X 是一个类型。 + typedef X TB; // X 等价于 X 的缩写 + typedef T TC; // T 不是一个类型还玩毛 // !!!注意我要变形了!!! class Y { - typedef X _D; // X 的内部,既然外部高枕无忧,内部更不用说了 - typedef X::Y _E; // 嗯,这里也没问题,编译器知道Y就是当前的类型, + typedef X TD; // X 的内部,既然外部高枕无忧,内部更不用说了 + typedef X::Y TE; // 嗯,这里也没问题,编译器知道Y就是当前的类型, // 这里在VS2015上会有错,需要添加 typename, // Clang 上顺利通过。 - typedef typename X::Y _F; // 这个居然要加 typename! + typedef typename X::Y TF; // 这个居然要加 typename! // 因为,X和X不一样哦, // 它可能会在实例化的时候被别的偏特化给抢过去实现了。 }; - typedef A _G; // 嗯,没问题,A在外面声明啦 - typedef B _H; // B也是一个类型 - typedef typename B::type _I; // 嗯,因为不知道B::type的信息, + typedef A TG; // 嗯,没问题,A在外面声明啦 + typedef B TH; // B也是一个类型 + typedef typename B::type TI; // 嗯,因为不知道B::type的信息, // 所以需要typename - typedef B::type _J; // B 不依赖模板参数, + typedef B::type TJ; // B 不依赖模板参数, // 所以编译器直接就实例化(instantiate)了 // 但是这个时候,B并没有被实现,所以就出错了 }; ``` -### 2.4 本章小结 +## 3.4. 本章小结 这一章是写作中最艰难的一章,中间停滞了将近一年。因为要说清楚C++模板中一些语法噪音和设计决议并不是一件轻松的事情。不过通过这一章的学习,我们知道了下面这几件事情: @@ -1654,15 +1737,15 @@ template struct X { 2. 在 2.3.3 一节我们插入了C++模板中最难理解的内容之一:名称查找。名称查找是语义分析的一个环节,模板内书写的 **变量声明**、**typedef**、**类型名称** 甚至 **类模板中成员函数的实现** 都要符合名称查找的规矩才不会出错; -3. C++编译器对语义的分析的原则是"大胆假设,小心求证":在能求证的地方尽量求证 —— 比如两段式名称查找的第一阶段;无法检查的地方假设你是正确的 —— 比如`typedef typename A::MemberType _X;`在模板定义时因为`T`不明确不会轻易判定这个语句的死刑。 +3. C++编译器对语义的分析的原则是"大胆假设,小心求证":在能求证的地方尽量求证 —— 比如两段式名称查找的第一阶段;无法检查的地方假设你是正确的 —— 比如`typedef typename A::MemberType X;`在模板定义时因为`T`不明确不会轻易判定这个语句的死刑。 从下一章开始,我们将进入元编程环节。我们将使用大量的示例,一方面帮助巩固大家学到的模板知识,一方面也会引导大家使用函数式思维去解决常见的问题。 -## 3 深入理解特化与偏特化 +# 4. 深入理解特化与偏特化 -### 3.1 正确的理解偏特化 +## 4.1. 正确的理解偏特化 -#### 3.1.1 偏特化与函数重载的比较 +### 4.1.1. 偏特化与函数重载的比较 在前面的章节中,我们介绍了偏特化的形式、也介绍了简单的用例。因为偏特化和函数重载存在着形式上的相似性,因此初学者便会借用重载的概念,来理解偏特化的行为。只是,重载和偏特化尽管相似但仍有差异。 @@ -1734,7 +1817,7 @@ DoWork i; // (4) DoWork pf; // (5) ``` -首先,编译器分析(0), (1), (2)三句,得知(0)是模板的原型,(1),(2),(3)是模板(0)的特化或偏特化。我们假设有两个字典,第一个字典存储了模板原型,我们称之为`TemplateDict`。第二个字典`TemplateSpecDict`,存储了模板原型所对应的特化/偏特化形式。所以编译器在这几句时,可以视作 +首先,编译器分析(0), (1), (2)三句,得知(0)是模板的原型,(1),(2),(3)是模板(0)的特化或偏特化。我们假设有两个字典,第一个字典存储了模板原型,我们称之为`TemplateDict`。第二个字典`TemplateSpecDict`,存储了模板原型所对应的特化/偏特化形式。所以编译器在处理这几句时,可以视作 ```C++ // 以下为伪代码 @@ -1800,7 +1883,7 @@ X v8; > 令`T`是模板类型实参或者类型列表(如 _int, float, double_ 这样的,`TT`是template-template实参(参见6.2节),`i`是模板的非类型参数(整数、指针等),则以下形式的形参都会参与匹配: -> `T`,`cv-list T`,`T*`, `template-name `, `T&`, `T&&` +> `T`, `cv-list T`, `T*`, `template-name `, `T&`, `T&&` >`T [ integer-constant ]` @@ -1812,11 +1895,11 @@ X v8; >`type [i]`, `template-name `, `TT`, `TT`, `TT` -对于某些实例化,偏特化的选择并不是唯一的。比如v4的参数是``,能够匹配的就有三条规则,1,6和7。很显然,6还是比7好一些,因为能多匹配一个指针。但是1和6,就很难说清楚谁更好了。一个说明了两者类型相同;另外一个则说明了两者都是指针。所以在这里,编译器也没办法决定使用那个,只好爆出了编译器错误。 +对于某些实例化,偏特化的选择并不是唯一的。比如v4的参数是``,能够匹配的就有三条规则,1,6和7。很显然,6还是比7好一些,因为能多匹配一个指针。但是1和6,就很难说清楚谁更好了。一个说明了两者类型相同;另外一个则说明了两者都是指针。所以在这里,编译器也没办法决定使用那个,只好报出了编译器错误。 其他的示例可以先自己推测一下, 再去编译器上尝试一番:[`goo.gl/9UVzje`](http://goo.gl/9UVzje)。 -#### 3.1.2 不定长的模板参数 +### 4.1.2. 不定长的模板参数 不过这个时候也许你还不死心。有没有一种办法能够让例子`DoWork`像重载一样,支持对长度不一的参数列表分别偏特化/特化呢? @@ -1935,7 +2018,7 @@ template class Y {}; // (4) error! 在这里,我们只提到了变长模板参数的声明,如何使用我们将在第四章讲述。 -#### 3.1.3 模板的默认实参 +### 4.1.3. 模板的默认实参 在上一节中,我们介绍了模板对默认实参的支持。当时我们的例子很简单,默认模板实参是一个确定的类型`void`或者自定义的`null_type`: @@ -1952,7 +2035,7 @@ template < 第一步,我们先把浮点正确的写出来: ```C++ -#include +include template T CustomDiv(T lhs, T rhs) { // Custom Div的实现 @@ -1964,7 +2047,7 @@ template ::value> struct Sa } }; -template struct SafeDivide{ // 偏特化A +template struct SafeDivide{ // 偏特化A static T Do(T lhs, T rhs){ return lhs/rhs; } @@ -1987,8 +2070,8 @@ void foo(){ 嗯,这个时候我们要再把整型和其他类型纳入进来,无外乎就是加这么一个参数[`goo.gl/0Lqywt`](http://goo.gl/0Lqywt): ```C++ -#include -#include +include +include template T CustomDiv(T lhs, T rhs) { T v; @@ -2028,8 +2111,8 @@ void foo(){ 当然,这时也许你会注意到,`is_integral`,`is_floating_point`和其他类类型三者是互斥的,那能不能只使用一个条件量来进行分派呢?答案当然是可以的:[`goo.gl/jYp5J2`](http://goo.gl/jYp5J2): ```cpp -#include -#include +include +include template T CustomDiv(T lhs, T rhs) { T v; @@ -2082,14 +2165,14 @@ void foo(){ * A和B都与模板实参无法匹配,所以使用原型,调用`CustomDiv` -### 3.2 后悔药:SFINAE +## 4.2. 后悔药:SFINAE 考虑下面这个函数模板: ``` C++ template void foo(T t, typename U::type u) { - // ... + // ... } ``` @@ -2097,16 +2180,16 @@ void foo(T t, typename U::type u) { ``` C++ struct X { - typedef float type; + typedef float type; }; template void foo(T t, typename U::type u) { - // ... + // ... } void callFoo() { - foo(5, 5.0); // T == int, typename U::type == X::type == int + foo(5, 5.0); // T == int, typename U::type == X::type == float } ``` @@ -2114,11 +2197,11 @@ void callFoo() { ```C++ struct X { - typedef float type; + typedef float type; }; struct Y { - typedef float type2; + typedef float type2; }; template @@ -2127,8 +2210,8 @@ void foo(T t, typename U::type u) { } void callFoo() { - foo(5, 5.0); // T == int, typename U::type == X::type == int - foo(5, 5.0); // ??? + foo(5, 5.0); // T == int, typename U::type == X::type == float + foo(5, 5.0); // ??? } ``` @@ -2148,16 +2231,16 @@ error: no matching function for call to 'foo' ```C++ struct X { - typedef float type; + typedef float type; }; struct Y { - typedef float type2; + typedef float type2; }; template void foo(T t, typename U::type u) { - // ... + // ... } template @@ -2165,8 +2248,8 @@ void foo(T t, typename U::type2 u) { // ... } void callFoo() { - foo(5, 5.0); // T == int, typename U::type == X::type == int - foo( 1, 1.0 ); // ??? + foo(5, 5.0); // T == int, typename U::type == X::type == float + foo( 1, 1.0 ); // ??? } ``` @@ -2211,13 +2294,13 @@ void foo(A const&) {} void foo(B const&) {} void callFoo() { - foo( A() ); - foo( B() ); - foo( C() ); + foo( A() ); + foo( B() ); + foo( C() ); } ``` -那么 `foo( A() )` 虽然匹配 `foo(B const&)` 会失败,但是它起码能匹配 `foo(A const&)`,所以它是正确的;`foo( B() )` 能同时匹配两个函数原型,但是 `foo(B const&)` 要更好一些,因此它选择了这个原型。而 `foo( C() );` 因为两个函数都匹配失败(Failure)了,所以它找不到相应的原型,这时才会爆出一个编译器错误(Error)。 +那么 `foo( A() )` 虽然匹配 `foo(B const&)` 会失败,但是它起码能匹配 `foo(A const&)`,所以它是正确的;`foo( B() )` 能同时匹配两个函数原型,但是 `foo(B const&)` 要更好一些,因此它选择了这个原型。而 `foo( C() );` 因为两个函数都匹配失败(Failure)了,所以它找不到相应的原型,这时才会报出一个编译器错误(Error)。 所以到这里我们就明白了,在很多情况下,Failure is not an error。编译器在遇到Failure的时候,往往还需要尝试其他的可能性。 @@ -2237,17 +2320,17 @@ void callFoo() { ```C++ template < - typename T0, - // 一大坨其他模板参数 - typename U = /* 和前面T有关的一大坨 */ + typename T0, + // 一大坨其他模板参数 + typename U = /* 和前面T有关的一大坨 */> RType /* 和模板参数有关的一大坨 */ functionName ( - PType0 /* PType0 是和模板参数有关的一大坨 */, - PType1 /* PType1 是和模板参数有关的一大坨 */, - // ... 其他参数 + PType0 /* PType0 是和模板参数有关的一大坨 */, + PType1 /* PType1 是和模板参数有关的一大坨 */, + // ... 其他参数 ) { - // 实现,和模板参数有关的一大坨 + // 实现,和模板参数有关的一大坨 } ``` @@ -2255,19 +2338,21 @@ functionName ( ```C++ template < - typename T, - typenname U = typename vector::iterator // 1 + typename T, + typename U = typename vector::iterator // 1 > typename vector::value_type // 1 - foo( - T*, // 1 - T&, // 1 - typename T::internal_type, // 1 - typename add_reference::type, // 1 - int // 这里都不需要 substitution - ) +foo( + T*, // 1 + T&, // 1 + typename T::internal_type, // 1 + typename add_reference::type, // 1 + int // 这里都不需要 substitution +) { - // 整个实现部分,都没有 substitution。这个很关键。 + // 根据定义,substitution只发生在函数签名上。 + // 故而整个函数实现部分都不会存在 substitution。 + // 这是一个重点需要记住。 } ``` @@ -2277,11 +2362,11 @@ typename vector::value_type // 1 ```C++ struct X { - typedef int type; + typedef int type; }; struct Y { - typedef int type2; + typedef int type2; }; template void foo(typename T::type); // Foo0 @@ -2289,9 +2374,9 @@ template void foo(typename T::type2); // Foo1 template void foo(T); // Foo2 void callFoo() { - foo(5); // Foo0: Succeed, Foo1: Failed, Foo2: Failed - foo(10); // Foo0: Failed, Foo1: Succeed, Foo2: Failed - foo(15); // Foo0: Failed, Foo1: Failed, Foo2: Succeed + foo(5); // Foo0: Succeed, Foo1: Failed, Foo2: Failed + foo(10); // Foo0: Failed, Foo1: Succeed, Foo2: Failed + foo(15); // Foo0: Failed, Foo1: Failed, Foo2: Succeed } ``` @@ -2303,33 +2388,33 @@ std/boost库中的 `enable_if` 是 SFINAE 最直接也是最主要的应用。 ```C++ struct ICounter { - virtual void increase() = 0; - virtual ~ICounter() {} + virtual void increase() = 0; + virtual ~ICounter() {} }; struct Counter: public ICounter { - void increase() override { - // Implements - } + void increase() override { + // Implements + } }; template void inc_counter(T& counterObj) { - counterObj.increase(); + counterObj.increase(); } template void inc_counter(T& intTypeCounter){ - ++intTypeCounter; + ++intTypeCounter; } void doSomething() { - Counter cntObj; - uint32_t cntUI32; + Counter cntObj; + uint32_t cntUI32; - // blah blah blah - inc_counter(cntObj); - inc_counter(cntUI32); + // blah blah blah + inc_counter(cntObj); + inc_counter(cntUI32); } ``` @@ -2348,16 +2433,16 @@ template void inc_counter(T& intTypeCounter); ```C++ template void inc_counter( - T& counterObj, - typename std::enable_if< - is_base_of::value ->::type* = nullptr ); + T& counterObj, + typename std::enable_if< + std::is_base_of::value +>::type* = nullptr ); template void inc_counter( - T& counterInt, - typename std::enable_if< - std::is_integral::value ->::type* = nullptr ); + T& counterInt, + typename std::enable_if< + std::is_integral::value +>::type* = nullptr ); ``` 然后我们解释一下,这个 `enable_if` 是怎么工作的,语法为什么这么丑: @@ -2378,11 +2463,11 @@ void inc_counter(ICounter& counterObj); 嗯,你说的没错,在这里这个特性一点都没用。 -这也提醒我们,当你觉得需要写 `enable_if` 的时候,首先要考虑到以下可能性: +这也提醒我们,当你觉得需要写 `enable_if` 的时候,首先要考虑到以下可能的替代方案: - * 重载(对模板函数) + * 重载(适用于函数模板) - * 偏特化(对模板类而言) + * 偏特化(适用于类模板) * 虚函数 @@ -2392,9 +2477,9 @@ void inc_counter(ICounter& counterObj); ```C++ struct ICounter {}; struct Counter: public ICounter { - void increase() { - // impl - } + void increase() { + // impl + } }; ``` @@ -2408,13 +2493,13 @@ template void inc_counter(T& c) { ++c; }; void doSomething() { - Counter cntObj; - uint32_t cntUI32; + Counter cntObj; + uint32_t cntUI32; - // blah blah blah - inc_counter(cntObj); // 1 - inc_counter(static_cast(cntObj)); // 2 - inc_counter(cntUI32); // 3 + // blah blah blah + inc_counter(cntObj); // 1 + inc_counter(static_cast(cntObj)); // 2 + inc_counter(cntUI32); // 3 } ``` @@ -2426,40 +2511,40 @@ void doSomething() { ```C++ -#include -#include -#include +include +include +include struct ICounter {}; struct Counter: public ICounter { - void increase() { - // impl - } + void increase() { + // impl + } }; template void inc_counter( - T& counterObj, - typename std::enable_if< - std::is_base_of::value ->::type* = nullptr ){ - counterObj.increase(); + T& counterObj, + typename std::enable_if< + std::is_base_of::value +>::type* = nullptr ){ + counterObj.increase(); } template void inc_counter( - T& counterInt, - typename std::enable_if< - std::is_integral::value ->::type* = nullptr ){ - ++counterInt; + T& counterInt, + typename std::enable_if< + std::is_integral::value +>::type* = nullptr ){ + ++counterInt; } void doSomething() { - Counter cntObj; - uint32_t cntUI32; + Counter cntObj; + uint32_t cntUI32; - // blah blah blah - inc_counter(cntObj); // OK! - inc_counter(cntUI32); // OK! + // blah blah blah + inc_counter(cntObj); // OK! + inc_counter(cntUI32); // OK! } ``` @@ -2479,28 +2564,28 @@ template void foo(T& c, decltype(c.increase())* = nullptr); ```C++ struct Counter { - void increase() { - // Implements - } + void increase() { + // Implements + } }; template void inc_counter(T& intTypeCounter, std::decay_t* = nullptr) { - ++intTypeCounter; + ++intTypeCounter; } template void inc_counter(T& counterObj, std::decay_t* = nullptr) { - counterObj.increase(); + counterObj.increase(); } void doSomething() { - Counter cntObj; - uint32_t cntUI32; + Counter cntObj; + uint32_t cntUI32; - // blah blah blah - inc_counter(cntObj); - inc_counter(cntUI32); + // blah blah blah + inc_counter(cntObj); + inc_counter(cntUI32); } ``` @@ -2511,7 +2596,7 @@ void doSomething() { template void foo(ArgT&& a); ``` -加入我们要限定ArgT只能是 float 的衍生类型,那么写成下面这个样子是不对的,它实际上只能接受 float 的右值引用。 +假如我们要限定ArgT只能是 float 的衍生类型,那么写成下面这个样子是不对的,它实际上只能接受 float 的右值引用。 ```C++ void foo(float&& a); @@ -2522,50 +2607,158 @@ void foo(float&& a); ```C++ template void foo( - ArgT&& a, - typename std::enabled_if< - is_same, float>::value ->::type* = nullptr + ArgT&& a, + typename std::enabled_if< + std::is_same, float>::value +>::type* = nullptr ); ``` 从上面这些例子可以看到,SFINAE最主要的作用,是保证编译器在泛型函数、偏特化、及一般重载函数中遴选函数原型的候选列表时不被打断。除此之外,它还有一个很重要的元编程作用就是实现部分的编译期自省和反射。 -虽然它写起来并不直观,但是对于既没有编译器自省、也没有Concept的C++1y来说,已经是最好的选择了。 +虽然它写起来并不直观,但是对于既没有编译器自省、也没有Concept的C++11来说,已经是最好的选择了。 + +## 4.3. Concept "概念":对模板参数约束的直接描述 + +### 4.3.1. "概念" 解决了什么问题 +从上一节可以看出,我们兜兜转转了那么久,就是为了解决两个问题: + +1. 在模板进行特化的时候,盘算一下并告诉编译器这里能不能特化; -(补充例子:构造函数上的enable_if) +2. 在函数决议面临多个候选的时候,如果有且仅有其中一个原型能够被函数决议接纳,那就决定是你了! -## 4 元编程下的数据结构与算法 -### 4.1 表达式与数值计算 -### 4.1 获得类型的属性——类型萃取(Type Traits) -### 4.2 列表与数组 -### 4.3 字典结构 -### 4.4 "快速"排序 -### 4.5 其它常用的"轮子" +如果语言能允许用户直接描述需求并传达给编译器,不就不用这么麻烦了么。其实在很多现代语言中,都有类似的语言要素存在,比如C的约束(constraint on type parameters): -## 5 模板的进阶技巧 -### 5.1 嵌入类 -### 5.2 Template-Template Class -### 5.3 高阶函数 -### 5.4 闭包:模板的"基于对象" +``` C +public class Employee { + // ... +} + +public class GenericList where T : Employee { + // ... +} +``` +上例就非常清晰的呈现了我们对`GenericList`中`T`的要求是:它得是一个`Employee`或`Employee`的子类。 + +这种"清晰的"类型约束,在C++中称作概念(Concept)。最早有迹可循的概念相关工作应当从2003年后就开始了。2006年Bjarne在POPL 06上的一篇报告"Specifying C++ concepts"算是"近代"Concept工作的首次公开亮相。委员会为Concept筹划数年,在2008年提出了第一版Concepts提案,试图进入C++0x的标准中。这也是Concept第一次在C++社群当中被广泛"炒作"。不过2009年的会议,让"近代"Concept在N2617草案戛然而止。 + +2013年之后,Concept改头换面为Concept Lite提案(N3701)卷土重来,历经多方博弈和多轮演化,最终形成了我们在C++20里看到的Concept。有关于Concept的方法论和比较,B.S. 在白皮书中有过比较详细的交代。 + +总之,在concept进入标准之后,模板特化的类型约束写起来就方便与直接多了。而且这些约束之间还可以像表达式一样复用和组合。虽然因为C++类型系统自身的琐碎导致基础库中的concept仍然相当的冗长,但是比起之前起码具备了可用性。 + +比如我们拿上一节中最后一个例子作为对比: +``` C++ +// SFINAE +template +void foo( + ArgT&& a, + typename std::enabled_if< + std::is_same, float>::value +>::type* = nullptr +); +// Concept +template + requires std::same_as, float> +void foo(ArgT&& a) { +} +``` +可以看到,concept之后的表达式消除了语法噪音,显得更为简洁一些。而对于之前++的例子,concept下则更为扼要: +```C++ +template concept Incrementable = requires (T t) { ++t; } +template +void inc_counter(T& intTypeCounter) { + ++intTypeCounter; +} +``` +直接告诉编译器,我们对T的要求是你得有`++`。 + +当然有人会问,那能不能直接写成以下形式,不是更简单吗? + +``` C++ +template requires (T t) { ++t; } +void inc_counter(T& cnt); +``` + +答案是:不能。 +因为`requires`作为关键字/保留字是存在二义性的。当它用于函数模板或者类模板的声明时,它是一个constraint,后面需要跟着concept表达式;而用于concept中,则是一个required expression,用于concept的求解。既然constraint后面跟着一个concept表达式,而requires也可以用来定义一个concept expression,那么一个风骚的想法形成了:我能不能用 `requires (requires (T t) {++t;})` 来约束模板参数呢? + +当然是可以的!C++就是这么的简(~~有~~)单(~~病~~)! + +``` C++ +template requires (requires (T t) { ++t; }) +void inc_counter(T& cnt); +``` + +总而言之,除了这些烦人的问题,"概念"的出现,使得模板的出错提示也清爽了些许 —— 虽然大佬们都在鼓吹concept让模板出错多么好调试,但是实际上模板出错,有一半是来源自类型系统本质上的复杂性,概念并不能解决这一问题。 + +比如这里使用SFINAE的提示: + +``` +:23:5: error: no matching function for call to 'Inc' + Inc(y); + ^~~ +:5:6: note: candidate template ignored: substitution failure [with T = X]: cannot increment value of type 'X' +void Inc(T& v, std::decay_t* = nullptr) + ^ ~~ +``` + +而这里是使用了concept的提示。 +``` +:25:5: error: no matching function for call to 'Inc_Concept' + Inc_Concept(y); + ^~~~~~~~~~~ +:13:6: note: candidate template ignored: constraints not satisfied [with T = X] +void Inc_Concept(T& v) + ^ +:12:11: note: because 'X' does not satisfy 'Incrementable' +template + ^ +:10:41: note: because '++t' would be invalid: cannot increment value of type 'X' +concept Incrementable = requires(T t) { ++t; }; +``` + +虽然在这个例子中,通过 *Concept* 获得出错提示看起来要比使用 *SFINAE* 所获得的错误描述要更长一点,但是对于更加复杂类型来说,则会友善许多。以后会找个例子给大家陈述。 + +### 4.3.2. "概念"入门 + + +# 5. 未完成章节 + +``` +# 6. 元编程下的数据结构与算法 +## 6.1. 表达式与数值计算 +## 6.2. 获得类型的属性——类型萃取(Type Traits) +## 6.3. 列表与数组 +## 6.4. 字典结构 +## 6.5. "快速"排序 +## 6.6. 其它常用的"轮子" + +# 7. 非模板的编译期计算 + +# 8. 模板的进阶技巧 +## 8.1. 嵌入类 +## 8.2. Template-Template Class +## 8.3. 高阶函数 +## 8.4. 闭包:模板的"基于对象" stl allocator? mpl::apply -### 5.5 占位符(placeholder):在C++中实现方言的基石 -### 5.6 编译期"多态" +## 8.5. 占位符(placeholder):在C++中实现方言的基石 +## 8.6. 编译期"多态" -## 6 模板的威力:从foreach, transform到Linq -### 6.1 Foreach与Transform -### 6.2 Boost中的模板 +# 9. 模板的威力:从foreach, transform到Linq +## 9.1. Foreach与Transform +## 9.2. Boost中的模板 Any Spirit Hana TypeErasure -### 6.3 Reactor、Linq与C++中的实践 -### 6.4 更高更快更强:从Linq到FP +## 9.3. Reactor、Linq与C++中的实践 +## 9.4. 更高更快更强:从Linq到FP -## 7 结语:讨论有益,争端无用 -### 7.1 更好的编译器,更友善的出错信息 -### 7.2 模板的症结:易于实现,难于完美 -### 7.3 一些期望 +# 10. 结语:讨论有益,争端无用 +## 10.1. 更好的编译器,更友善的出错信息 +## 10.2. 模板的症结:易于实现,难于完美 +## 10.3. 一些期望 alexandrescu 关于 min max 的讨论:《再谈Min和Max》 std::experimental::any / boost.any 对于 reference 的处理 +``` [1]: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf

AltStyle によって変換されたページ (->オリジナル) /