跳转至

模版

为什么要用模版

看下面的这个例子.

#include <iostream>

int square(int input) {
    return input * input;
}

int main() {
    std::cout << square(5) << std::endl;
    std::cout << square(5.5) << std::endl;
    std::cout << square(5.5f) << std::endl;
    return 0;
}

输出:

25
25

hmmm. 怎么全是25, 这是因为都调用的是int square(int input)这个函数. 所以你要使用重载把用到的每个类型都写上, 例如.

int square(int input) {
    return input * input;
}

float square(float input) {
    return input * input;
}

double square(double input) {
    return input * input;
}

int main() {
    std::cout << square(5) << std::endl;
    std::cout << square(5.5) << std::endl;
    std::cout << square(5.5f) << std::endl;
    return 0;
}

所以, 最后的结果是25, 30.25, 30.25. 但是, 我们写了很多个类似只是类型不同的函数, 如果我们要修改其中一个函数的逻辑, 就要修改其他所有函数的逻辑, 这是非常的耗时耗力的, 所以有没有什么方法只写一个"template", 然后在我们要用到的时候生成他的类型的代码呢? 这就是模版的作用. 上述的代码可以写为:

#include <iostream>

template <typename T>
T square(T input) {
    return input * input;
}

int main() {
    std::cout << square<int>(5) << std::endl;
    std::cout << square<double>(5.5) << std::endl;
    std::cout << square<float>(5.5f) << std::endl;
    return 0;
}

函数模版

使用template

就是上面这么用.

template <typename T, typename U, typename V>
T foo(const U& u, const V& v) {};
int main() {
    foo<U, V>(u, v)
    return 0;
}

T, U, V的生命周期是这个函数内.

#include <iostream>

template <typename T>
T square(T input) {
    return input * input;
}

int main() {
    std::cout << square<int>(5) << std::endl;
    std::cout << square<double>(5.5) << std::endl;
    std::cout << square<float>(5.5f) << std::endl;
    return 0;
}

这段代码在编译器的眼里其实是这样的:

#include <iostream>

template<typename T>
T square(T input)
{
  return input * input;
}

