模板元编程与函数式
参考:
【公开课】现代C++进阶:模板元编程与函数式
ppt和代码
在高性能计算中,一般使用函数式和元编程,而不使用面向对象。
简单的介绍:类型自动推导模板参数、模板特化
简单的实例:
#include <iostream>
template <class T>
T twice(T t) {
return t * 2;
}
std::string twice(std::string t) { return t + t; }
// int main() {
// std::cout << twice<int>(21) << std::endl;
// std::cout << twice<float>(3.14f) << std::endl;
// std::cout << twice<double>(2.718) << std::endl;
// std::cout << twice<std::string>("hello") << std::endl;
// }
// 自动推导模板参数
int main() {
std::cout << twice(21) << std::endl;
std::cout << twice(3.14f) << std::endl;
std::cout << twice(2.718) << std::endl;
std::cout << twice(std::string("hello")) << std::endl;
// twice("hello")不会使用twice(std::string t),而是使用T twice(T t),从而导致错误。
// std::cout << twice("hello") << std::endl;
}
- 没有指定模板函数的类型时,会自动推导模板参数。
【注】当我们使用特化重载的时候,需要注意代码调用的是否为特化函数,如代码中的 twice("hello")不会使用twice(std::string t),而是使用twice(std::string t),从而导致错误。这个问题可以使用SFINAE解决。 - 使用
template <class T>
和template <typename T>
是完全等价的。
参数部分特化:
#include <iostream>
#include <vector>
template <class T>
T sum(std::vector<T> const &arr) { // 这里用了 const & 避免不必要的的拷贝。
T res = 0;
for (int i = 0; i < arr.size(); i++) {
res += arr[i];
}
return res;
}
int main() {
std::vector<int> a = {4, 3, 2, 1};
// std::cout << sum({4, 3, 2, 1}) << std::endl; // 错误,部分特化也不支持隐式转换。
std::cout << sum(a) << std::endl;
std::vector<float> b = {3.14f, 2.718f};
std::cout << sum(b) << std::endl;
}
func(vector<T> t)
这样则可以限定仅仅为 vector 类型的参数。
整数也可以作为参数
#include <iostream>
template <int N = 1, class T> // 整数也可以作为模板参数
void show_times(T msg) {
for (int i = 0; i < N; i++) {
std::cout << msg << std::endl;
}
}
int main() {
show_times("one");
show_times<3>(42);
show_times<4>('%');
}
代码说明:
- 整数也可以作为模板参数,不过模板参数只支持整数类型(包括 enum),浮点类型、指针类型,不能声明为模板参数。自定义类型也不可以。【问题】为什么要支持整数作为模板参数:因为是编译期常量
- int N 和 class T 可以一起使用。你只需要指定其中一部分参数即可,会自动根据参数类型(T msg)、默认值(int N = 1),推断尖括号里没有指定的那些参数。
template <int N> void func();
与void func(int N);
的区别:
-
template <int N>
传入的 N,是一个编译期常量,每个不同的 N,编译器都会单独生成一份代码,并且对代码进行优化(如删除掉没有必要的判断语句)。比如上述代码中show_times<0>()
函数调用,编译器就可以自动优化为一个空函数。因此模板元编程对高性能编程很重要。 -
func(int N)
,则变成运行期常量,编译器无法自动优化,只能运行时根据被调用参数 N 的不同。
模板的内部实现需要被暴露出来,除非使用特殊的手段,否则,声明和定义都必须放在头文件里,即不能分离声明和定义。但也正因如此,如果过度使用模板,会导致生成的二进制文件大小剧增,编译变得很慢等。
编译期常量
参考:C++干货系列——从编译期常量谈到constexpr(一)
总有些东西是编译器要求编译期间就要确定的,除了变量的类型外,最频繁出现的地方就是数组、switch的case标签和模板了。
数组中的编译期常量: int someArray[520];
,有些时候我们不用显示得指明数组的大小,我们用字符串或花括号来初始化数组的时候,编译器会实现帮我们数好这个数组的大小,如:
int someArray[] = {5, 2, 0};
char charArray[] = "Ich liebe dich.";
模板中的编译期常量:除了类型以外,数字也可以作为模板的参数。这些数值变量包括int,long,short,bool,char和弱枚举enum等。如:
enum Color {RED, GREEN, BLUE};
template<unsigned long N, char ID, Color C>
struct someStruct {};
someStruct<42ul, 'e', GREEN> theStruct;
switch语句的分支判断也必须是编译期常量,和上边模板的情况非常类似。
void comment(int phrase) {
switch(phrase) {
case 42:
std::cout << "You are right!" << std::endl;
break;
case BLUE:
std::cout << "Don't be upset!" << std::endl;
break;
case 'z':
std::cout << "You are the last one!" << std::endl;
break;
default:
std::cout << "This is beyond what I can handle..." << std::endl;
}
}
模板的应用:编译期优化案例
不进行优化前:
#include <iostream>
int sumto(int n, bool debug) {
int res = 0;
for (int i = 1; i <= n; i++) {
res += i;
if (debug)
std::cout << i << "-th: " << res << std::endl;
}
return res;
}
int main() {
std::cout << sumto(4, true) << std::endl;
std::cout << sumto(4, false) << std::endl;
return 0;
}
上述代码,我们声明了一个 sumto 函数,作用是求出从 1 到 n 所有数字的和。用一个 debug 参数控制是否输出调试信息。但是这样 debug 是运行时判断,这样即使是 debug 为 false 也会浪费 CPU 时间(始终需要执行判断语句,导致cpu浪费)。
我们当然可以使用ifdef来预编译阶段优化代码,这里我们演示使用模板函数来在编译器优化代码:
#include <iostream>
template <bool debug>
int sumto(int n) {
int res = 0;
for (int i = 1; i <= n; i++) {
res += i;
if constexpr (debug) // C++11的constexpr用于保证debug是编译期常量,如果constexpr中不是编译常量就会报错
std::cout << i << "-th: " << res << std::endl;
}
return res;
}
int main() {
std::cout << sumto<true>(4) << std::endl;
std::cout << sumto<false>(4) << std::endl;
return 0;
}
上述代码,
-
生成多份代码:在编译阶段会根据sumto的被调用的情况生成相应的代码,如
sumto<false>(4)
就会将if语句和if语句下面的东西都删除;sumto<true>(4)
就会将if语句删除,保留if语句下面的东西。也就是说上述代码在编译器会生成两份代码。 -
constexpr和模板尖括号内不能为运行时变量:除了constexpr()中的表达式不能用运行时变量,模板尖括号内的参数也不能。
-
编译期 constexpr 的表达式,一般是无法调用其他函数的。但是如果能保证被调用函数里都可以在编译期求值,将函数前面也标上 constexpr 即可,如
constexpr bool isnegative(int n){return n < 0;}
【注】constexpr 函数不能调用 non-constexpr 函数。而且 constexpr 函数必须是内联(inline)的,不能分离声明和定义在另一个文件里。标准库的很多函数如 std::min 也是 constexpr 函数,可以放心大胆在模板尖括号内使用。
将模板函数移到另一个文件中定义
观察如下代码:
// sumto.h
#pragma once
template <bool debug>
int sumto(int n);
// sumto.cpp
#include "sumto.h"
#include <iostream>
template <bool debug>
int sumto(int n) {
int res = 0;
for (int i = 1; i <= n; i++) {
res += i;
if constexpr (debug)
std::cout << i << "-th: " << res << std::endl;
}
return res;
}
template int sumto<true>(int n);
template int sumto<false>(int n);
// main.cpp
#include "sumto.h"
#include <iostream>
int main() {
constexpr bool debug = true;
std::cout << sumto<debug>(4) << std::endl;
return 0;
}
只有模板函数sumto有被sumto.cpp中的其他代码使用的时候,才会生成相应的代码。所以在sumto.cpp中增加如下两个显式编译模板的声明:template int sumto<true>(int n);
和template int sumto<false>(int n);
。也就是说debug有几种可能就需要定义几种显式声明,那如果debug是int,那就不可能一个一个显示声明。
如果没有这两个显式的声明,那么main.cpp中的#include"sumto.h"引入了模板函数sumto的声明,但是会找不到sumto的定义。
所以一般来说,我会建议模板不要分离声明和定义,直接将定义了写在头文件里即可。如果分离还要罗列出所有模板参数的排列组合,违背了开-闭原则。
模板的惰性:延迟编译
#include <iostream>
template <class T = void>
void func_that_never_pass_compile() {
"字符串" = 2333;
}
int main() {
return 0;
}
而只有当 main 调用了这个函数,才会被编译,才会报错!上述代码中,明显错误,但是由于main函数没有使用func_that_never_pass_compile()函数,所以不会编译这个函数,所以不会出错。
模板的惰性:就是模板函数没有被使用,编译器就会去编译函数。
还没看
视频还没看完:https://www.bilibili.com/video/BV1ui4y1R78s/ ,这个视频可以用于作为C++进阶视频,太他妈多了。
SFINAE没看懂以后再说
http://kaiyuan.me/2018/05/08/sfinae/
https://en.cppreference.com/w/cpp/language/sfinae