模板元编程与函数式

参考:
【公开课】现代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

posted @ 2023-04-14 22:32  好人~  阅读(39)  评论(0编辑  收藏  举报