C++模板编程初识

MirrorYuChen
MirrorYuChen
发布于 2025-01-02 / 17 阅读
0
0

C++模板编程初识

1.数据类型剥离

​ 假定要实现一个 Add函数将两个数 a,b相加,对于整形来说,代码实现如下:

int Add(int a, int b) {
  return a + b;
}

​ 但是,现在有两个浮点数相加,也想要调用 Add函数,这时就需要对 Add函数进行重载:

float Add(float a, float b) {
  return a + b;
}

​ 这里补充一下函数重载定义:在同一个作用域内,可以用一组相同函数名,不同参数列表的函数,这组函数被称为函数重载,函数重载通常用来命名一组功能相似的函数,以减少函数名数量,避免命名空间污染,同时提高程序的可读性。

​ 同样的,若需要支持对两个双精度浮点数相加,需要进一步重载 Add函数:

double Add(double a, double b) {
  return a + b;
}

​ 于此类推,当需要支持更多类型的数据输入时,就会需要穷举类型组合,重复实现相同算法

​ 那么,有没有一种方法,能够简化这个过程,将数据类型从代码实现中剥离出来?答案是肯定的,这时就需要引入模板 template了,具体代码如下:

#include <iostream>

template <typename T>
T Add(T a, T b) {
  return a + b;
}

int main(int argc, char *argv[]) {
  int a = 10;
  int b = 20;
  int c = Add(a, b);
  std::cout << "Sum of " << a << " and " << b << " is " << c << std::endl;

  float x = 2.0f;
  float y = 3.0f;
  float z = Add(x, y);
  std::cout << "Sum of " << x << " and " << y << " is " << z << std::endl;

  return 0;
}

​ 运行结果如下:

Sum of 10 and 20 is 30
Sum of 2 and 3 is 5

​ 通过模板将数据类型从代码实现中剥离,大大地减少了重复代码地编写。

2.函数模板

2.1 函数模板初识

​ 函数模板是参数化的函数,代表一组具有相似行为的函数,它提供了适用于不同数据类型的函数行为,如,上面的 Add函数:

template <typename T>
T Add(T a, T b) {
  return a + b;
}

​ 这个模板定义了一组函数,它们都返回两个传入参数的和,这两个参数类型并未被明确指明,而被标识为模板参数 T。注意,通常可以使用 typenameclass来标识模板的类型参数,声明语法如下:

template <由逗号分隔的模板参数>
  • (1) 关键字 typename在C++98标准发展过程中引入较晚,在那之前,关键字 class是唯一来定义模板类型参数的关键字,而且,当前这一关键字依然有效。
  • (2) 从语义上来讲,关键字 typenameclass对于模板类型参数的标识不会有任何不同,只是使用 class可能会引起一定歧义(T并不是只能为class类型),所以最好使用 typename关键字。

​ 前面 Add函数传入两个参数需要为相同类型的,下面可以优化一下,让其更加通用:

#include <iostream>

template <typename T, typename U>
auto Add(T a, U b) -> decltype(a + b) {
  return a + b;
}

int main(int argc, char *argv[]) {
  int a = 1;
  int b = 2;
  double c = 3.6;
  Add(a, b);
  Add(b, c);
  return 0;
}

​ 编译运行过程分为三步:(1) 首先,编译器发现模板函数 Add,然后编译器在整个项目中查到调用 Add函数的地方,最后,main函数中找到两处调用,一处传参类型为 int, int,一处传参类型为 int, double;(2) 将源码中模板删除掉,用具体类型替换掉模板中类型 T,U得到:

#include <iostream>

int Add(int a, int b) -> int {
  return a + b;
}

double Add(int a, double b) -> double {
  return a + b;
}

int main(int argc, char *argv[]) {
  int a = 1;
  int b = 2;
  double c = 3.6;
  Add(a, b);
  Add(b, c);
  return 0;
}

​ (3) 将重写后代码再次编译,所有函数调用都可以找到匹配实现了。

2.2 两阶段编译检查

​ 模板定义阶段:不包含参数检查,只进行如下几方面检查:

  • (1) 语法检查:如,是否少了分号;
  • (2) 不依赖模板参数的符号未定义:类型名或函数名等;
  • (3) 不依赖于模板参数的 static_assert

​ 模板实例化阶段:为确保所有代码有效性,模板会再次检查,尤其是那些依赖于类型参数的部分,如:

template <typename T>
void foo(T t) {
    undeclared(); // 若undeclared()未定义,第一阶段会报错,因为与模板参数无关
    undeclared(t); // 若undeclared()未定义,第二阶段会报错,因为与模板参数有关;
    static_assert(sizeof(int) > 10, "int too small."); // 与模板参数无关,总会报错
    static_assert(sizeof(T) > 10, "T too small"); // 与模板参数有关,只会第二阶段报错
}

