C++ 模板
- Created: 2024-03-24T20:24+08:00
- Published: 2024-11-17T16:37+08:00
- Categories: CPP
在模板中,我们可以声明三种类型的形参(Parameters),分别是:
- 非类型模板形参(Non-type Template Parameters)
- 类型模板形参(Type Template Parameters)
- 模板模板形参(Template Template Parameters)
——C++模板元编程(零):前言 - 知乎 的笔记,但是原文有些地方还是不太明白。
也就是说,定义模板的时候,参数不一定要写作 typename T
,还可以直接写 int
之类的,如 my_template<5>()
。
偏特化
偏特化的意思是:当传入的类型参数为 T 时候,我希望有一个自己重写的实现,如:
#include <iostream>
template <typename T>
void print(T x)
{
std::cout << x << std::endl;
}
template <>
void print<int>(int x)
{
std::cout << "print(" << x << ")" << std::endl;
}
template <>
void print(double x)
{
std::cout << "print(" << x << ")" << std::endl;
}
int main()
{
print(1);
print(1.1);
}
类模板
类模板基础的用法略过,类模板 C++11 不支持参数推导,所以使用时候一定要提供模板参数。
它的神奇之处在于:
- 支持偏特化
- 支持引用的偏特化,引用类型偏特化的语法也是独树一格:
// example from https://zhuanlan.zhihu.com/p/384826036
template <typename T> struct is_reference { static constexpr bool value = false; }; // #1
template <typename T> struct is_reference<T&> { static constexpr bool value = true; }; // #2
template <typename T> struct is_reference<T&&> { static constexpr bool value = true; }; // #3
std::cout << is_reference<int>::value << std::endl; // 0
std::cout << is_reference<int&>::value << std::endl; // 1
std::cout << is_reference<int&&>::value << std::endl; // 1
更多用法参考:C++模板元编程(三):从简单案例中学习 - 知乎
函数模板
首先要破除一些关于函数模板的迷思:
- 函数模板的模板参数虽然可以通过函数的参数推导,但是也可以直接指定,像
foo<int>()
就是在直接指定,并非语法错误。 - 函数模板只支持全特化。所以特化的模板一定要依赖于主模板,语法是把
template<typename T, ...>
中尖括号<>
中所有的模板参数都列出来,一个都不能少。
函数和函数模板可以放在一起重载,看起来很烧脑,
那么发生了一次函数调用,如何确定使用哪一个函数呢?按照我的理解:
- 函数调用时候,如果提供了模板参数列表,则显式实例化,如
foo<int>(1)
提供了<int>
,- 先查找是否有特化的函数模板,类似这个格式:
template<> foo<int>()
- 没有就找函数主模板
- 先查找是否有特化的函数模板,类似这个格式:
- 如果没有提供模板的参数列表,则说明需要根据函数参数推导类型
- 从全特化的模板中查找符合条件的
- 找不到就用主模板
严谨表述参考:C++模板元编程(四):深入模板 - 知乎
template<typename T>
void bar(T v) {
std::cout << "1 template param" << std::endl;
return;
}
template<>
void bar(int v) {
std::cout << "1 template param specialization" << std::endl;
return;
}
template<typename T, typename U>
void bar(T v) {
std::cout << "2 template param" << std::endl;
return;
}
int main()
{
bar<int, float>(1); // 2 template param
bar<float>(1); // 1 template param
bar(1); // 1 template param specialization
return 0;
}
bar<int, float>
显式提供了模板参数,故实例化第三个 bar
bar<float>
也显式提供了模板参数,确定类型 T
为 float,并且没有找到对应的特换版本的函数,故实例化第一个
bar(1)
只提供了一个函数参数,只需要检查函数签名,对于第三个模板,无法推导出类型 U
;使用第二个模板。
形参包
形参包 (C++11 起) - cppreference.com
- 语法:我的记忆方法是,把
...
写在类型和包名中间,这样可以避免忘记写...
template<typename ...Args> print(const Args& ...args) { int _[]{ (std::cout << args << ' ' ,0)... }; }
- 包展开:和中文语法下省略号的用法一致
Question
我记不住模板的语法,尤其是偏特化的语法,怎么办?
以 remove_reference 为例:
template <typename T> // line 1
struct remove_ref
{
typedef T type;
};
template <typename T> // line 1
struct remove_ref<T &>
{
typedef T type;
};
让我们忽略 template 那一行,可以得到如下代码:
struct remove_ref // primary template does not need args
{
typedef T type;
};
struct remove_ref<T &&> // specialization
{
typedef T type;
};
- 主模板不需要写
<args...>
- specialization 写上自己需要实例化的时候,对应主模板的形式,最后在第一行补上未知的类型参数
template 那一行表示的是,下面要用到的未知的类型参数。
面试问题
利用类模板和函数模板实现编译器计算斐波那契数列
注意:类模板成员变量要用 static const
修饰
- static 允许
fib<5>::value
这样形式访问 const static
允许编译器进行计算
模板的声明和定义为什么不能分开写,要想分开写该怎么做
因为是 C++ 是分离式编译,不会递归查找自己需要的函数。
// foo.h
template<typename T>
void foo(T a);
// foo.cpp
template<typename T>
void foo(T a) {
std::cout << a << std::endl;
}
// main.cpp
#include"foo.h"
int main() {
foo<int>();
}
编译到 main.cpp
中 foo<int>
时候,main.o
里会有一个 call foo_int
的指令,等待链接的时候补上。
但是编译到 foo.cpp
的时候,他不知道 main.o
里面调用了 foo<int>
,所以不会实例化 foo_int
这个函数。
可以直接把 foo.h
和 foo.cpp
合并到一个文件解决这个问题,叫做 hpp
。
stackoverflow 上有两种方法:
-
在
foo.h
最后加上一行#include"foo.cpp"
,也就是合并到一块写。我看来不是从根本上解决这个问题。 -
问题的关键是,分离式编译的时候,如何让
foo.cpp
得知需要实例化foo<int>
,修改为:// foo.cpp template<typename T> void foo(T a) { std::cout << a << std::endl; } template void foo<int>; // tell the compiler to instantiation this function