聊一聊C++设计模式、函数式编程等

1.variant技巧

自定义match模板函数可以将多个lamda打包成一个,通过重载来调用

#include <iostream>
#include <variant>

template <class ...Fs>
struct match: protected Fs...{
	explicit match(Fs const&... fs)
		:Fs(fs)... {}
	using Fs::operator()...;
};

template <class ...Fs>
match(Fs const&... fs) -> match<Fs...>;

int main()
{
	std::variant<float, int, std::string> v;
	v = "abcd";
	std::visit(match[&](auto x){
		std::cout<<x<<std::endl;}, [&](std::string x){std::cout<<x<<std::endl;},v)
	return 0;
}

  • 测试:
    在这里插入图片描述
也可以利用C++17 新增的 overloaded 模板,可以直接生成匿名访问器,简化代码, 下面的代码是等价的
int main() {
  std::variant<int, float, string> value = 1.0;
  std::visit(overloaded{
    void operator()(int i) { cout << "int: " << i << '\n'; }
    void operator()(float f) { cout << "float: " << f << '\n'; }
    void operator()(const std::string& s) { cout << "str: " << s << '\n'; }
    },value);
 }

用static可以检测实例化了几次

#include <iostream>
#include <variant>

template <class ...Fs>
struct match: protected Fs...{
	explicit match(Fs const&... fs)
		:Fs(fs)... {}
	using Fs::operator()...;
};

int main()
{
 	auto func = [&](auto val){
 		static int _=printf("instance\n");
 		std::cout<<val>>std::endl;
 	};
 	func(1);
 	func(2);
 	func(3.14f);
	return 0;
}

可以在编译器进行判断

  • 编译器可以进行优化
#include <iostream>
#include <variant>

template <class ...Fs>
struct match: protected Fs...{
	explicit match(Fs const&... fs)
		:Fs(fs)... {}
	using Fs::operator()...;
};

int main()
{
 	auto func = [&](auto val){
 		static int _=printf("instance\n");
 		if constexpr (val)
 			std::cout << "true" <<std::endl;
 		else
 			std::cout << "false" <<std::endl;
 	};
	func(std::integral_constant<bool,false>{});//等价于std::false_type
	func(std::integral_constant<bool,true>{});//等价于std::true_type
	return 0;
}

运行期判断转变成编译器判断

#include <iostream>
#include <variant>

template <class ...Fs>
struct match: protected Fs...{
	explicit match(Fs const&... fs)
		:Fs(fs)... {}
	using Fs::operator()...;
};

//bool转std::variant
//编译器就知道是true类型,false类型
std::variant<std::false_type, std::true_type>
bool_variant(bool x)
{
	if (x)
		return std::true_type{};
	else
		return std::false_type{};
}

void saxpy(std::vector<float> a, float b, float c)
{
	auto has_b = bool_variant(b!=1);
	auto has_c = bool_variant(c!=1);
	std::visit([&](auto has_b, auto has_c){
		for (auto& ai: a)
		{
			//编译器会自动产生4个分支
			if constexpr (has_b)
				ai* = b;
			if constexpr (has_c)
				ai+ = c;
		}
	})
}


int main()
{
	std::vector<float> a(1024);
	saxpy(a,1,0);
	saxpy(a,1,3.14f);
	return 0;
}

2.std::enable_if_t用法

(1)C++模板 SFINAE

SFINAE: Substitution Failure Is Not An Error (替换失败不是错误)

SFINAE 可用于模板类型参数 和 返回值类型 两个地方,
在这两个地方做类型不正确的操作 它不会报错和退出程序, 而是跳过当前匹配去尝试其他匹配.

  • eg:函数体内做类型不正确的操作, 会报错
#include <iostream>
using std::cout;
using std::endl;


struct book {
    // 重新定义一个类型, 这个类型叫做 page_type
    // page_type 属于 book 作用域中的一个成员.
    typedef int page_type;      

    // 使用另外一种语法来重新定义一个类型, 这个类型叫做 name_type,
    // name_type 属于 book 作用域中的一个成员.
    using name_type = string;
};


