模板基本语法

模板参数:typename, class和具体类型

class和typename的作用一样,一般写成typename。调用时传一个具体的类型。
也可以在模板参数上写一个具体的类型,调用时需要传该类型的一个实例。

  1. template<class C1, int, typename C2>
  2. class C3;
  3. C3<float, 5, double> c3;

模板的编译和实例化

参考:https://docs.microsoft.com/zh-cn/cpp/cpp/source-code-organization-cpp-templates?view=msvc-160
https://blog.csdn.net/flowshell/article/details/5999588
模板的实例化有显式指定、实参推演、显式实例化三种方式。
类模板实例化时必须显示指定类型,函数模板可以进行实参推演。
模板本身不对应任何代码,一个模板只有实例化的时候编译器才会生成代码。
惰性实例化:对于类模板,如果碰到一个新的实例化,编译器才会生成一个新的类。并且没使用过的模板类的成员函数(即函数模板)也不会被实例化。
实例化的时候编译器需要找到模板的声明和模板的定义。

The inclusion model(包含模型)

让实例化的cpp文件同时看到模板的声明和定义,在一个.h文件中同时包含类模板和函数的完整定义,或者同时包含分离的两个.h文件和.cpp文件。
编译慢,两个.cpp文件中有重复内容(编译的第一步是把.h文件插入cpp文件),但是灵活性高。

The explicit instantiation model(显式实例化模型)

模板的声明和定义分离,函数的定义写到cpp文件中,实例化的cpp文件不能同时看到模板的声明和定义。
如果不使用显式实例化,在两个cpp文件中编译器都无法完成模板的实例化(实例化的地方看不到模板的定义,模板的定义处看不到实例化参数)。
编译快(工作丢给链接器了),但是需要使用显式实例化,否则就会出现链接错误。
显式实例化没有办法惰性实例化模板类的成员函数(函数模板),所有成员函数都会被实例化。

// 显示实例化:在.cpp文件中指出要实例化的具体类型。
#include <iostream>
#include "MyArray.h"

using namespace std;

template<typename T, size_t N>
MyArray<T,N>::MyArray(){}

template<typename T, size_t N>
void MyArray<T,N>::Print()
{
    for (const auto v : arr)
    {
        cout << v << "'";
    }
    cout << endl;
}

// 注意这里,指明了只实例化 double 和 string 两个类型
template MyArray<double, 5>;template MyArray<string, 5>;

类模板

参考:https://docs.microsoft.com/zh-cn/cpp/cpp/templates-cpp?view=msvc-160
https://www.runoob.com/w3cnote/c-templates-detail.html
有三种类型的模板形参:类型形参,非类型形参和默认模板形参。
类型模板形参:类型形参由关见字class或typename后接说明符构成。
模板的非类型形参也就是内置类型形参,如template class B{};其中int a就是非类型的模板形参。
带默认值的模板形参,应该放在最后面。
类模板形参不存在实参推演的问题。也就是说不能把整型值2推演为int 型传递给模板形参。要把类模板形参调置为int 型必须这样指定A m。(类声明时必须指示清楚参数是什么类型,如vecor,不存在vector<1>这种写法。)

template <typename T>
T minimum(const T& lhs, const T& rhs)
{
    return lhs < rhs ? lhs : rhs;
}

template<class T1, class T2 = int>
class A {
public:
    void h();
};

// 在类模板的外部定义类中的成员时template 后的形参表应省略默认的形参类型,这里没有写T2=int
template<class T1, class T2>
void A<T1,T2>::h() {
}

函数模板

对函数模板的调用可以使用实参推演来进行,也可以显示指定模板形参的类型。
C98中函数模板不能指定默认值,从C++11开始函数模板也可以指定默认值。
当函数模板拥有多个默认模板参数时,其出现的顺序可以任意,不需要连续出现在模板参数的最后面。
类模板的成员函数都是函数模板。
没使用过的成员函数(即函数模板)不会被实例化。

模板特化

函数模板不允许偏特化,直接用函数重载即可

// 类模板
template <class T1, class T2>
class A{
    T1 data1;
    T2 data2;
};

// 全特化类模板
template <>
class A<int, double>{
    int data1;
    double data2;
};

// 偏特化类模板
template <class T2>
class A<int, T2>{
    ...
};

// 偏特化类模板,T1和T2必须包含const
template <class T1, class T2>
class A<T1 const, T2 const>{
    T1 data1;
    T2 data2;
};

SFINAE

Substitution failure is not an error
下面这段代码不会报错,因为int::foo适配错误不算错误,只要能找到一个可以实例化的版本就行。

struct Test {
    typedef int foo;
};

template <typename T> 
void f(typename T::foo) {} // Definition #1

template <typename T> 
void f(T) {}               // Definition #2

int main() {
    f<Test>(10); // Call #1.
    f<int>(10);  // Call #2. Without error (even though there is no int::foo) thanks to SFINAE.
}

模板递归

template<int N>
struct Factorial
{
    enum { Value = N * Factorial<N - 1>::Value };
};

