template auto

六月 11, 2022 [c++, template, auto] #c++ #template #auto

template auto

Advantages of auto in template parameters in C++17

The template <auto> feature (P0127R1) was accepted into C++ in the ISO C++ 2016 meeting in Oulu, Finland.

An auto keyword in a template parameter can be used to indicate a non-type parameter the type of which is deduced at the point of instantiation. It helps to think of this as a more convenient way of writing:

template <typename Type, Type value>

# For example,
template <typename Type, Type value> constexpr Type constant = value;
constexpr auto const IntConstant42 = constant<int, 42>;

# can now be written as

template <auto value> constexpr auto constant = value;
constexpr auto const IntConstant42 = constant<42>;

where you don't need to explicitly spell out the type any more. P0127R1 also includes some simple but good examples where using template with variadic template parameters is very handy, for example for implementations of compile-time lists constant values:

template <auto ... vs> struct HeterogenousValueList {};
using MyList1 = HeterogenousValueList<42, 'X', 13u>;

template <auto v0, decltype(v0) ... vs> struct HomogenousValueList {};
using MyList2 = HomogenousValueList<1, 2, 3>;

In pre-C++1z, while HomogenousValueList could be simply written as

template <typename T, T ... vs> struct Cxx14HomogenousValueList {};
using MyList3 = Cxx14HomogenousValueList<int, 1, 2, 3>;

writing an equivalent of HeterogenousValueList would not be possible without wrapping the values in some other templates, for example:

template <typename ... ValueTypes> struct Cxx14HeterogenousValueList {};
using MyList4 = Cxx14HeterogenousValueList<constant<int, 42>,
                                           constant<char, 'X'> >;

non-type parameter cannot be double

template<auto n> struct B { /* ... */ };
B<5> b1;   // OK: template parameter type is int
B<'a'> b2; // OK: template parameter type is char
B<2.5> b3; // error: template parameter type cannot be double

模板特化与模板函数重载

模板重载

可以并存,并不代表可以被正常使用,因为可能会导致重载歧义。比如如果尝试调用f2(42),那么很显然会导致歧义(使用模板3还是模板4?)

template<typename T1, typename T2>   //模板1
void f1(T1, T2);
template<typename T1, typename T2>  //模板2,与模板1的函数参数类型不同
void f1(T2, T1); 

template<typename T>  // 模板3
long f2(T);
template<typename T>  // 模板4,与模板3的返回值类型不同
char f2(T);

函数模板特例化

函数模板只能全特化,不能偏特化,如果要偏特化的话只能重载

函数模板重载(不存在偏特化)

因为偏特化版本本质上仍然是模板,所以如果需要的话,可以重载一个函数模板

template<class T>
T add(T a, T b) { return a + b;}

template<class T1>  // 重载版本,接收参数为指针
T1 add(T1* a, T1* b) { return *a + *b; }   

如果需要一个接收指针的偏特化版本,那么可以用重载模板实现(其实强行写偏特化版本,和模板函数重载版本一样)

类模板特例化

类模板既有全特化,又有偏特化 这里重新写一个更一般的模板类来说明类模板的特例化

类模板的偏特化会稍微复杂一点点,它有多种形式 类模板偏特化本质上都是指定部分类型,让偏特化版本称为普通版本的子集,若实例化时参数类型为指定的类型,则优先调用特例化版本

第一种形式

template<class T1, class T2>      // 普通版本,有两个模板参数
class B { ..... };
 
template<class T2>            // 偏特化版本,指定其中一个参数,即指定了部分类型
class B<int , T2> { ..... };  // 当实例化时的第一个参数为int 则会优先调用这个版本

第二种形式,也是最重要的版本

template<class T>     // 普通版本
class B { ..... };
 
template<class T>   //这个偏特化版本只接收指针类型的模板实参 
class B<T*> { ..... }; 
 
template<class T>
class B<T&> { ..... };     // 这个偏特化版本只接受引用类型的模板实参

第三种形式

template<class T>    //普通版本
class B { ..... };
 
template<class T>   // 这种只接受用T实例化的vector的模板实参.也是一种偏特化
class B<vector<T>> { ......  }; 

类模板偏特化与STL

偏特化在STL中最重要的两个应用

应用在迭代器设计中,为了使迭代器既可以萃取出值类型,又可以包容原生指针

如果要通过一个迭代器就能知道它的值类型,那么一般会使用iterator_traits 迭代器萃取技术的两个核心是: 1)在每个迭代器类中定义value_type值类型的类型成员,这样直接通过迭代器的value_type类型成员就可以知道值类型 2)问题就在于,迭代器必须兼容原生指针,而原生指针很难被重新定义,即要在原生指针的类中添加value_type的值类型的类型成员.这时候,靠的就是类模板的偏特化了.新添加一层iterator_traits类,专门萃取迭代器的属性,然后再对iterator_traits类设计原生指针与原生引用的偏特化版本,就解决了这个棘手的问题