int main(void) {

    // 使用book结构中的类型别名来定义变量.
    book::page_type a = 1099;
    book::name_type b = "C++ Standard Library";

    // author_type 不存在于 book 结构中, 因此会报错.
    book::author_type c = "Nicolai M.Josuttis";         // error.

    // output:
    // 'author_type': is not a member of 'book'

    return 0;
}
  • eg:模板尖括号里做类型不正确的操作, 不会报错.
#include <iostream>
#include <string>
using std::cout;
using std::endl;

struct book
{
    typedef int page_type;
    using name_type = std::string;
};

// T::author_type 是一个不存在的东西, 在这里叫做替换失败, 不是类型操作错误.
// 当替换失败时, 编译器会跳过这个匹配, 继续寻找下一个模板函数.
template <typename T, class U = typename T::author_type>
void example(T t)
{
    cout << "template<typename T = book::author_type > void example();" << endl;
}

template <typename T>
void example(T t)
{
    cout << "void example(book b)" << endl;
}

int main(void)
{
    book b;
    example(b);

    return 0;
}

(2)C++模板 匿名类型参数

模板的类型参数, 除了能够定义默认类型之外.

还可以定义匿名类型参数, 这个匿名类型参数不能被函数体使用, 也不能被返回值使用.
匿名类型参数利用替换失败不是错误的特性来判定, 如果没有出现替换失败, 那么就是符合匹配的.

什么场景下需要用到匿名类型参数?

  • 以上面的eg为例,这个函数声明了 class U 这个类型参数, 并且设定了一个默认值T::author_type,
    但是函数体、返回值中都没有使用到 U 这个类型, 如果实际项目中函数体的代码非常复杂,
    多出一个不明确的类型是非常恐怖的一件事情, 而匿名类型参数可以减少这个心里负担.
template<typename T, class U = typename T::author_type>
void example(T t) {
    cout << "template<typename T = book::author_type > void example();" << endl;
}

所以, 匿名类型参数就是用来降低心里复杂度的蜜枣.

  • 由于 struct book 没有定义 author_type 这个成员类型, 因此 T::author_type 是一个无效的调用, 编译器不会报错, 而是选择跳过当前匹配进而去寻找其他匹配.
#include <iostream>
#include <string>
using std::cout;
using std::endl;

struct book
{
    typedef int page_type;
    using name_type = std::string;
};

// class = typename T::author_type 就是一个匿名类型参数.
template <typename T, class = typename T::author_type>
void example(T t)
{
    cout << "template<typename T = book::author_type > void example();" << endl;
}

// template<typename T, class = typename T::page_type>
// 等同于
// template<typename T, typename = int>
// 第二个类型参数是一个匿名类型参数(没有提供一个identifier), 并且提供了一个int作为默认类型.
template <typename T, class = typename T::page_type>
void example(T t)
{
    cout << "void example(book b)" << endl;
}

int main(void)
{
    book b;
    example(b);
    return 0;
}

模板参数的定义方式

什么是 Identifier?

template <typename T>    // T 就是 Identifier.
void example() {};

Identifier 的命名不能跟 C++ 中的关键字出现冲突.

// 错误的
// 因为 identifier 占用和覆盖了 int , 会导致后续功能不正常, 因此编译是不会通过的.
template<typename int>
void normal(int s){
    cout << "normal: " << s << endl;
}

// 正确的
// 但是建议使用常见的 T 或者 U 来当作 Identifier.
template<typename aaa>
void normal(aaa s){
    cout << "normal: " << s << endl;
}

template <> 中 typename 和 class 有什么区别?

没有区别, 但是建议统一风格, 只使用其中任意一种,
我个人基本都是使用typename.

typename 这个关键字是什么含义?

它是一个标识符, 即便不放在 template <> 中, 也可以在常规的函数体中使用, 例如: typename int a = 10;
但是在 template<> 中, 要声明一个类型, 必须要使用 typename 或 class 标识符, 用来标识它是一个Identifier.

  • eg:
