c++11-模板元实战

前言

黑魔法,应用场景 :
1.实现宿生语言
2.实现一些常规手段做不到的东西,比如 c++11::share_prt::enable_share_from_this
3.作为实现各种库的基本组件 :Stl,Boost,标准库都大量运用了模板元技术

很多人比较排斥这个东西...个人觉得实用就行,工具终究是为人服务的
just enjoy it 😃

推荐的资料

《C++ Templates》
《C++模板元编程》
C++11模板元入门 https://www.cnblogs.com/qicosmos/p/4480460.html
C++高质量社区 http://purecpp.org/

Part 1 实现一个基于C++的宿生Lisp方言

基本约定

建议先看看江南的博客入个门 :C++11模板元入门 https://www.cnblogs.com/qicosmos/p/4480460.html
这是一个类型的世界,首先定义三种基本类型
1.integral_constant :与值关联的类型
2.元类型
3.元函数

1.integral_constant

c++11中的标准库元类型,先看源代码

template <typename T,T v>
struct integral_constant
  {
	using value_type = T ;
	using type = integral_constant<T,v>;
	static constexpr T value = v;
	constexpr operator value_type() 
	  {
	  	return value;
	  }
  };

看几个栗子

using true_type = integral_constant<bool, true>;
using ten = integral_constant<int,10>;
ten::value_type val = 10;
cout << ten::type::type::type::value << endl;
cout << ten() << endl;

typedef和uisng指令等效,using指令更清晰
可以观察到 integral_constant可以生成一个新类型,新类型绑定一个值和该值的类型
并且特别的是 :integral_constant::type指向类型本身

类型2.元类型