type_traits类型萃取,对待特殊类型,特殊处理,提高效率

对于没有构造函数,析构函数等的内置类型,如果与复杂类型一样,执行同样的操作,显然是效率不高的 先实现一个对所有类型都设置一个最保守值的type_traits模板类,然后再对每个内置类型设置偏特化版本,内置类型设置一个更为激进的值,表明可以采取更为高效的操作来提高效率 比如copy函数,如果传递的对象是一个复杂类型,那么可能只能采取最保守的处理方式,一个一个的构造;如果是内置类型,这样显然太低效,使用memcpy()可能会好一些

其实iterator_traits也不止是处理兼容原生指针的问题,它也可以提高效率. 迭代器分为很多种,有可以随机访问的(vector),有只能前后一个一个移动的(list),也有只能单向移动的(slist),所以一般把迭代器分为五种: InputIterator       输入迭代器 OutputIterator        输出迭代器 ForwardIterator      单向迭代器 BidirectionIterator     双向迭代器 RandomAccessIterator   随机访问迭代器 比如一个advance(n)函数,对于单向迭代器只能一个一个移动过去,但是这种实现对于随机访问迭代器显然不是理想的处理方式 处理的方式就是先实现这五个类,用作标记用,在每个迭代器里面都定义迭代器类型的类型成员iterator_catagory,再对不同版本的迭代器实现不同的advance(n)处理方式

函数模板

函数模板支持:overload(重载),和full specialization(全特化),但是暂不支持partial specialization(偏特化)。。

Overload && Signature

谈到重载就需要先介绍函数的签名(Signature),它包含如下信息:

函数的裸名(不包含所属class或者namespace的作用域修饰,或者是被实例化以得到当前函数的函数模板的名字)。 函数所属的class或者namespace的作用域信息。如果该函数有内部链接属性的话,还应包含该函数所属的编译单元的相关信息。 如果函数是一个class的成员函数,且被const,volatile,或者const volatile修饰的话,还应包含这些信息。 如果函数是一个class的成员函数,且被&或者&&修饰的话,也应包含这些信息。 函数参数的类型(如果函数是经由函数模板实例化得到的话,该类型指的是模板参数被实例化之前的类型,比如T或者T1). 如果函数是经由函数模板实例化得到,还应包含返回类型(非模板函数不包含此项内容)。 如果函数是经由函数模板实例化得到,还应包含模板参数(比如T)以及用来实例化改模板的类型。 如果两个函数有不同的函数签名,那么原则上它们就可以在程序中共存,比如下面这些模板

template<typename T1, typename T2>   //模板1
void f1(T1, T2);
template<typename T1, typename T2>  //模板2,与模板1的函数参数类型不同
void f1(T2, T1); 

template<typename T>  // 模板3
long f2(T);
template<typename T>  // 模板4,与模板3的返回值类型不同
char f2(T);

但是可以并存,并不代表可以被正常使用,因为可能会导致重载歧义。比如如果尝试调用f2(42),那么很显然会导致歧义(使用模板3还是模板4?)

模板排序原则

如果第2个模板针对第1份列表可以进行成功的实参演绎(能够进行精确的匹配),而第1个模板针对第2份列表的实参演绎以失败告终,那么我们就称第1个模板要比第2个模板更加特殊。 反之,如果第1个模板针对第2份列表可以进行成功的实参演绎(能够进行精确的匹配),而第2个模板针对第1份列表的实参演绎失败,那么我们就称第2个模板要比第1个模板更加特殊。 否则的话(或者是两个都不能成功演绎,或者是两个都能成功演绎),我们就称这两个模板之间不存在特殊的排序关系。

template<typename T>
int f(T)
{
    return 1;
}
 
template<typename T>
int f(T*)
{
    return 2;
}
 
int main()
{
    std::cout << f(0) << std::endl;
    std::cout << f((int*)0) << std::endl;
}

让我们先考虑调用f(0):实参的类型是int,如果用int替换T,就能和第1个模板的参数匹配。 然而,第2个模板的参数类型总是一个指针;因此,经过演绎之后,只有产生自第1个模板的实例才是该调用的候选函数。在这个调用中,重载解析并没有发挥作用。

第2个调用f((int*)0) 就显得比较有趣:对于这两个模板,实参演绎都可以获得成功,于是就获得两个函数, 即f<int >(int)和f<int>(int )。如果根据原来的重载解析观点,这两个函数和实参类型为 int的调用的匹配程度是一样的, 这也就意味着该调用是二义性的。 然而,在这种情况下,还应该考虑重载解析的额外规则:选择“产生自更特殊的模板的函数”。因此,第2个模板被认为是更加特殊的模板