#include <iostream>
using std::cout;
using std::endl;

// 具体化
// int 是一个具体的类型
// n 是一个Identifier 也是一个具体值, 所以不能用 n 来声明一个变量.
template <int n> //
void specialization()
{
    cout << n << endl;
};

// 具体化 和 匿名类型参数
// 因为 int 是一个具体类型, 所以不能也没必要定义一个 Identifier , 所以函数体无法使用这个参数.
// 匿名类型参数必须要有一个默认值.
template <int = 0> //
void specialization_and_anonymous()
{
    cout << "specialization_and_anonymous" << endl;
};

int main(void)
{
    specialization<10>(); // 必须这样使用.
    specialization_and_anonymous();
    // output:
    // 10
    // specialization_and_anonymous
    return 0;
}

//typename和匿名类型
template <typename T, typename = bool>
T example(T t)
{
    cout << "template<typename T = book::author_type > void example();" << endl;
    return t;
}


(3)enable_if

模板的主要任务就是推导/替换, 将调用时传递的具体值推导出其类型, 然后将 T 替换成该值的类型.

  • 模板支持推导和替换任意类型参数, 但每种类型支持的操作并非都相同, 因此一份模板函数不可能解决所有情况.

  • 当一份标准模板函数无法解决特定情况时,按条件匹配进入特定模板函数就变成了不可或缺.

enable_if 利用了模板匹配的技巧和struct结构, 巧妙的将条件匹配分割成两种情况,

一种是true的情况: 为结构绑定一个type
一种是false的情况: 采取留空策略

template <bool _Test, class _Ty = void>
struct enable_if {}; // no member "type" when !_Test

// 这个实现叫做部分具体化(partial specialization), 即: 第一个参数 (<true>) 采用具体值.
// 这个模板函数简要但是浓缩了几个规则:
// 当调用时传递的参数是true时, 一定会进入这个函数.
// 当调用时传递的参数是true且不提供其他参数时, <class _Ty> 会把自动合并上面一个enable_if的 <class _Ty = void>;
// 当调用时传递的参数是true且提供其他参数时, _Ty 会替换成传递参数的类型(放弃void).
template <class _Ty>
struct enable_if<true, _Ty> { // type is _Ty for _Test
    using type = _Ty;
};
  • eg:
#include <iostream>
#include <type_traits>
using std::cout;
using std::endl;

//template <typename T,  std::enable_if<true, int>::type = 0>也可以编译通过
template <typename T, typename std::enable_if<true, int>::type = 0>
T example(T t)
{
    cout << "template<typename T = book::author_type > void example();" << endl;
    return t;
}

int main(void)
{
    example(10);
    return 0;
}

enable_if<is_integral::value, int> 等同于 enable_if<true, int> 或者 enable_if<false, int>,如果第一个参数是 true , 那么 enable_if 这个结构就有一个 type 成员, 这时调用 enable_if::type 就是OK的.

如果第一个参数是 false, 那么 enable_if 这个结构就没有一个 type 成员, 这时调用 enable_if::type 就是失败的, 但是不会报错而是跳过当前匹配.

enable_if 的第二个参数提供了一个int, 所以 enable_if::type 其实就是一个 int, 也就是说
<typename T, typename enable_if<is_integral<T>::value, int>::type = 0> 等同于
<typename T, typename int = 0> 等同于
<typename T, int = 0>

(4)enable_if_t

它基于模板的 SFINAE 和 匿名类型参数 的基础概念上进行了简洁且完美的封装(落地).
enable_if_t 强制使用 enable_if 的 ::type 来触发 SFINAE 规则, 如果失败则跳过当前匹配进入下一个匹配.

template <bool _Test, class _Ty = void>
using enable_if_t = typename enable_if<_Test, _Ty>::type;

enable_if_t 节省掉了 typename 和 ::type , 是因为它背后使用 using 帮我们多写了typename 和 ::type (即: 强行重定义了类型).

  • eg:
#include <iostream>
#include <type_traits>
using std::cout;
using std::endl;

