c++模板 模板参数有默认值时模板特例化匹配问题
模板就是代码自动生成器,根据模板的规则自动生成类和函数。所谓的主模板和偏特化,都是代码生成的框架模型(类似面向对象的类和实现了部分功能的子类)。它们是抽象的存在,而不是具体的实例。只有全特化后,才是有了具体的实例,实实在在的存在(好比人(模板)和小明(实例)的关系)。
自己代码编写的全特化,就是自己不用编译器自动生成代码了,自己定制实现了。
而根据模板,定义一个类,比如 std::vector<int>, 等于编译器自己会根据这个使用,生成一个全特化的参数是int时的类:template<> std::vector<int>。
而偏特化,就类似于函数的参数绑定机制。部分模板参数被特化,这样形成一个待全特化的中间模板。最终也只有全特化时,才是最终结果。
偏特化的模板参数不是实例化模板时的参数,一定是主模板的模板参数才是(类似于函数原型的参数)。也就是后面讨论en_if<true>不能去对应偏特化的第二个参数。
所以如果模板没有被申明或者用它。它只是个模型框架,也就不会自动生成代码。也就是说,根本就没用它。只有申明用它时才有用。
替换,实例化,和特例化
在处理模板相关的代码时,C++编译器必须经常去用模板实参替换模板参数。有时后这种替
换只是试探性的:编译器需要验证这个替换是否有效(参见8.4节以及15.7节)。
用实际参数替换模板参数,以从一个模板创建一个常规类、类型别名、函数、成员函数或者
变量的过程,被称为“模板实例化”。
通过实例化或者不完全实例化产生的实体通常被称为特例化(specialization)。
在C++中,实例化过程并不是产生特例化的唯一方式。另外一些方式允许程序员显式的
指定一个被关联到模板参数的、被进行了特殊替换的声明。正如2.5节介绍的那样,这一类
特例化以一个template<>开始:
严格来说,这被称为显式特例化(explicitspecialization)。
如果特例化之后依然还有模板参数,就称之为部分特例化。
在讨论(显式或者部分)特例化的时候,特例化之前的通用模板被称为主模板。
声明和定义
classC;//adeclarationofCasaclass
voidf(intp);//adeclarationoff()asafunctionandpasanamed
parameter
externintv;//adeclarationofvasavariable
对于声明,如果其细节已知,或者是需要申请相关变量的存储空间,那么声明就变成了定义。
对于class类型的定义和函数定义,意味着需要提供一个包含在{}中的主体,或者是对函数使
用了=defaul/=delete。对于变量,如果进行了初始化或者没有使用extern,那么声明也会变
成定义。
完整类型和非完整类型
非完整类型是以下情况之一:
一个被声明但是还没有被定义的class类型。
一个没有指定边界的数组。
一个存储非完整类型的数组。
Void类型。
一个底层类型未定义或者枚举值未定义的枚举类型。
任何一个被const或者volatile修饰的以上某种类型。其它所有类型都是完整类型。
模板参数(templateparameters)和模板实参(templatearguments)
模板参数是那些在模板定义或者声明中,出现在template关键字后面的尖括号中的名称。
模板实参是那些用来替换模板参数的内容。不同于模板参数,模板实参可以不只是“名称”。
不管这些实参是否和模板参数有关,模板名称以及其后面的尖括号和其中的模板实参,被称
为template-id。
当指出模板的template-id的时候,用模板实参替换模板参数的过程就是显式的,但是在很
多情况这一替换则是隐式的(比如模板参数被其默认值替换的情况)。
主模板,就不需要模板名称后加上尖括号中的模板参数:
template<typename T> class Box;
template<typename T> class Box<T>;
非主模板是在声明类或变量模板的偏特化时发生;
函数模板必须是主模板.
非类型参数:
按传值。对字符串和数组会蜕变成指针:
template<int buf[5]> class Lexer; // buf is really an int*
template<int* buf> class Lexer; // OK: this is a redeclaration
template<int fun()> struct FuncWrap; // fun really has pointer to function type
template<int (*)()> struct FuncWrap; // OK: this is a redeclaration
就像变量,static, mutable不能出现在template里面。const和volatile可以。但是在最外层的参数类型就会被忽略。
非类型模板参数是指模板参数的类型不是类型本身,而是一个具体的值,如整数、指针或引用等。
template<int const length> class Buffer; // const is useless here
template<int length> class Buffer; // same as previous declaration
const修饰的int是最外层了。
template<const int* ptr> struct C {}; // `const` 修饰的是 `int`,不是指针本身
template<int* const ptr> struct D {}; // `const` 修饰的是指针本身,是最外层,会被忽略非类型参数传值时总是prvalue,是没有地址,不能赋值的:如下
template<int Counter>
struct LocalIncrement {
LocalIncrement() { Counter = Counter + 1; } 编译错,不能赋值
~LocalIncrement() { Counter = Counter - 1; }
};int myi=3;
LocalIncrement<myi> aaa;改成引用,就可以了:
template<int& Counter>
struct LocalIncrement。。。。。。
只能在全局变量去调用。要不会报不是const的错误:
// 非`const` 整数变量int myCounter = 3;int main() {// 使用模板LocalIncrement<myCounter> aaa;std::cout << "Value after increment: " << myCounter << std::endl;// 在离开作用域时,析构函数会被调用,Counter 将被减 1return 0;}模板参数包:
必须只有一个参数包,除非可推导,有多个参数包也行:
template<typename… Types, typename Last>
class LastType; // ERROR: template parameter pack is not the last template parameter
template<typename… TestTypes, typename T>
void runTests(T value); // OK: template parameter pack is followed by a deducible template
parameter
template<unsigned…> struct Tensor;
template<unsigned… Dims1, unsigned… Dims2>
auto compose(Tensor<Dims1…>, Tensor<Dims2…>);// OK: the tensor dimensions can be deduced
类和变量模板的偏特化声明 (参见第 16 章) 可以有多个参数包。与主模板对应的参数包不同,
这是因为偏特化是通过推导选择的,该推导过程与函数模板的推导过程相同。
用struct 当示例,与class一样。只是省得写public。struct默认是public的。
模板参数有两种类型:一个是类型 typename
;另外是类型固定(也叫非类型模板)
,是取值如 int A。
访问类型时加上typename
一个类中有 类型和值,比如
class A{
public:
using IntType = int;
int i;
static int STATIC_I;
}
那么,当访问它的类型 IntType 时, A::IntType
。由于这个语句有歧义,因为访问A的静态变量,也是这么写 A::STATIC_I
. 所以要在访问类型时,前面加上 typename,即 typename A::IntType
模板类似函数:
1,模板参数:template<typename T1, typename T2> class ClassA{};
原型这里是没有 ClassA<T1,T2>的。在外部引用的时候才用。
模板的声明类似函数的形式参数:template<typename T, typename U = int>
vs function(int, float=1.0)
2, 模板参数调用 类调用时
template<> ClassA<int, string>
类定义时: 主模板与偏特化(模型框架),全特化(实例)
源码:
template<typename T, typename U = int> //这个是相当于函数的接口原型。即模板的接口原型
//U参数如果被省略的话,默认值是int。
class S{ //#1
public:
void f1(){};
};
template<>
class S<void> { //#2 这个是个全特化,等于函数赋予了实参,等于给接口 S<T,U=int> 的参数赋值为 S<void>,
// 因为默认值是int,所以最终为
//S<void, int> 这个特例化就是给接口全部赋上实参。
public:
void f2(){}; //#3.
};
int main()
{
S<void, int> sv; // OK: uses #2, definition available #3
// S<void, int> 这个去优先匹配特例化,发现匹配,所以就用了 #2。
//sv.f1();//error
sv.f2();//ok
}
主模板(原型模板,primary template)
-
全特化:就是给参数全部赋值
template<>
struct S<bool, float>;
template里面是空的,说明没有模板参数了,就是全部都特化,也就是说模板参数全被定义死类型了,赋值了。
(
赋值实例化是另外回事。这里是模板定义。不是实例化用它!!!
)
当然,由于第二个参数有默认值, 这个定义template<> struct S<bool>; 就成了 S<bool, int>。说明偏特化定义时,是可以在bind参数时省略默认值的。
上面的源码
#include <type_traits> template<bool ,typename T=int> struct en_if{}; template<> struct en_if<true>{ using TYPE = float; }; int main() { static_assert( std::is_same_v< en_if<true>::TYPE, float >); }
这里在编译生成的代码:
template<> struct en_if<true> { using TYPE = float; };
其实它是:
template<>
struct en_if<true,int>
{
using TYPE = float;
};因为主模板说了,省略了第二个参数时,默认是int类型。
所以如果main调用时,改成<true, bool>:
static_assert( std::is_same_v< en_if<true, bool>::TYPE, float >);那么它先看无法匹配全特化 template<> struct en_if<true>,最终还是会找到主模板,生成新的特化模板:
下面这个和上面的区别是:
上面是个自定义的全特化,虽然 en_if<true>只写了第一个参数,但是第二个在主模板上有默认值。template<>,这个就是全特化的标志。
下面这个就是自定义的偏特化。而这个留着的模板参数T,它必定是对应的主模板的第二个参数。en_if<true>, 这个true一定是主模板的第一个参数。第二个没写,就是默认void。由参数反推模板参数,推出T,就是void了。就是这个流程。
千万不要以为 en_if<true>,这个true对应上了T,最终变成 en_if<true,true>。这就是混淆了去主模板找。不能从偏特化里面匹配!!!
偏特化的模板参数不是实例化模板时的参数,一定是主模板的模板参数才是(类似于函数原型的参数)。
另外,在定义阶段:定义了模板参数T,在偏特化时(不可能是全特化了,全特化template<>,是不能定义模板参数的 ),必须用在模板参数调用上。否则报下面错误:
-
偏特化(部分特化):就是类似函数的bind,绑定了一些参数,其他还待绑定。(全特化就是全部绑定了)
比如:2号 template<typename T> struct S<true,T>
; 这个s<true,T>模板调用里面有一个参数T,说明有一个模板参数还没有绑定。就是部分特化。
而且注意这个部分特化,需要待定的参数是原型的第二个模板参数。
偏特化可能还会模板参数变多,这个变多与偏特化没关系。是这个函数需要外面更多的参数:template<typename T,typename E,typename E2> struct S<true,T>{ E except; E2 e2;}
偏特化的是主模板,肯定是不变的: 上面例子就是模板参数变多了,但是对原型的引用是不变的S<true,T>。
源码分析
对于上面的源码,clang模板展开时,#3变成:
S<void> sv = S<void>();
为啥S<void, int> 不走主模板分支 #1?
S<void>这个出现在模板参数赋值。不是类模板的定义里面,那里是偏特化。这里是需要全特化,就看有没有自己实现,否则就需要编译器试了。既然 S<void>是全特化的,它去看接口原型是 S<T,U=int>。而这里的全特化少了第二个参数,根据原型,默认值就是int,即 S<void, int>。
然后找是不是有全特化实现,代码有自定义的,那就用了(不用编译器去生成了)。
实现过程:
class S<void> 的实例化,查找到前面有主模板定义:
template<typename T, typename U = int> class S
因为第二个模板参数默认值是int所以 会自动生成
S<void, int>
也就是说,class S<void> 根据主模板,就是 S<void, int>。
接下来就是 S<void, int> 选择哪个匹配问题。根据模板匹配找最适合的,范围更小的。最终选定偏特化的,主模板的范围太广了。
再看一个小的示例:
//这个是主模板
template<bool, typename T= void> //这个T,可以省略,因为没有被引用 即:template<bool, typename=void>
struct en_if {};
//一个偏特化模板。之所以不是全特化,还保留了主模板的第二个参数
template< typename T>
struct en_if <true, T> { using TYPE = T; };
/////------
en_if<true>::TYPE* a;
static_assert(std::is_same_v<en_if<true>::TYPE, void>);
en_if<true>:它就是个参数实例化,参数赋值,肯定得符合原型:
第一步,看模板原型参数,调用参数 en_if<true> 对应到 模板原型 :template<bool, typename T= void>。true是第一个参数。
即补充完整成:en_if<true, void>。
第二步,决策哪个适合,第二个偏序的更范围小。优先选。
,这是个全特化,因为没有 templace<xx> class<t,xxx>存在了,那就是 templace<>全特化了。
两个都符合:
- 选主模板:应该是根据主模板,不是选择。<true> 推出 <bool, T=void> 即 <true,void>
- 选偏特化模板:<true> 推出 <true ,T> 即<true,true>
上面选哪个?上面分析错误。true是个值,不是类型。而第二个模板参数是 typename T(
这里模板偏特化的 T 参数。en_if<true>,这个true值可不是对应这个 template<typename T>的:
template< typename T>
struct en_if <true, T> { using TYPE = T; };
这个T其实对应的是模板原型的第二个参数:即 en_if <true, T>,第一个是true,第二个是T。
)。所以导出 <true,true>是有问题的。所以选中了第一个模板。en_if<true>永远不可能选中第二个模板,因为这个true是模板原型<bool,typename T>的第二个参数,而第二个是类型。true是bool值,不能赋值给类型。
首先会匹配偏特化,发现可以匹配,只是少了个参数。再根据主模板接口定义 <bool, T=void>,少的参数有默认值void。最终形成: en_if<true, void>。
接下来 en_if<true, void>选择主模板,还是偏特化的问题。
主模板:template<bool,typename T=void> 这里第二个参数可以说任意类型。只是没指定时默认是void。
偏特化:en_if<true, void> 这里第二个参数必须是void!
偏特化更符合,范围更小。即符合偏特化,肯定符合主模板;符合主模板,不一定符合偏特化。
偏特化被改的话,都会报错:
//一个偏特化模板。之所以不是全特化,还保留了主模板的第二个参数template< typename T=void>struct en_if <true, T> { using TYPE = T; };//一个偏特化模板。之所以不是全特化,还保留了主模板的第二个参数template< typename T=int>struct en_if <true, T> { using TYPE = T; };
上面两种修改都会导致报错:这是由于默认值只能出现在模板原型中。偏特化不能定义默认值。
error: default template argument in a class template partial specialization
template< typename T=void>
^
1 error generated.
实际应用
- 使用主模板原型的默认模板参数类型,一般为void。并实现全特化版本。当不符合全特化时走到主模板;符合时走到全特化版本: 通过void_t或者 enable_if_t选择;
- 若为值,则用bool常量表达式结果作为默认参数并在特化版本中通过true false选择。
以上摘自《c++20高级编程》罗能 p64.
enable_if
作为约束使用,它虽然
出现在模板上的一个参数
,但是没有用,只是作为模板模式匹配用途。在c++20用约束来替换。
1, 关于默认值的模式匹配
//主模板模型
template<bool B, class T=long>
struct enable_if{};
//特化一个true的版本
template<class T>
struct enable_if<true,T>{
using type = T;
};
enable_if<true>::type a;
可以看到,a是个 long 类型。
template<bool B, class T = long>
struct enable_if
{
};
/* First instantiated from: insights.cpp:9 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct enable_if<true, long>
{
using type = long;
};
#endif
template<class T>
struct enable_if<true, T>
{
using type = T;
};
enable_if<true>::type a;
注意将偏特化,定义时去掉模板参数T时的区别:
上面的代码是实现了全特化的,所以直接用就行。
下面代码缺乏全特化,所以根据偏特化,生成全特化。
上面的意思就是,全特化时,可以只写待定参数,那些有默认值的参数省略掉也行!!!也就是说,template<>肯定是全特化,但是 struct en_if<true>时,写的不全。也没关系。后面就是默认值int。不过,由于没有单拎出来的参数T,这个默认值int也无法使用吧。
这个实验也说明,最终代码生成肯定得是全特化出现:template<> ,才算是生成代码。下面的由于缺 template<>,所以必须由编译器生成。
另外, en_if<true>, 这个true是主模板的第一个参数。可不是对应 下面这个T模板形参:
template<typename T>
struct en_if<true, T>{
using TYPE = int;
};否则,那可成了特错大错,变成了 en_if<true, true> 了。
2,它真正用途是作为 选择器。上面的long其实没用,用void代替。
#include <iostream>
template<bool B, class T=void>
struct enable_if{};
template<class T>
struct enable_if<true,T>{
using type = T;
};
template<class T, class U=typename enable_if< std::is_integral_v<T> >::type >
struct X{
};
X<int> ok;
X<std::string> compile_failed;
当enable_if<true>时,才会匹配上带type类型的偏序模板。由于enable_if<false> 会匹配主模板定义,而里面是没有 type 这个定义的。所以导致 compile_failed。
可以看到上面的 class U模板参数只是作为约束存在,没有任何实际意义。c++20用 concept。
class U=typename enable_if< std::is_integral_v<T> >::type
这里因为U被赋值为类型,这种用类::访问的类型,需要用 typename修饰一下。要不与静态变量会混淆
#include <iostream>
template<class T>
concept IntType= std::is_integral_v<T>;
template<IntType T>
struct X{
};
X<int> ok;
//X<std::string> compile_failed;
进一步可以简化为:
template<class T>
requires std::is_integral_v<T>
struct X{
};
X<int> ok;