显式特化

全局的类模板特化

引入全局特化需要用到下面3个标记序列:template、< 和 > 。另外,紧跟在类名称声明后面的就是要进行特化的模板实参。下面的例子说明了这一点:

template<typename T>
class S 
{
  public:
    void info() 
    {
      std::cout << "generic (S<T>::info())\n";
    }
};

template<>
class S<void> 
{
  public:
    void msg() 
    {
      std::cout << "fully specialized (S<void>::msg())\n";
    }
}

我们看到,全局特化的实现并不需要与(原来的)泛型实现有任何关联,这就允许我们可以包含不同名称的成员函数(info相对msg)。实际上,全局特化只和类模板的名称有关联。 指定的模板实参列表必须和相应的模板参数列表一一对应。例如,我们不能用一个非类型值来替换一个模板类型参数。然而,如果模板参数具有缺省模板实参,那么用来替换的模板实参就是可选的(即不是必须的)

template<typename T>
class Types 
{
public:
    typedef int I;
};
 
template<typename T, typename U = typename Types<T>::I>
class S;           // (1)
 
template<> class S<void>  // (2)
{       
public:
    void f();
};
template<> class S<char, char>; // (3)
template<> class S<char, 0>;  // 错误:不能用0来替换U
 
int main()
{
    S<int>*   pi;  // 正确:使用(1),这里不需要定义
    S<int>   e1;  // 错误:使用(1),需要定义,但找不到定义
    S<void>*  pv;  // 正确:使用(2)
    S<void,int> sv;  // 正确:使用(2),这里定义是存在的
    S<void,char> e2;  // 错误:使用(1),需要定义,但找不到定义
    S<char,char> e3;  // 错误:使用(3),需要定义,但找不到定义
}
 
template<>
class S<char, char> // (3)处的定义
{};

(模板)全局特化的声明并不一定是定义。另外,当一个全局特化声明之后,针对该(特化的)模板实参列表的调用,将不再使用模板的泛型定义,而是使用这个全局特化的定义。因此,如果在调用处需要该特化的定义,而在这之前并没有提供这个定义,那么程序将会出现错误。对于类模板特化而言,“前置声明”类型有时候是很有用的,因为这样就可以构造相互依赖的类型。另外,以这种方式获得的全局特化声明(应该记住它并不是模板声明)和普通的类声明是类似的,唯一的区别在于语法以及该特化的声明必须匹配前面的模板声明。

对于特化声明而言,因为它并不是模板声明,所以应该使用(位于类外部)的普通成员定义语法,来定义全局类模板特化的成员(也就是说,不能指定template<>前缀):


template<typename T>
class S;
 
template<> class S<char**> 
{
public:
    void print() const;
};
 
//下面的定义不能使用template<>前缀
void S<char**>::print() const
{
    std::cout << "pointer to pointer to char\n";
}
template<typename T>
class Outside 
{
public:
    template<typename U>
    class Inside {
    };
};
 
template<>
class Outside<void> 
{
// 下面的嵌套类和前面定义的泛型模板之间并不存在联系
    template<typename U>
    class Inside
    {
    private:
        static int count;
    };
};
 
//下面的定义不能使用template<>前缀
template<typename U>
int Outside<void>::Inside<U>::count = 1;

可以用全局模板特化来代替对应泛型模板的某个实例化体。然而,全局模板特化和由模板生成的实例化版本是不能够共存于同一个程序中的。如果试图在同一个文件中使用这两者的话,那么通常都会导致一个编译期错误:

template <typename T>
class Invalid {
};
 
Invalid<double> x1;  // 产生一个Invalid<double>实例化体
 
template<>
class Invalid<double>; // 错误:Invalid<double>已经被实例化了

全局的函数模板特化

就语法及其后所蕴涵的原则而言,(显式的)全局函数模板特化和类模板特化大体上是一致的,唯一的区别在于:函数模板特化引入了重载和实参演绎这两个概念。

如果可以借助实参演绎(用实参类型来演绎声明中给出的参数类型)来确定模板的特殊化版本,那么全局特化就可以不声明显式的模板实参。让我们考虑下面的例子:


template<typename T>
int f(T)       // (1)
{
    return 1;
}
 
template<typename T>
int f(T*)       // (2)
{
    return 2;
}
 
template<> int f(int)  // OK: (1)的特化
{
    return 3;
}
 
template<> int f(int*) // OK: (2)的特化。
{
    return 4;
}

全局函数模板特化不能包含缺省的实参值。然而,对于基本(即要被特化的)模板所指定的任何缺省实参,显式特化版本都可以应用这些缺省实参值。例如:

