c++模板元编程

模板元编程与函数式编程

模板元编程

  • 模板函数:默认参数类型
1
2
3
4
5
6
7
8
9
10
11
template <class T=int>
T two(){
return 2;
}

template <int N>
void show_time(std::string s){
for(int i=0;i<N;++i){
std::cout<<s<<std::endl;
}
}

  • 编译器优化案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include<iostream>
#include <time.h>

int sum(int n,bool debug){
int res=0;
for(int i=0;i<n;++i){
res += i;
if(debug)
std::cout<<i<<"-th: "<<res<<std::endl;
}
return res;
}

template <bool debug>
int sum(int n){
int res=0;
for(int i=0;i<n;++i){
res += i;
if(debug)
std::cout<<i<<"-th: "<<res<<std::endl;
}
return res;
}

int main(){
int n;
std::cin >> n;
auto st=clock();
std::cout<<sum(n ,true)<<std::endl;
std::cout<<sum(n,false)<<std::endl;
auto ed=clock();
std::cout<<ed-st<<"ms"<<std::endl;
st=ed;
std::cout<<sum<true>(n)<<std::endl;
std::cout<<sum<false>(n)<<std::endl;
ed=clock();
std::cout<<ed-st<<"ms"<<std::endl;
return 0;
}

第一种sum每次循环的时候都需要做判断,对于性能有影响,而且编译器不好优化。后者(第二种sum)在编译器看来就是 if(false),显然会被自动优化掉。

更进一步,可以用C++17的if constexpr保证编译器确定的分支。

  • 模板的惰性:延迟编译

假如有一个函数(有可能是错误的),暂时未被调用,我们可以采用模板的惰性,只要没有被调用就不会实例化,因此可以加快编译速度。

  • 懒汉单例模式
1
2
3
4
auto &product_table(){
static std::map<string,int> instance;
return instance;
}
  • 可以通过decltype获取表达式的类型,typeid只有在有虚函数的时候才会起作用。

对于我想定义一个和之前定义过的函数一样的变量/函数,可以使用

declgtype(auto) p=func();

decltype(func()) p=func();

  • using
    • typedef std::vector<int> VecInt;using VecInt=std::vector<int>;等价
    • typedef int (*PFunc)(int);using PFunc=int(*)(int)等价
1
2
3
4
5
6
7
8
9
template<class T1,class T2>
auto add(std::vector<T1> const& a, std::vector<T2> const& b){
using T0=decltype(T1{}+T2{});
std::vector<T0> vec;
for(int _=0;_<a.size();++_){
vec.push_back(vec[_]);
}
return vec;
}
  • type_traits

函数式编程

函数也可以作为另一个函数的参数

  • 在lambda表达式中[&]用于捕获同一作用域中的变量。(闭包)此外还可以修改同一作用域下的变量
  • 但是可能会出现如下问题:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<iostream>
#define debug(x) std::cout<<#x<<": "<<x<<std::endl

template<class Func>
void call_twice(Func const& func){
debug(func(0));
debug(func(1));
debug(sizeof(Func));
}

auto make_twice(int fac){
return [&](int n){
return n*fac;
};
}

int main(){
auto twice = make_twice(2);
call_twice(twice);
return 0;
}

输出为

1
2
3
func(0): 0
func(1): 6422040
sizeof(Func): 8

这是因为make_twice函数在调用之后会对fac进行销毁,但是之前传入的是函数的引用,所在在fac销毁之后其指针会随意指向某处,导致输出很怪。

小彭老师的建议是对make_twice()函数进行修改:

1
2
3
4
5
auto make_twice(int fac){
return [=](int n){
return n*fac;
};
}

这样输出就没有问题了

1
2
3
func(0): 0
func(1): 2
sizeof(Func): 4

此时可以把fac作为一个值存下来,而不是存下fac的指针。

因此,[&]需要保证lambda对象的声明周期不超过其捕获的所有引用的声明周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<iostream>
#include<functional>
#define debug(x) std::cout<<#x<<": "<<x<<std::endl

// template<class Func>
void call_twice(std::function<int(int)> const& func){
debug(func(0));
debug(func(1));
debug(sizeof(func));
}

std::function<int(int)> make_twice(int fac){
return [=](int n){
return n*fac;
};
}

int main(){
auto twice = make_twice(2);
call_twice(twice);
return 0;
}

这种形势下function<int(int)>是虚函数,会有额外的开销

1
2
3
func(0): 0
func(1): 2
sizeof(func): 32
  • lambada的用途:yield模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <vector>
#include <type_traits>

template<class Func>
void fetch_data(Func const& func){
for(int i=0;i<32;++i){
func(i);
func(i+0.5f);
}
}

int main(){
std::vector<int> res_i;
std::vector<float> res_f;
fetch_data([&] (auto const &x){
using T = std::decay_t<decltype(x)>;
if constexpr (std::is_same_v<T, int>){
res_i.push_back(x);
}else if constexpr (std::is_same_v<T, float>){
res_f.push_back(x);
}
}
);
return 0;
}
  • 立即求值

  • 匿名递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <vector>
#include <set>

int main(){
std::vector<int> arr={1,4,2,8,5,7,1,4};
std::set<int> visited;
auto dfs = [&] (auto const &dfs, int index) ->void{
if(visited.find(index)==visited.end()){
visited.insert(index);
std::cout<<index<<std::endl;
int next=arr[index];
dfs(dfs, next);
}
};
dfs(dfs, 0);
return 0;
}
  • 常用容器tuple

std::tuple<...>可以讲多个不同类型的值打包成一个。尖括号里填各种类型

之后可以使用std::get<0>([tuplename])获取对应索引值

  1. 结构化绑定
1
2
auto tup = std::tuple(3,3.14,"6");
auto [x,y,z] = tup;
  1. tuple还可以使用在有多个返回值的函数
  • 常用容器optional
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <cmath>
#include <optional>
#include <memory>
#include <vector>

std::optional<float> mysqrt(float x){
if(x>=0.f) {
return std::sqrt(x);
}else{
return std::nullopt;
}
}

int main(){
auto res = mysqrt(-3.f);
if(res.has_value()){
std::cout<<"answer: "<<res.value()<<std::endl;
}else{
std::cout<<"failed"<<std::endl;
}
return 0;
}
  • optional:operator bool()has_value()等价,是一个更安全的指针
  • variant是更安全的union,存储多个不同类型的值

我的作业:https://github.com/WangYuHang-cmd/hw03