# Template编程

# 1.NoneType Arguments (opens new window)

常规模板编程时的参数Arguments是带类型的,形如

template<typename T1, typename T2, ..., typename Tn>
void func(...)
{

}

除了向上面这样使用类型参数外,还可以使用非类型参数,

template<typename T, int N>
T* func()
{
    T a[N];
    return a;
}

void main(void)
{
    auto p = func<int,4>();
}

第二个模板参数是指定了int类型,除此之外,还能给定初始值,形如:

template<typename T, int N=5>
...

这种使用方法在C++标准模板库STL中也比较常见,如array的实现部分,

template<typename _Tp, std::size_t _Nm>
struct array
{
    ...
}

# 2.class 与 typename的区别

在定义模板时,经常会classtypename混合使用,其实class是更早的定义template的方法,后来引入了typename

大部分情况下typenameclass是通用了,但在需要告诉编译器一个变量是类型名时,需要使用typename而不能使用class

template<class T>
T add(T x, T y) { return x + y; }

template<typename T>
T add(T x, T y) { return x + y; }

// Both cases are OK.

template<typename T>
struct basic_type { using type = T; }

template<typename T>
using Type = typename basic_type<T>::type; // has to use typename instead of class

# 3.template parameter and argument

一个模板的定义包括由1到多个模板参数,通常模板的声明形式如下:

template < parameter-list > declaration;

参数列表parameter-list中的参数可能为:

  • non-type 模板参数
  • 类型模板参数
  • 模板模板参数

// non-type模板参数

template<int n>
struct B { /* ... */ };

// 类型模板参数
template<class T>
class My_vector { /* ... */ };

// 模板模板参数:

template<typename T>
class my_array {};
 
// two type template parameters and one template template parameter:
template<typename K, typename V, template<typename> typename C = my_array>
class Map
{
    C<K> key;
    C<V> value;
};

template argument是实例化模板时传入的值,parameter的含义是指变量,也就是常说的形参,argument是传入的值,也就是常说的实参。

# 3.Template中的打包参数typename...Args

模板参数打包template parameter pack是指能够接受零个或多个模板参数的变量,同函数一样,函数参数打包是能够接受零或多个参数的变量。

参数打包的展开方式为pattern...

在类模板中打包参数只能在模板参数的最后出现,

template<typename U, typename... Ts>    // OK: can deduce U
struct valid;

在模板函数中,使用因为类型可以从函数声明中推断,因此打包参数可以不总在参数最后出现。

template<typename... Ts, typename U, typename=void>
void valid(U, Ts...);    // OK: can deduce U
// void valid(Ts..., U); // Can't be used: Ts... is a non-deduced context in this position
 
valid(1.0, 1, 2, 3);     // OK: deduces U as double, Ts as {int, int, int}

对变参函数的常用操作:

  • sizeof...查看参数个数
  • 打印变参中的每个参数
template<typename... Ts>
void func(Ts... args)
{
    const int size = sizeof...(args) + 2;
    int res[size] = {1, args..., 2};
 
    // since initializer lists guarantee sequencing, this can be used to
    // call a function on each element of a pack, in order:
    int dummy[sizeof...(Ts)] = {(std::cout << args, 0)...};
}

一个使用参数打包的例子,

template<typename T>
class Creator;


template<typename Ret, typename... Args>
class Creator<Ret(Args...)>
{
    public:
        using FuncType = std::function<Ret(Args...)>;
        explicit Creator(std::string_view name, FuncType func) noexcept : 
        name_(name), func_(func) {}

        Ret Create(Args... args) { return func_(args...);}
    private:
        FuncType func_;
        std::string_view name_;
};

int add(int x, int y)
{
    return x + y;
}

void main(void)
{
    std::string_view s("add1");
    Creator<int(int,int)> creator(s, add);
    int r = creator.Create(1, 2);
    std::cout << r << "\n";    
}

通过上面这种方式可以创建一个函数容器,用来作为工厂模式的创建器,以包含工厂方法。

# 4.模板编译

当编译器遇到一个模板定义时,它并不生成代码。只有在实例化出一个模板的特定版本时,编译器才会生成代码。当使用模板而不是定义模板时,编译器才生成代码,这一特性会影响代码如何组织代码以及错误何时被检出。

在调用一个函数时,编译器只需要掌握函数的声明。类似,在使用一个类类型时,类定义必须可用,但成员函数只需要声明,不一定定义。因此,在编写代码时,通常可以将类定义和函数声明放在头文件中,而普通函数和类的成员函数放在源文件中。

而对于模板来说,为了生成一个实例化的版本,编译器需要掌握函数模板或类模板成员函数的定义。因此,与非模板代码不同,模板的头文件通常既包括声明也包括定义。