​ 名称被两次检查,这一现象称为“两阶段检查”。需要注意,有些编译器并不会执行第一阶段所有检查,因此,若模板没有被实例化一次的话,可能一直都发现不了代码中的常规错误。两阶段编译检查给模板处理带来了一个问题:当实例化一个模板时,编译器需要(一定程度上)看到模板的完整定义,常规做法就是将模板实现也写在头文件中。

3.变参模板

3.1 变参模板和非模板函数重载

​ C++11开始,模板可接受一组数量可变的参数,这样就可以在参数数量和类型都不确定情况下使用模板。

#include <iostream>
#include <string>

void print() {}

// 使用模板参数包(template parameter pack)定义的类型"Types"
// args为剩余参数,是一个函数参数包(function parameter pack)
template <typename T, typename...Types>
void print(T firstArg, Types...args) {
  std::cout << firstArg << '\n';
  print(args...);
}

int main(int argc, char *argv[]) {
  std::string s("World");
  print(7.5, "hello", s);
  return 0;
}

​ 调用过程如下:

  • (1) 模板展开如下,其中 firstArg为7.5,类型为 doubleargs为一个可变模板,包含类型 const char *的"hello"和 std::string类型的"world"。
 print(7.5, "hello", s); -> print<double, const char *, std::string>(7.5, "hello", s);
  • (2) 打印完 firstArg后,继续调用 print打印剩余参数,这里展开如下,firstArg类型为 const char *,值为"world",args为一个空的可变模板参数,没有任何值。
print<std::string>(s);
  • (3) 打印完"world"后,就会调用被重载不接受参数的非模板参数 print,从而结束递归。

3.2 变参和非变参模板重载

​ 上述例子也可以这样实现:

#include <iostream>

template <typename T>
void print(T arg) {
  std::cout << arg << '\n'; // print passed argument
}

template <typename T, typename...Types>
void print(T firstArg, Types...args) {
  print(firstArg);
  print(args...);
}

int main(int argc, char *argv[]) {
  std::string s("World");
  print(7.5, "hello", s);
  return 0;
}

​ 两个函数模板区别只在于处理尾部参数包时,会优先选择没有尾部参数包的那一个函数模板。

3.3 sizeof...运算符

​ C++11为变参模板引入了新的 sizeof运算符,它会获取扩展成参数包中所包含参数数目。

#include <iostream>

template <typename T>
void print(T arg) {
  std::cout << arg << '\n'; // print passed argument
}

template <typename T, typename...Types>
void print(T firstArg, Types...args) {
  print(firstArg);
  std::cout << sizeof...(Types) << '\n'; // print number of remaining types
  std::cout << sizeof...(args) << '\n'; // print number of remaining args
  print(args...);
}

int main(int argc, char *argv[]) {
  std::string s("World");
  print(7.5, "hello", s);
  return 0;
}

​ sizeof...既可以用于模板参数包,也可以用于函数参数包。这样是否可以不使用为结束递归而重载的不接受参数的非模板函数 print(),只要在没有参数时不去调用任何函数即可:

#include <iostream>

template <typename T, typename...Types>
void print(T firstArg, Types...args) {
  std::cout << firstArg << '\n';
  if (sizeof...(args) > 0) {
    print(args...);
  }
}

int main(int argc, char *argv[]) {
  std::string s("World");
  print(7.5, "hello", s);
  return 0;
}

​ 但这方式是错误的,因为通常函数模板中if语句的两个分支都会被实例化。是否使用被实例化出来的代码是在运行时(run-time)决定的,而是否实例化代码是在编译期(compile-time)决定的。因此,若只有一个参数时调用 print()函数模板,虽然 args...为空,if语句中 print(args...)也依然会被实例化,但此时没有定义不接受参数的print()函数,因此会报错。C++17可以使用编译阶段的if语句,即,将 if语句替换为 if constexpr

#include <iostream>

template <typename T, typename...Types>
void print(T firstArg, Types...args) {
    std::cout << firstArg << '\n';
    if constexpr (sizeof...(args) > 0) {
        print(args...);
    }
}

int main(int argc, char *argv[]) {
    std::string s("World");
    print(7.5, "hello", s);
    return 0;
}

4.函数模板特化

​ 函数模板特化是指在统一函数模板不能在所有类型实例下正常工作时,需要定义类型参数在实例化为特定类型时,函数模板特定的实现版本。

#include <iostream>
#include <cmath>

struct Point2f {
  float x;
  float y;
};

template <typename T>
float Norm(const T &value) {
  return fabs(value);
}

template <>
float Norm(const Point2f &value) {
  return sqrtf(value.x * value.x + value.y * value.y);
}


int main(int argc, char *argv[]) {
  Point2f pt { 2.0f, 3.0f };
  std::cout << Norm(pt) << std::endl;

  float a = 5.0f;
  std::cout << Norm(a) << std::endl;

  return 0;
}

5.参考资料


评论