// 省略了 typename
// 省略了 ::type
//template <typename T, typename std::enable_if_t<std::is_integral<T>::value, int> = 0>也可以编译通过
template <typename T, std::enable_if_t<std::is_integral<T>::value, int> = 0>
void example(T t)
{
    cout << "template<typename T = book::author_type > void example();" << endl;
}

int main(void)
{
    example(10);
    return 0;
}

(5)后面那个 = 0 是个什么东西?

由于 enable_if 第二个类型设定了默认为 void 类型

  • 模板的非类型参数不能用void,如果使用void会报错
  • 因此 typename enable_if::void = 0(typename enable_if::void = nullptr) 是失败的, 它会导致程序跳过这个匹配.
  • eg:
#include <iostream>
#include <type_traits>
using std::cout;
using std::endl;

template <typename T, typename std::enable_if<true>::type = nullptr>
T example(T t)
{
    cout << "template<typename T = book::author_type > void example();" << endl;
    return t;
}

int main(void)
{
    example(10);
    return 0;
}
  • 测试:
    在这里插入图片描述

由于 enable_if 或 enable_if_t 都是采取 匿名类型参数 形式来完成判断行为。

  • 由于 匿名类型参数 必须要有一个默认值
  • 因此 大部分的STL、TR1 的代码都会在使用 enable_if 或者 enable_if_t 时, 为第二个类型提供一个 int, 然后设定一个 0 为默认值.
  • eg:
#include <iostream>
#include <type_traits>
using std::cout;
using std::endl;

template <typename T, typename std::enable_if<true, int>::type>
T example(T t)
{
    cout << "template<typename T = book::author_type > void example();" << endl;
    return t;
}

int main(void)
{
    example(10);
    return 0;
}
  • 测试:
    在这里插入图片描述

  • eg:

//不是int类型
template <class T, std::enable_if_t<!std::is_interal_v<T>, int> = 0>
void func(T const& t)
{
	std::cout<< t <<std::endl;
}


//是int类型
template <class T, std::enable_if_t<std::is_interal_v<T>, int> = 0>
void func(T const& t)
{
	std::cout<<"is intarage: "<<t<<std::endl;
}

int main()
{
	func(23);
	func('c');
	func(3.14f);
}
  • 测试:
    在这里插入图片描述

3.PIMPL模式

目的:由于具体的实现在Course.cpp中,所以在给Course( Course::Impl)增加属性的时候,main.cc是不会重新编译的

//Course.h
#pragma once
#include <memory>

struct Course{
	struct Impl;
	std::shared_ptr<Impl> impl;

	Course();
	~Course();
	void func();
};
//Course.cc
#include "Course.h"

struct Course::Impl
{
	int x, y;
	void func(){
		x+=1;
	}
};
Course::Course():impl(std::make_shared<Impl>()) {}
Course::~Course() = default;

void Course::func()
{
	impl->func();
}

目的:传值进行浅拷贝,因为Course本身就是一个指针(内部有智能指针)

//main.cc
#include "Course.h"

void func(Course course)
{
	course.func();
}

int main()
{
	Course co;
	func(co);
	return 0;
}

4.二维数组

//不好的做法
constexpr int N = 32;
struct Grid{
	vector<int> p;
	size_t nx;
	Grid(size_t x, size_t y)
		: p(x * y), nx(x) {}
	int& operator()(size_t x,size_t y)
	{
		return p[x*nx+y];
	}
};



int java_main()
{
	int** p = new int*[N];
	for (int i = 0; i<N;++i)
	{
		p[i] = new int[N];
	}
	for (int i = 0; i<N;++i)
	{
		for (int j = 0; j<N;++i)
		{
			p[i][j] = 1;
		}
	}

	for (int i=0; i< N;++i)
	{
		delete[] p[i];
	}
	delete[] p; 
	return 0;
}

//好的做法
int main()
{
	//等价于vector<int> p(N*N);
	int*p = new int [N*N];
	for (int i = 0; i<N;++i)
	{
		for (int j = 0; j<N;++i)
		{
			p[i*N+j]=1;
		}
	}
	delete[] p;
	//更近一步
    Grid g(N, N);
    for (int j = 0; j < N; j++) {
        for (int i = 0; i < N; i++) {
            g(i, j) = 1;
        }
    }
	
	return 0;
}