template<typename T>
int f(T, T x = 42)
{
    return x;
}
 
template<> int f(int, int = 35) // 错误,不能包含缺省实参值
{
    return 0;
}
 
template<typename T>
int g(T, T x = 42)
{
    return x;
}
 
template<> int g(int, int y)
{
    return y/2;
}
 
int main()
{
    std::cout << g(0) << std::endl; // 正确,输出21
}

全局成员特化

除了成员模板之外,类模板的成员函数和普通的静态成员变量也可以被全局特化;实现特化的语法会要求给每个外围类模板加上template<>前缀。如果要对一个成员模板进行特化,也必须加上另一个template<>前缀,来说明该声明表示的是一个特化。为了说明这些含义,让我们假设具有下面的声明:


template<typename T>
class Outer 
{             // (1)
public:
    template<typename U>
    class Inner 
    {       // (2)
    private:
        static int count;       // (3)
    };
 
    static int code;        // (4)
 
    void print() const           // (5)
    {        
        std::cout << "generic";
    }
};
 
template<typename T>
int Outer<T>::code = 6;        // (6)
 
template<typename T> 
template<typename U>
int Outer<T>::Inner<U>::count = 7;  // (7)
 
template<> class Outer<bool> 
{          // (8)
public:
    template<typename U>
    class Inner 
    {           // (9)
    private:
        static int count;       // (10)
    };
 
void print() const {        // (11)
}
};

在(1)处的泛型模板Outer中,(4)处的code和(5)处print(),这两个普通成员都具有一个外围类模板。因此,需要使用一个template<>前缀说明:后面将用一个模板实参集来对它进行全局特化:


template<>
int Outer<void>::code = 12;
 
template<>
void Outer<void>::print() const
{
    std::cout << "Outer<void>";
}

这些定义将会用于替代类Outer<void>在(4)处和(5)处的泛型定义;但是,类Outer<void>的其它成员仍然默认地产生自(1)处的模板。另外,在提供了上面的声明之后,就不能再次提供Outer<void>的显式特化。

类似于全局函数模板特化,我们需要一种可以在不指定定义的前提下(为了避免多处定义),可以声明类模板普通成员特化的方法。尽管对于普通类的成员函数和静态成员变量而言,非定义的类外声明在C++中是不允许的;但如果是针对类模板的特化成员,该声明则是合法的。也就是说,前面的定义可以具有如下声明:

template<>
int Outer<void>::code;
template<>
void Outer<void>::print() const;

对于成员模板Outer<T>::Inner,也可以用一个特定的模板实参对它进行特化,而且对于该特化所在的外围 Outer<T>而言,这个特化操作并不会影响 Outer<T>相应实例化体的其它成员。另外,由于存在一个外围模板(也就是Outer<T>),所以我们需要添加一个template<>前缀。最后所获得的代码大致如下:

template<> template<typename X>
class Outer<wchar_t>::Inner 
{
public:
    static long count; // 成员类型发生了改变
};
 
template<> template<typename X>
long Outer<wchar_t>::Inner<X>::count;

模板Outer::Inner也可以被全局特化,但只能针对Outer的某个给定实例。而且,我们需要添加两个 template<>前缀:因为外围类需要一个 template<>前缀,我们所要全局特化的内围模板(inner template)也需要一个template<>前缀:

template<>
template<>
class Outer<char>::Inner<wchar_t> 
{
public:
    enum { count = 1};
};

// 下面的C++程序是不合法的: // template<> 不能位于模板实参列表的后面

template<typename X>
template<> class Outer<X>::Inner<void>; // 错误
// 我们可以将上面这个特化与Outer<bool>的成员模板的特化比较一下。由于Outer<bool>已经在前面全局特化了,所有它的成员模板也就不存在外围模板,因此我们就只需要一个template<>前缀:

template<>
class Outer<bool>::Inner<wchar_t> 
{
public:
    enum { count = 2 };
};

元编程

类型参数(type template parameter),用 typename 或 class 标记; 非类型参数(non-type template parameter)可以是:整数及枚举类型、对象或函数的指针、对象或函数的引用、对象的成员指针,非类型参数是模板实例的常量; 模板型参数(template template parameter),如“template<typename T, template class A> someclass {};”; 模板参数可以有默认值(函数模板参数默认是从 C++11 开始支持); 函数模板的和函数参数类型有关的模板参数可以自动推导,类模板参数不存在推导机制; C++11 引入变长模板参数

链接

https://stackoverflow.com/questions/2183087/why-cant-i-use-float-value-as-a-template-parameter https://stackoverflow.com/questions/38026884/advantages-of-auto-in-template-parameters-in-c17 https://zhuanlan.zhihu.com/p/314340244 https://blog.csdn.net/u012481976/article/details/84502874