template<>
struct Factorial<1>
{
    enum { Value = 1 };
};

元函数

仿函数和元函数都是类,仿函数重载()运算符伪装成普通函数。
元函数是一个模板类,通过class_name::value的方式获取想要的值,value是一个静态成员函数。

值元函数

// Simple regular function: identity
int int_identity(int x) { 
    return x;
}
assert(42 == int_identity(42));

// Simple metafunction: identity
template <int X>
struct IntIdentity {
    static constexpr int value = X;
}
static_assert( 42 == IntIdentity<42>::value);

// Simple metafunction: identity
template <typename T, T X>
struct IntIdentity {
    static constexpr T value = X;
}
static_assert( 42 == IntIdentity<unsigned long long, 42ull>::value);

// C++17以后
template <auto X>
struct IntIdentity {
    static constexpr auto value = X;
}
static_assert( 42ull == IntIdentity<42ull>::value);

// 用变量模板来调用变量元函数
template <auto X>
inline constexpr auto ValueIdentity_v = IntIdentity<X>::value;

static_assert( 42ull == ValueIdentity_v<42>);

// 求和函数示例
template <auto x, auto y>
struct Sum {
    static constexpr auto value = x + y;
}
static_assert( 52ull == IntIdentity<42ull, 10u>::value);

// 注意,这个函数在实际场景中用constexpr更好
template <typename X, Typename Y>
constexpr auto Sum(X x, Y y)
{
    return x + y;
}

类型元函数

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

TypeIdentity<int>::type

//这里T是一个不确定类型,为了让编译器知道TypeIdentity<T>::type是一个类型,必须在前面加上typename
typename TypeIdentity<T>::type 

// 用变量模板省去typename
template <typename T>
using TypeIdentity_t = typename TypeIdentity<T>::type;

static_assert(std::is_same_v<int, TypeIdentity_t<int>>);

type trait

所有type trait都继承std::integral_constant。既要包含value,也要包含type

std::integral_constant

既是值元函数,也是类型元函数。所有type trait都继承std::integral_constant。
输入什么值value就是什么值,输入什么类型type就是什么类型。

template<class T, T v>
struct integral_constant {
    static constexpr T value = v;

    using value_type = T;
    using type = integral_constant; // using injected-class-name

    constexpr operator value_type() const noexcept { return value; }
    constexpr value_type operator()() const noexcept { return value; } // since c++14
};

bool_constant

template <bool B>
using bool_constant = integral_constant<bool, B>;

using true_type = std::integral_constant<bool, true>
using false_type = std::integral_constant<bool, false>

std::is_void
模板特化,只有传入void时value为true

template <typename T>
struct is_void : std::false_type{};

template <> struct is_void<void> : std::true_type{};
template <> struct is_void<void const> : std::true_type{};
template <> struct is_void<void volatile> : std::true_type{};
template <> struct is_void<void const volatile> : std::true_type{};

Transformation Trait

只需要包含返回一个type

std::remove_cv, std::remove_const, std::remove_volatile

std::remove_const移除顶级限定符的const
注意remove_const::type是const int
remove_const<const int
>::type是const int *


template< class T > struct remove_cv                   { typedef T type; };
template< class T > struct remove_cv<const T>          { typedef T type; };
template< class T > struct remove_cv<volatile T>       { typedef T type; };
template< class T > struct remove_cv<const volatile T> { typedef T type; };

template< class T > struct remove_const                { typedef T type; };
template< class T > struct remove_const<const T>       { typedef T type; };

template< class T > struct remove_volatile             { typedef T type; };
template< class T > struct remove_volatile<volatile T> { typedef T type; };

std::add_lvalue_reference, std::add_rvalue_reference

与直接写T&的区别:如果T是void,T&会报编译错误,因为不允许引用void类型
add_lvalue_reference会将所有传进来的类型变成左值引用
std::add_lvalue_reference::type 是 T&
std::add_lvalue_reference::type 是 T&
std::add_lvalue_reference::type 是 T&
std::add_lvalue_reference::type 是 void
add_rvalue_reference会保持左值引用
std::add_rvalue_reference::type 是 T&&
std::add_rvalue_reference::type 是 T&
std::add_rvalue_reference::type 是 T&&
std::add_rvalue_reference::type 是 void

std::declval

参考:https://stdrc.cc/post/2020/09/12/std-declval/
decltype()的缺陷
decltype()不会实际运行括号内的表达式,但这个表达式需要符合语法,通过编译器和链接器的检查

struct A {
    A() = delete;
    int foo();
};

int main() {
    decltype(A().foo()) foo = 1; // 无法通过编译,因为无法构造一个A类的实例
    decltype(std::declval<A>().foo()) foo = 1; // OK
}

std::declval的原理

template<class T>
typename std::add_rvalue_reference<T>::type declval() noexcept;