/* First instantiated from: insights.cpp:9 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
int square<int>(int input)
{
  return input * input;
}
#endif


/* First instantiated from: insights.cpp:10 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
double square<double>(double input)
{
  return input * input;
}
#endif


/* First instantiated from: insights.cpp:11 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
float square<float>(float input)
{
  return input * input;
}
#endif


int main()
{
  std::cout.operator<<(square<int>(5)).operator<<(std::endl);
  std::cout.operator<<(square<double>(5.5)).operator<<(std::endl);
  std::cout.operator<<(square<float>(5.5F)).operator<<(std::endl);
  return 0;
}

使用auto自动推断返回值类型

可以使用auto自动推断返回值的类型, 例如:

template <typename T1, typename T2>
auto Multiply(const T1& a, const T2& b) {
    return a * b;
}

使用auto

另外一种实现模版的方法是使用auto, 它会帮我们自动推断类型.

#include <iostream>

auto square(auto input) {
    return input * input;
}

int main()
{
  std::cout.operator<<(square<int>(5)).operator<<(std::endl);
  std::cout.operator<<(square<double>(5.5)).operator<<(std::endl);
  std::cout.operator<<(square<float>(5.5F)).operator<<(std::endl);
  return 0;
}

上述的这段代码在编译器的眼里其实是这样的:

#include <iostream>

template<class type_parameter_0_0>
auto square(type_parameter_0_0 input)
{
  return input * input;
}

#ifdef INSIGHTS_USE_TEMPLATE
template<>
int square<int>(int input)
{
  return input * input;
}
#endif


#ifdef INSIGHTS_USE_TEMPLATE
template<>
double square<double>(double input)
{
  return input * input;
}
#endif


#ifdef INSIGHTS_USE_TEMPLATE
template<>
float square<float>(float input)
{
  return input * input;
}
#endif


int main()
{
  std::cout.operator<<(square<int>(5)).operator<<(std::endl);
  std::cout.operator<<(square<double>(5.5)).operator<<(std::endl);
  std::cout.operator<<(square<float>(5.5F)).operator<<(std::endl);
  return 0;
}

你会发现, 其实和用template没什么两样.


怎么得到上面的'编译器眼里'的代码的

使用这个网站: C++ Insights.

传入非对象参数

有时候, 我们需要模版帮助我们填写一些非对象的参数, 例如在std::array的实现中:

template <class T, std::size_t N> struct array;

再举个例子:

#include <iostream>

template <typename T1, size_t N>
void foo(T1 input1) {
    for (size_t i = 0; i < N; i++) {
        std::cout << "hello" << std::endl;
    }
}

int main() {
    foo<int, 5>(5);
    foo<int, 4>(5);
    foo<int, 3>(5);
    return 0;
}

上述的代码在编译器眼里是这样的:

#include <iostream>

template<typename T1, size_t N>
void foo(T1 input1)
{
  for(size_t i = 0; i < N; i++) {
    std::operator<<(std::cout, "hello").operator<<(std::endl);
  }

}

/* First instantiated from: insights.cpp:11 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
void foo<int, 5>(int input1)
{
  for(size_t i = 0; i < 5UL; i++) {
    std::operator<<(std::cout, "hello").operator<<(std::endl);
  }

}
#endif


/* First instantiated from: insights.cpp:12 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
void foo<int, 4>(int input1)
{
  for(size_t i = 0; i < 4UL; i++) {
    std::operator<<(std::cout, "hello").operator<<(std::endl);
  }

}
#endif


/* First instantiated from: insights.cpp:13 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
void foo<int, 3>(int input1)
{
  for(size_t i = 0; i < 3UL; i++) {
    std::operator<<(std::cout, "hello").operator<<(std::endl);
  }

}
#endif


int main()
{
  foo<int, 5>(5);
  foo<int, 4>(5);
  foo<int, 3>(5);
  return 0;
}

部分模板化

有些时候, 我们对所有的类都定义了一个统一的模版, 这可能不是我们想要的, 比如对于int, char来说, 能用==判断两个数是否相等; 但是对于float, double来说, 是无法通过==判断两个数是否相等的, 例如下面的代码:

#include <iostream>

template <typename T>
bool equal(T a, T b) {
    return a == b;
}

int main() {
    std::cout << equal<int>(1, 1) << std::endl;
    std::cout << equal<float>(1.0f-0.999999f, 0.000001f) << std::endl;
    return 0;
}

输出:

1
0 # 这里是0, 说明两个数是不相等的, 但是实际上是相等的

所以我们就想, 能不能为float专门写一个模版呢? 是可以的:

#include <iostream>
#include <cmath>

template <typename T>
bool equal(T a, T b) {
    return a == b;
}

template<>
bool equal<float>(float a, float b) {
    std::cout << "partial template called" << std::endl;
    return fabs(a - b) < 0.00001f;
}

template<>
bool equal<double>(double a, double b) {
    std::cout << "partial template called" << std::endl;
    return abs(a - b) < 0.00001;
}

int main() {
    std::cout << equal<int>(1, 1) << std::endl;
    std::cout << equal<float>(1.0f-0.999999f, 0.000001f) << std::endl;
    return 0;
}

你会发现, 就是从C++ insights里面挑了一个用float重载的函数出来, 然后稍微改了一下. 输出:

1
partial template called
1

可变参数模版

可变参数模板(Variadic Templates)是 C++11 引入的一个强大的特性. 它允许你创建可以接受任意数量参数的函数和类模板, 而不需要在模板定义时指定参数的个数和类型

#include <iostream>

template<typename T>
T Sum(T arg){ // 基本情况: 只有一个参数
    return arg;
}

template<typename T, typename... Args>
T Sum(T start, Args... args){ // 递归情况: 至少有两个参数
    return start + Sum(args...); // 将第一个参数与剩余参数的和相加
}

int main() {
    int sum1 = Sum(1);        // 调用 Sum(int)
    // 1. 匹配到 template<typename T> T Sum(T arg)
    // 2. T 被推导为 int, arg 的值为 1
    // 3. 返回 arg, 即返回 1

    double sum2 = Sum(1.1, 2.2, 3, 4.4);
    // 请见Cpp insights

    std::cout << sum1 << ", " << sum2 << std::endl;  // 输出: 1, 10.3
    return 0;
}

上述的代码在编码器眼里是这样的:

#include <iostream>

template<typename T>
T Sum(T arg)
{
  return arg;
}

/* First instantiated from: insights.cpp:14 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
int Sum<int>(int arg)
{
  return arg;
}
#endif


/* First instantiated from: insights.cpp:10 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
double Sum<double>(double arg)
{
  return arg;
}
#endif


template<typename T, typename ... Args>
T Sum(T start, Args... args)
{
  return start + Sum(args... );
}

#ifdef INSIGHTS_USE_TEMPLATE
template<>
int Sum<int>(int start);
#endif


/* First instantiated from: insights.cpp:19 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
double Sum<double, double, int, double>(double start, double __args1, int __args2, double __args3)
{
  return start + Sum(__args1, __args2, __args3);
}
#endif


/* First instantiated from: insights.cpp:10 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
double Sum<double, int, double>(double start, int __args1, double __args2)
{
  return start + static_cast<double>(Sum(__args1, __args2));
}
#endif


/* First instantiated from: insights.cpp:10 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
int Sum<int, double>(int start, double __args1)
{
  return static_cast<int>(static_cast<double>(start) + Sum(__args1));
}
#endif


#ifdef INSIGHTS_USE_TEMPLATE
template<>
double Sum<double>(double start);
#endif


int main()
{
  int sum1 = Sum(1);
  double sum2 = Sum(1.1000000000000001, 2.2000000000000002, 3, 4.4000000000000004);
  std::operator<<(std::cout.operator<<(sum1), ", ").operator<<(sum2).operator<<(std::endl);
  return 0;
}

可以看到, 这是一个递归的逻辑, 当然, 如果我这样写:

#include <iostream>

template<typename T>
T Sum(T arg){ // 基本情况: 只有一个参数
    return arg;
}

template<typename T, typename... Args>
T Sum(T start, Args... args){ // 递归情况: 至少有两个参数
    return start + Sum(args...); // 将第一个参数与剩余参数的和相加
}

int main() {
    int sum1 = Sum(1);        // 调用 Sum(int)
    // 1. 匹配到 template<typename T> T Sum(T arg)
    // 2. T 被推导为 int, arg 的值为 1
    // 3. 返回 arg, 即返回 1

    double sum2 = Sum<double, double, double>(1.1, 2.2, 3, 4.4);
    // 请见Cpp insights

    std::cout << sum1 << ", " << sum2 << std::endl; // 输出: 1, 10.7
    return 0;
}

再看一下C++ insights的代码:

#include <iostream>

template<typename T>
T Sum(T arg)
{
  return arg;
}

/* First instantiated from: insights.cpp:14 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
int Sum<int>(int arg)
{
  return arg;
}
#endif


/* First instantiated from: insights.cpp:10 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
double Sum<double>(double arg)
{
  return arg;
}
#endif


template<typename T, typename ... Args>
T Sum(T start, Args... args)
{
  return start + Sum(args... );
}

#ifdef INSIGHTS_USE_TEMPLATE
template<>
int Sum<int>(int start);
#endif


/* First instantiated from: insights.cpp:19 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
double Sum<double, double, double, double>(double start, double __args1, double __args2, double __args3)
{
  return start + Sum(__args1, __args2, __args3);
}
#endif


/* First instantiated from: insights.cpp:10 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
double Sum<double, double, double>(double start, double __args1, double __args2)
{
  return start + Sum(__args1, __args2);
}
#endif


/* First instantiated from: insights.cpp:10 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
double Sum<double, double>(double start, double __args1)
{
  return start + Sum(__args1);
}
#endif


#ifdef INSIGHTS_USE_TEMPLATE
template<>
double Sum<double>(double start);
#endif


int main()
{
  int sum1 = Sum(1);
  double sum2 = Sum<double, double, double>(1.1000000000000001, 2.2000000000000002, 3, 4.4000000000000004);
  std::operator<<(std::cout.operator<<(sum1), ", ").operator<<(sum2).operator<<(std::endl);
  return 0;
}

为啥要写成<double, double, double>

你会发现生成的代码中原本的int变成了double. 这其实是正确的做法, 为啥呢? 因为最后一个元素4.4double类型的, 如果第三个元素不声明是double, 那么这个函数:

/* First instantiated from: insights.cpp:10 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
int Sum<int, double>(int start, double __args1)
{
return static_cast<int>(static_cast<double>(start) + Sum(__args1));
}
#endif

由于上面的函数要求返回一个int, 会使用static_cast<int>static_cast<double>(start) + Sum(__args1)的结果7.4强制转为7, 所以有0.4消失了, 这就是为什么结果是1.1+2.2+7=10.3. 而如果我们使用<double, double, double>, 声明第三个是double, 就不会有static_cast<int>这样的转换, 就不会损失精度了, 所以输出是正确的10.7.

模板类

模板类的思想是和函数模版是差不多的.

#include <iostream>

template <typename T>
class Container {
    public:
        Container(int N) {
            m_data = new T[N];
        }
        ~Container() {
            delete[] m_data;
        }
    private:
        T* m_data;
};

int main() {
    Container<int> c{10};
    Container<double> c2{10};
    Container<float> c3{10};
    return 0;
}

在编译器眼里是这样的:

#include <iostream>

template<typename T>
class Container
{

  public:
  inline Container(int N)
  {
    this->m_data = new T[static_cast<unsigned long>(N)];
  }

  inline ~Container()
  {
    delete[] this->m_data;
  }


  private:
  T * m_data;
};

/* First instantiated from: insights.cpp:17 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
class Container<int>
{

  public:
  inline Container(int N)
  {
    this->m_data = new int[static_cast<unsigned long>(N)];
  }

  inline ~Container() noexcept
  {
    delete[] this->m_data;
  }


  private:
  int * m_data;
  public:
};

#endif
/* First instantiated from: insights.cpp:18 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
class Container<double>
{

  public:
  inline Container(int N)
  {
    this->m_data = new double[static_cast<unsigned long>(N)];
  }

  inline ~Container() noexcept
  {
    delete[] this->m_data;
  }


  private:
  double * m_data;
  public:
};

#endif
/* First instantiated from: insights.cpp:19 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
class Container<float>
{

  public:
  inline Container(int N)
  {
    this->m_data = new float[static_cast<unsigned long>(N)];
  }

  inline ~Container() noexcept
  {
    delete[] this->m_data;
  }


  private:
  float * m_data;
  public:
};

#endif

int main()
{
  Container<int> c = Container<int>{10};
  Container<double> c2 = Container<double>{10};
  Container<float> c3 = Container<float>{10};
  return 0;
}

带有静态成员变量

有时候, 可能在模板类中需要包含一个静态的成员变量, 比如:

#include <iostream>

template <typename T>
class Container {
    public:
        Container(int N) {
            m_data = new T[N];
        }
        ~Container() {
            delete[] m_data;
        }

        static T m_variable;
    private:
        T* m_data;
};

int main() {
    Container<int> c{10};
    Container<double> c2{10};
    Container<float> c3{10};
    return 0;
}

上述的代码在编译器眼里是这样的:

#include <iostream>

template<typename T>
class Container
{

  public:
  inline Container(int N)
  {
    this->m_data = new T[static_cast<unsigned long>(N)];
  }

  inline ~Container()
  {
    delete[] this->m_data;
  }

  static T m_variable;

  private:
  T * m_data;
};

/* First instantiated from: insights.cpp:19 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
class Container<int>
{

  public:
  inline Container(int N)
  {
    this->m_data = new int[static_cast<unsigned long>(N)];
  }

  inline ~Container() noexcept
  {
    delete[] this->m_data;
  }

  static int m_variable;

  private:
  int * m_data;
  public:
};

#endif
/* First instantiated from: insights.cpp:20 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
class Container<double>
{

  public:
  inline Container(int N)
  {
    this->m_data = new double[static_cast<unsigned long>(N)];
  }

  inline ~Container() noexcept
  {
    delete[] this->m_data;
  }

  static double m_variable;

  private:
  double * m_data;
  public:
};

#endif
/* First instantiated from: insights.cpp:21 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
class Container<float>
{

  public:
  inline Container(int N)
  {
    this->m_data = new float[static_cast<unsigned long>(N)];
  }

  inline ~Container() noexcept
  {
    delete[] this->m_data;
  }

  static float m_variable;

  private:
  float * m_data;
  public:
};

#endif

int main()
{
  Container<int> c = Container<int>{10};
  Container<double> c2 = Container<double>{10};
  Container<float> c3 = Container<float>{10};
  return 0;
}

你会发现, 在每个生成的模板类的示例中, 都会有一个不的静态m_variable. 第一个m_variable的所属类是Container<int>, 第二个m_variable的所属类是Container<double>, 第三个m_variable的所属类是Container<float>, 所以这三个m_variable实际上在静态区上的位置是不同的, 需要通过Container<int>::m_variable来访问第一个静态变量, 以此类推. 当然, 必须在全局作用域进行一个声明: template <typename T> T Container<T>::m_variable;. 所以正确的代码应该写为:

#include <iostream>

template <typename T>
class Container {
    public:
        Container(int N) {
            m_data = new T[N];
        }
        ~Container() {
            delete[] m_data;
        }

        static T m_variable;
    private:
        T* m_data;
};

template <typename T>
T Container<T>::m_variable;

int main() {
    Container<int> c{10};
    Container<double> c2{10};
    Container<float> c3{10};
    Container<int>::m_variable = 10;
    Container<double>::m_variable = 10.5;
    Container<float>::m_variable = 10.4f;
    return 0;
}

你会发现编译器帮我们生成了这些声明:

#ifdef INSIGHTS_USE_TEMPLATE
int Container<int>::m_variable;
#endif
#ifdef INSIGHTS_USE_TEMPLATE
double Container<double>::m_variable;
#endif
#ifdef INSIGHTS_USE_TEMPLATE
float Container<float>::m_variable;
#endif

CTAD

CTAD, Class Template Argument Deduction, 简单来说, 它是 C++17 引入的一个特性, 允许编译器在创建类模板的对象时, 根据构造函数参数的类型自动推导出模板参数, 而不需要你显式指定它们.

#include <vector>
#include <string>
#include <utility> // for std::pair

template <typename T1, typename T2>
struct MyPair {
    T1 first;
    T2 second;
    MyPair(T1 f, T2 s) : first(f), second(s) {}
};

int main() {
    std::pair p1(10, "hello"); // 编译器自动推导出 T1=int, T2=const char*
                               // 对于 std::pair<int, std::string> p(10, "hello") 来说,
                               // 推导出的pair类型是 std::pair<int, const char*>,
                               // 然后 p1 会被构造成 std::pair<int, std::string>.

    MyPair mp1(3.14, "pi"); // 编译器自动推导出 T1=double, T2=const char*
                            // mp1 的类型是 MyPair<double, const char*>

    std::vector v = {1, 2, 3, 4, 5}; // 推导出 std::vector<int>
}

最好不要使用CTAD.

默认参数

模板类的默认模板参数 (default template arguments) 允许你在定义类模板时, 为它的一个或多个模板参数指定一个默认值.

#include <iostream>

template <typename T, int size=10>
class Container {
    public:
        Container() {
            m_data = new T[size];
        }
        ~Container() {
            delete[] m_data;
        }

        static T m_variable;
    private:
        T* m_data;
};

int main() {
    Container<int, 15> c;
    Container<int> c2; // size是10
    return 0;
}

或者:

#include <iostream>

template <typename T=int, int size=10>
class Container {
    public:
        Container() {
            m_data = new T[size];
        }
        ~Container() {
            delete[] m_data;
        }

        static T m_variable;
    private:
        T* m_data;
};

int main() {
    Container<int, 15> c;
    Container<int> c2; // size是10
    Container c3; // T是int, size是10
    return 0;
}

如果你看一下std::vector的实现, 你会发现他其实模版是template <class T, class Allocator = std::allocator<T>> class vector, 所以有一个默认给定的Allocator类, 我们可以自定义的其实. 还有std::unique_ptr的实现是template <class T, class Deleter = std::default_delete<T>> class unique_ptr这个Deleter也是可以自己实现的.