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 将被减 1
    return 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.
c++中,偏特化中不能有默认值!
en_if<true> 就不可能选第二个模板。第二个模板的参数是个类型,template< typename T=int>, 而true是值,
那就必须选第一个模板<bool, typeface T>.

实际应用

通过默认模板参数 来选择默认的特化版本,当条件不成立时,退回到主模板。
  • 使用主模板原型的默认模板参数类型,一般为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;

 

posted @ 2022-12-24 17:56  Bigben  阅读(693)  评论(0编辑  收藏  举报