declval是被声明成一个返回T类型引用的函数。这样就不需要执行构造函数也能得到T类型,可以通过编译器检查(上例中A()换成std::declval(),注意std::declval()整体是一个函数,这里没有构造函数A()了),而实际上std::declval()这个函数不需要被运行,所以只需要声明不需要实现。

class Silly { private: Silly( Silly const& ) = delete; };

auto foo() -> Silly&&;

auto main() -> int
{
    sizeof( foo() );
}

为什么返回引用?
像 int[10] 这种数组类型是不能直接按值返回的,直接宣判了返回 T 方案的死刑,在更复杂的情况下只会更糟糕,因此还是需要返回一个引用。
为什么使用 std::add_rvalue_reference 而不是 std::add_lvalue_reference?
加右值引用可以进行引用折叠,最终 T 和 T && 变 T &&,T & 还是 T &,不会改变类型的性质,但如果添加左值引用,T 就会变 T &,性质直接变了,比如声明为 int foo() & 的成员函数,本来不能访问现在可以访问,显然是错误的。

std::is_trivial

只要改了默认构造函数、拷贝构造函数、移动构造函数,就会返回false。

#include <iostream>
#include <type_traits>

struct A {
    int m;
};

struct B {
    B() {}
};

int main() 
{
    std::cout << std::boolalpha;
    std::cout << std::is_trivial<A>::value << '\n';
    std::cout << std::is_trivial<B>::value << '\n';
}

std::is_constructible, std::is_trivially_constructible, std::is_nothrow_constructible

std::is_constructible检查该类是可以用指定的参数构造,对传入类型调用std::declval。
std::is_trivially_constructible检查指定类型是否可以trivially构造。到C++17为之,trivially构造包括默认构造、拷贝构造、移动构造。从C++20开始,trivially构造也包括包括列表初始化。

class classA
{
    int a;

public:
    classA() = delete;
    classA(int _a) : a(_a) {}
};

cout << boolalpha;
// 输出false,classA没有默认构造函数
cout << "classA:" << is_constructible<classA>::value << endl;
// 输出true,classA可以传单int参数构造
cout << "classA:" << is_constructible<classA, int>::value << endl;

std::is_default_constructible

检查该类有默认构造函数(无参数的构造函数)

std::enable_if

template<bool B, class T = void>
struct enable_if {};

template<class T>
struct enable_if<true, T> { typedef T type; };

用法一:类型偏特化
check 只希望选择 value==true 的 T,否则就报编译时错误。

template <typename T, typename Enable=void>
struct check;

template <typename T>
struct check<T, typename std::enable_if<T::value>::type> {
  static constexpr bool value = T::value;
};

用法二:控制函数返回类型
对于模板函数,有时希望根据不同的模板参数返回不同类型的值,进而给函数模板也赋予类型模板特化的性质。典型的例子可以参看 tuple 的获取第 k 个元素的 get 函数:

template <std::size_t k, class T, class... Ts>
typename std::enable_if<k==0, typename element_type_holder<0, T, Ts...>::type&>::type
get(tuple<T, Ts...> &t) {
  return t.tail; 
}

template <std::size_t k, class T, class... Ts>
typename std::enable_if<k!=0, typename element_type_holder<k, T, Ts...>::type&>::type
get(tuple<T, Ts...> &t) {
  tuple<Ts...> &base = t;
  return get<k-1>(base); 
}

由于函数模板不能偏特化,通过 enable_if 便可以根据 k 值的不同情况选择调用哪个 get,进而实现函数模板的多态。
用法三:校验函数模板参数类型
有时定义的模板函数,只希望特定的类型可以调用,参考 cppreference 官网示例,很好的说明了如何限制只有整型可以调用的函数定义:

template <typename T>
typename std::enable_if<std::is_integral<T>::value, bool>::type
is_odd(T t) {
  return bool(t%2);
}

template <typename T, typename = typename std::enable_if<std::is_integral<T>::value>::type>
bool is_even(T t) {
  return !is_odd(t); 
}

一个通过返回值,一个通过默认模板参数,都可以实现校验模板参数是整型的功能。

Concept(C++20)

// 声明一个concept Addable, Addable代表该类型能相加,并且相加的结果还是该类型
template<typename T>
concept Addable = requires (T obj) {{obj + obj}->std::same_as<T>;};

// 传入类型T必须能相加,并且相加的结果还是该类型
template<Addable T>
T add(T in1, T in2)
{
    return in1 + in2;
}

CRTP,奇异递归模板模式(Curiously Recurring Template Pattern)

CRTP实现静态多态

template<typename Child>
class Animal
{
public:
    void Run()
    {
        static_cast<Child*>(this)->Run();
    }
};

class Dog :public Animal<Dog>
{
public:
    void Run()
    {
        cout << "Dog Run" << endl;
    }
};

class Cat :public Animal<Cat>
{
public:
    void Run()
    {
        cout << "Cat Run" << endl;
    }
};

template<typename T>
void Action(Animal<T> &animal)
{
    animal.Run();
}

int main()
{
    Dog dog;
    Action(dog);

    Cat cat;
    Action(cat);
    return 0;
}