特征十分明显 :结构体为空 (建议元编程的时候全部采用struct,struct与class没什么区别,仅仅默认访问权限、继承全部为public

template<typename a,typename b>
struct sum : integral_constant<typename a::value_type,a::value + b::value>
  {};
int main()
  {
  	using ten = integral_constant<int,10>;
  	using five = integral_constant<int,5>;
  	cout << sum<ten,five>::value << endl;
	return 0;
  }

继续观察样例,可以看到sum完成了两个数的加法
观察点1 : 第二行 typename a::value_type,为什么要加奇怪的typename?因为c++模板的特殊性质,编译器在看到a::value_type的时候并不能分析出来a::value_type是成员变量还是类型,所以需要显式说明
观察点2 : sum的结构体为空,所以sum是元类型,但是,sum却完成一个元函数行为 :计算两数和。既然元类型可以进行元计算,为什么我们还需要划分出元函数?
答案是元类型可以进行元计算,但是完成不了复杂的元计算,因为在元类型做元计算的时候,声明不了新类型,导致只能实现一句代码的元计算,做不了复杂计算

类型3.元函数

特征也十分明显:没有继承行为,结构体内部充满一些using指令,用来做元计算

template<typename a,typename b>
struct sum
  {
  	using result = integral_constant<typename a::value_type,a::value + b::value>;
  };
int main()
  {
  	using ten = integral_constant<int,10>;
  	using five = integral_constant<int,5>;
  	cout << sum<ten,five>::result() << endl;
	return 0;
  }

类似于c/c++中的函数,元函数也有"函数名" : sum,"函数参数" :typename a,typename b,"返回值" result
观察点1.我们约定,元函数的"返回值"全部定义为result
观察点2.cout << sum<ten,five>::result() << endl; result是类型,我们可以通过类似 cout << int() << endl;的形式来打印元信息,重载一下流输出即可,后面还会涉及
观察点3.我们约定,元函数不继承任何类型

lisp方言基本类型与操作

1.基本类型,如整形,字符串
2.null类型
3.pair类型
4.元函数car,cdr
有一个非常庞大的证明和实践体系证明上述简单类型/操作能实现非常复杂的计算,是图灵完备的

1.基本类型

整形

template<int val>
struct number : integral_constant<int,val> {};

字符串 : 因为c++11不滋滋模板字符串,所以不能直接定义(因为 "string"的类型是const cahr* const 两个相同的字符串常量拥有不同的常量地址,模板不能识别这种
因为本次模板元实战主要在模板,我采用了一种取巧的办法 :把字符串压进unsigned long long int(简记为ull,可以滋滋长度在8及以下的任何字符串

  	constexpr ull stoull(const char *p,ull now = 0)
  	  {
  	  	return *p == 0 ? now : stoull(p + 1,now * 255 + *p); 
		}
	template<ull val>
	struct string : public integral_constant<ull,val> {};
#define str(x) string<stoull(x)>

这样就可以很方便的用str("string")来产生字符串类型
浮点 : 因为c++的历史原因,模板也不滋滋浮点常量,取巧的办法是定义一个pair<int,int> 即"int.int",与主题关系不大,故不再赘述

2.null类型

为什么要把null单独说明呢...因为空类型相当重要,设计良好的空类型可以很好的简化很多设计

struct null : str("null") {};

null也可以定义成别的样子,这样方便一些

3.pair类型

和c++中的pair类似,只不过first -> car,second -> cdr(仅仅是遵循lisp方言惯例,没什么特别含义

template<typename T,typename U>
struct pair
  {
  	using car = T;
  	using cdr = U;
  };
4.元函数car,cdr
#define car(x) typename Car<x>::result
	template<typename T>
	struct Car
	  {
	  	using result = typename T::car;
	  };
#define cdr(x) typename Cdr<x>::result
	template<typename T>
	struct Cdr
	  {
	  	using result = typename T::cdr;
	  };
#define equal(typea,typeb) typename std::is_same<typea,typeb>::type
#define if_else(a,b,c) typename std::conditional<a,b,c>::type

顺便包装一下std::is_same std::conditional

元函数约定 :

1.每一个元函数都会搭配一个宏
2.统一用result作为元函数返回值
3.元函数不继承任何类型

实现一个异类字典

先看效果

using table = list<pair<str("a"),str("value_a")>,
             	   pair<str("b"),integral_constant<bool,0>>,
		   pair<number<10>,str("x")>
		   >;
using resulta = table_find(table,str("a"));
using resultb = table_find(table,number<10>);
using resultc = table_find(table,str("c"));
cout << resulta() << endl; //value_a
cout << resultb() << endl; //x
cout << resultc() << endl; //null

看起来很复杂?我们分三步实现
1.实现list
2.实现table_find
3.实现输出

1.实现list
template<typename T,typename ...arg>
struct list : pair<T,list<arg...>> {};
template<typename T>
struct list<T> : pair<T,null> {};

不会模板参数包的建议百度
概念上list<a,b,c> = pair<a,pair<b,pair<c,null>>>;
利用参数包 + pair不断展开即可

template<typename T,typename ...arg>
struct list<T,arg...> : pair<T,list<arg...>> {};
template<typename T>
struct list<T> : pair<T,null> {};

因为list对类型没有啥要求,所以直接实现了一个异类字典

2.实现table_find
#define table_find(table,value) typename Table_find<table,value>::result	
	template<typename list,typename key>
	struct Table_find
	  {
	  	using if_found = cdr(car(list));
	  	using if_not_found = table_find(cdr(list),key);
	  	using check_equal = equal(car(car(list)),key);
	  	using result = if_else(check_equal::value,if_found,if_not_found);
	  };
	template<typename key>
	struct Table_find<null,key>
	  {
	  	using result = null;
	  };

首先定义宏,得益我们良好的约定,我们可以写出非常阳间的代码
递归终止的条件十分简单:找不到返回null即可,注意偏特化的写法
概念上,table_find里面核心就是一个if_else逻辑
代码写的十分清楚,不停的递归list结构,层层寻找即可
注意using check_equal = equal(car(car(list)),key);因为我们的宏中有typename
所以equal(car(car(list)),key)::value;的代码是错误的,因为value是个静态成员,解决办法是加个间接类型

实现输出

下面结尾c++输出风格,如果你喜欢printf()这样的,原理一样

cout << integral_constant<int,10>() << endl;            // 10
cout << table_find<table,str("12")>::result() << endl;  // null

首先integral_constant里面重载了类型转换运算符,编译器会自动转化,我们只需要写自己类型的输出格式即可

template<ull val>
ostream& operator << (ostream& os,string<val>)
  {
  	std::string ret;
  	ull now = val;
  	while (now)
  	  ret.push_back(now % 255),
  	  now /= 255;
  	reverse(ret.begin(),ret.end());
  	return os << ret;
  }
ostream& operator << (ostream& os,null)
  {
  	return os << "null";
  }
template<typename T,typename U>
ostream& operator << (ostream& os,pair<T,U>)
  {
  	return os << "(" << T() << "," << U() <<")";
  }

注意ostream& operator << (ostream& os,string);第二个没参数,只有类型
因为信息都在类型里面,参数写不写无所谓,你写的话,编译器自动会优化掉
调用的时候func(type()),编译器就能演绎类型了,func(type)这样是不行滴,因为违背了c\c++参数传递规则
就是函数重载 + 模板特化,大多数输出就这三种特化方式...都在这了...比较简单,照猫画虎即可
list<number<10>,str("22")> : [10,22]
list的输出比较麻烦一点,因为要控制输出格式 + 参数包,代码里有,原理没区别,不赘述了

小结

至此,我们实现了一个字典...难者不会,会者不难...写熟练之后还是挺简单的
小练习,实现一个length的元函数,计算list,字典的元素个数
多动手,练习是必须滴

#define length(type) typename Length<type>::result    
	template<typename T>
	struct Length
	  {
	  	using len = length(cdr(T));
	  	using result = number<1 + len::value>;
	  };
	template<>
	struct Length<null>
	  {
	  	using result = number<0>;
	  };

实现一个归并排序

1.实现merge : 按顺序合并两个list得到新的list
2.实现split : 切割一个list为长度差不超过1的两个list
3.实现sort_merge : 合并两个有序list为一个有序list
4.实现sort : 排序一个list

1.实现merge : 按顺序合并两个list得到新的list

这里需要一个技巧 : 模板的模板参数,不会的百度一下
模板的模板参数,在这里的作用是拿到list内部的类型,常规手段拿不到
其实模板的模板参数类似于函数的实参演绎,只不过挪到了编译期,作用依旧是推导类型

#define merge(typea,typeb) typename Merge<typea,typeb>::result	  
	template<typename list1,typename list2>
	struct Merge {};
	template<typename ...args1,template<typename ...args1> class list1,
	         typename ...args2,template<typename ...args2> class list2
			 >
	struct Merge<list1<args1...>,list2<args2...>>
	  {
	  	using result = list<args1...,args2...>;
	  };
	template<typename ...args1,template<typename ...args1> class list1>
	struct Merge<list1<args1...>,list<null>>
	  {
	  	using result = list<args1...>;
	  };
	template<typename ...args2,template<typename ...args2> class list2>
	struct Merge<list2<null>,list2<args2...>>
	  {
	  	using result = list<args2...>;
	  };

结构就是一个声明 + 4个偏特化...后面三个偏特化很简单,重点看第一个偏特化

template<typename ...args1,template<typename ...args1> class list1,
         typename ...args2,template<typename ...args2> class list2 
         >
struct Merge<list1<args1...>,list2<args2...>>
  {
  	using result = list<args1...,args2...>;
  };

这里的merge其实可以通过lisp形式完成...(偷个懒顺便练习一下模板的模板参数...后面还会用lisp的风格写元函数的
元函数体里面很直观,就是拿到两个list的参数,合并到一个新的list就完了
重点在于模板的模板参数,因为是偏特化,所以template中的顺序无所谓,编译器会推导出正确的类型
template<typename ...args1> class list1因为模板类才有模板的模板参数,c++要求在这里写一个class...(比较憨,但是你得写...以后的标准可能改进这一点...
merge就没了...(偷懒成功,大雾

2.实现split : 切割一个list为长度差不超过1的两个list

split的实现就很lisp了,先康代码

#define split(type) typename Split<type>::result
#define _split(type,typeb,typec) typename Split<type,typeb,typec>::result
	template<typename T,typename first = list<null>,typename second = list<null>>
	struct Split
	  {
	  	using new_first  = merge(first,list<car(T)>);
	  	using new_second = merge(second,list<car(T)>);
	  	using len_first = length(new_first);
	  	using len_second = length(new_second);
	  	using result = if_else(len_first::value < len_second::value,
	                               _split(cdr(T),new_first,second),
			               _split(cdr(T),first,new_second));			    
	  }; 
	template<typename first,typename second>
	struct Split<null,first,second>
	  {
	  	using result = pair<first,second>;
	  };

老样子,先看简单的第二个特化...
split概念上是一个带参数的dfs(T,first,second),运算结果存在参数里面,T为空的时候运算结束,返回参数作为计算结果
再看

#define split(type) typename Split<type>::result
#define _split(type,typeb,typec) typename Split<type,typeb,typec>::result
	template<typename T,typename first = list<null>,typename second = list<null>>
	struct Split
	  {
	  	using new_first  = merge(first,list<car(T)>);
	  	using new_second = merge(second,list<car(T)>);
	  	using len_first = length(new_first);
	  	using len_second = length(new_second);
	  	using result = if_else(len_first::value < len_second::value,
	                               _split(cdr(T),new_first,second),
			               _split(cdr(T),first,new_second));			    
	  }; 

注意偏特化不能有默认参数
思路很直观,比较一下两个参数new_first,new_second的长短,给短的添加当前元素
这里有一个阴间坑 : 如果你split实现的不对,比如 [1,2] -> [1,2],[] 也就是没有正确切割...split会编译通过,但是在某些计算中模板会报一大推错...
老样子,if_else实现一下,给短的添加一个元素即可...注意我们不能销毁、改写一个已经存在的类型...必须生成新的类型,代码很直观...
其实这样子的元函数已经和普通函数差别不大了...只不过把值的运算变成了类型的运算

3.实现sort_merge : 合并两个有序list为一个有序list
#define sort_merge(typea,typeb) typename Sort_merge<typea,typeb>::result
#define _sort_merge(typea,typeb,typec) typename Sort_merge<typea,typeb,typec>::result	
	template<typename list1,typename list2,typename ret = list<null>>
	struct Sort_merge
	  {
	  	using car1 = car(list1);
	  	using car2 = car(list2);
	  	using cdr1 = cdr(list1);
	  	using cdr2 = cdr(list2);
	  	using ret1 = merge(ret,list<car1>);
	  	using ret2 = merge(ret,list<car2>);
	  	using result = if_else(car1::value <= car2::value,
	                               _sort_merge(cdr1,list2,ret1),
				       _sort_merge(list1,cdr2,ret2));
	  };
	template<typename list1,typename ret>
	struct Sort_merge<list1,null,ret>
	  {
	  	using result = merge(ret,list1);
	  };
	template<typename list2,typename ret>
	struct Sort_merge<null,list2,ret>
	  {
	  	using result = merge(ret,list2);
	  };

老样子,先看简单的偏特化:)
和slpit类似,计算结果带在参数ret里面
只有一个list的时候,直接和ret合并返回,注意合并顺序

#define sort_merge(typea,typeb) typename Sort_merge<typea,typeb>::result
#define _sort_merge(typea,typeb,typec) typename Sort_merge<typea,typeb,typec>::result	
	template<typename list1,typename list2,typename ret = list<null>>
	struct Sort_merge
	  {
	  	using car1 = car(list1);
	  	using car2 = car(list2);
	  	using cdr1 = cdr(list1);
	  	using cdr2 = cdr(list2);
	  	using ret1 = merge(ret,list<car1>);
	  	using ret2 = merge(ret,list<car2>);
	  	using result = if_else(car1::value <= car2::value,
	                               _sort_merge(cdr1,list2,ret1),
				       _sort_merge(list1,cdr2,ret2));
	  };

很直观了...这里再强调一下,为什么,元类型也可以计算,但是我们不用
元类型通过继承做元计算的时候,没办法声明新类型
这导致了它的计算能力大大下降...直接写元函数,简单直观,没必要用一些诡异的技巧折磨自己
sort_merge就没了

4.实现sort : 排序一个list

万事俱备...轻松愉快

#define sort(type) typename Sort<type>::result	  
	template<typename T>
	struct Sort
	  {
	  	using left = car(split(T));
	  	using right = cdr(split(T));
	  	using result = sort_merge(sort(left),sort(right));
	  };
	template<typename T>
	struct Sort<list<T>>
	  {
	  	using result = list<T>;
	  };

是8是很直观...某种意义上编译期排序并不比普通排序难...来个栗子

因为函数式编程语言在语法简洁性上有巨大优势

编译期计算8皇后

#include <bits/stdc++.h>
using namespace std;
int dfs(int n,int now = 0,int zuo = 0,int you = 0,int has = 0)
  {
  	if ((1 << n) - 1 == has)
  	  return 1;
  	int ret = 0;
  	int state = zuo | you | has;
  	state = ((1 << n) - 1) & (~state);
  	for (int i = 0;i < 32;i++)
  	  if ((1 << i) & (state))
  	    {
  	  	    int pos = (1 << i);
  	    	ret += dfs(n,now + 1,zuo + pos << 1,you + pos >> 1,has + pos);
		}
	return ret;
  }
int main()
{
    int n;
    cin >> n;
    cout << dfs(n);
    return 0;
}

我们把这个良好的实现挪到编译期就行了...八皇后的实现还是比较复杂了...包含了一部分元编程难点...有兴趣的自己看看代码
https://www.cnblogs.com/XDU-mzb/p/14832089.html
完整实现过一遍模板元实现lisp,对编程能力有一个巨大的锻炼

posted @ 2021-05-24 15:19  XDU18清欢  阅读(434)  评论(1)    收藏  举报