测试访问效率

  • eg:my_course/course/tools/POC/04/grid.cpp
#include <iostream>
#include <cstdlib>
#include <vector>
#include <cmath>
#include <execution>
#include "ticktock.h"
#include <chrono>
#define TICK(x) auto bench_##x = std::chrono::steady_clock::now();
#define TOCK(x) std::cout << #x ": " << std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - bench_##x).count() << "s" << std::endl;
constexpr int N = 1<<12;

struct Grid {
    std::vector<int> p;
    size_t nx;

    Grid(size_t nx, size_t ny)
        : p(nx * ny), nx(nx)
    {
    }

    int &operator()(size_t x, size_t y) {
        return p[y * nx + x];
    }
};

int main() {
    TICK(java);
    int **p = new int *[N]{};
    for (int i = 0; i < N; i++) {
        p[i] = new int[N]{};
    }
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++) {
            p[std::rand() % N][std::rand() % N] += 1;
        }
    }
    for (int i = 0; i < N; i++) {
        delete[] p[i];
    }
    delete[] p;
    TOCK(java);

    TICK(cpp);
    Grid g(N, N);
    for (int j = 0; j < N; j++) {
        for (int i = 0; i < N; i++) {
        	//%N防止越界
            g(std::rand() % N, std::rand() % N) += 1;
        }
    }
    TOCK(cpp);
    return 0;
}

5.折叠表达式

#include <iostream>
#include <cstdlib>
#include <vector>
#include <cmath>
#include <numeric>
#include <algorithm>

template<class ...T>
auto sum(T ...t)
{
    return (0+...+t);//sum()也可以运行,return (+...+t)也行,但是sum()中必须有值才行
}

int main()
{
    std::cout<<sum(1,2,3)<<std::endl;
}
  • eg:course/tools/POC/04/main.cpp
#include <iostream>
#include <cstdlib>
#include <vector>
#include <cmath>
#include <numeric>
#include <algorithm>

template <class ...T>
constexpr auto sum(T ...t) {
    return (sizeof(T) + ...);
}

template <class T0, class ...T>
void print(T0 const &t0, T const &...t) {
    std::cout << t0;
    ((std::cout << ' ' << t), ...);
    std::cout << std::endl;
}


template<typename... Ts>
void printAll(Ts&&... mXs)
{
    (std::cout << ... << mXs) << std::endl;
}

void func2(int const &t) {
    printf("int const &\n");
}

void func2(int &&t) {
    printf("int &&\n");
}

//std::forward作用:
//当T为左值引用类型时,t将被转换为T类型的左值
//当T不是左值引用类型时,t将被转换为T类型的右值
template <class ...T>
void func1(T &&...t) {
    func2(std::forward<decltype(t)>(t)...);//等价于: func2(std::forward<(T)>(t)...);
}

int main() {
    printAll(1,2,3);
    float arr[1<<20]; int i = 0;
    std::generate_n(std::begin(arr), std::size(arr), [&] () { return std::sin(i++); });
    float sum = std::accumulate(std::begin(arr), std::end(arr), 0.f, std::minus{});
    print(sum);

    //std::reduce可以reduce一个vector的数组
    std::vector<float> arr;
    float sum = std::reduce(arr.begin().arr.end());
    //为了支持vector和C数组,可以使用std::begin().std::end(), std::reduce();来自C++17
    float arr[32] = {1,2,3};
    //等价于float sum = std::reduce(std::begin(arr), std::end(arr),0.f, std::plus<float>{});//std::plus{};是针对所有类型的加法
    float sum = std::reduce(std::begin(arr), std::end(arr),0.f, [](float a, float b){return a+b;});
    // std::reduce 比std::accumulate更准确而已

    float arr2[1<<20];
    std::generate(std::begin(arr),std::end(arr),std::rand);

    return 0;
}