-
2021-04-04 17:26:59更多相关内容
-
C++模板元编程 pdf 带完整目录
2018-12-10 19:37:32C++模板元编程 pdf 带完整目录 本书清晰地揭示了现代C++最具威力的使用手法,将实际的工具和技术交付普通程序员的手中。 元编程是产生或操纵程序码的程序。自从泛型编程被引入C++中以来,程序员们已经发现用于当程序... -
C++模板元编程
2020-12-22 17:34:57模板元编程(metaprogramming)意思是,编程系统将会执行我们所写的代码,来生成新的代码,而这些新代码才真正实现了我们所期望的功能。元编程大的特点在于:某些用户自定义的计算可以在编译期进行,二者通常能够在... -
C++11模版元编程
2020-08-12 23:08:011.概述 模版元编程(template metaprogram)是C++中最复杂也是威力最强大的编程范式,它是一...因此,模版元编程需要很多技巧,常常需要类型重定义、枚举常量、继承、模板偏特化等方法来配合,因此编写模版元编程比1.概述
模版元编程(template metaprogram)是C++中最复杂也是威力最强大的编程范式,它是一种可以创建和操纵程序的程序。模版元编程完全不同于普通的运行期程序,它很独特,因为模版元程序的执行完全是在编译期,并且模版元程序操纵的数据不能是运行时变量,只能是编译期常量,不可修改,另外它用到的语法元素也是相当有限,不能使用运行期的一些语法,比如if-else,for等语句都不能用。因此,模版元编程需要很多技巧,常常需要类型重定义、枚举常量、继承、模板偏特化等方法来配合,因此编写模版元编程比较复杂也比较困难。
现在C++11新增了一些模版元相关的特性,不仅可以让我们编写模版元程序变得更容易,还进一步增强了泛型编程的能力,比如type_traits让我们不必再重复发明轮子了,给我们提供了大量便利的元函数,还提供了可变模板参数和tuple,让模版元编程“如虎添翼”。本文将向读者展示C++11中模版元编程常用的技巧和具体应用。
2.模版元基本概念
模版元程序由元数据和元函数组成,元数据就是元编程可以操作的数据,即C++编译器在编译期可以操作的数据。元数据不是运行期变量,只能是编译期常量,不能修改,常见的元数据有enum枚举常量、静态常量、基本类型和自定义类型等。
元函数是模板元编程中用于操作处理元数据的“构件”,可以在编译期被“调用”,因为它的功能和形式和运行时的函数类似,而被称为元函数,它是元编程中最重要的构件。元函数实际上表现为C++的一个类、模板类或模板函数,它的通常形式如下:
template<int N, int M> struct meta_func { static const int value = N+M; }
调用元函数获取value值:cout<<meta_func<1, 2>::value<<endl;
meta_func的执行过程是在编译期完成的,实际执行程序时,是没有计算动作而是直接使用编译期的计算结果的。元函数只处理元数据,元数据是编译期常量和类型,所以下面的代码是编译不过的:
int i = 1, j = 2; meta_func<i, j>::value; //错误,元函数无法处理运行时普通数据
模板元编程产生的源程序是在编译期执行的程序,因此它首先要遵循C++和模板的语法,但是它操作的对象不是运行时普通的变量,因此不能使用运行时的C++关键字(如if、else、for),可用的语法元素相当有限,最常用的是:
- enum、static const,用来定义编译期的整数常量;
- typedef/using,用于定义元数据;
- T、Args...,声明元数据类型;
- template,主要用于定义元函数;
- "::",域运算符,用于解析类型作用域获取计算结果(元数据)。
如果模板元编程中需要if-else、for等逻辑时该怎么办呢?
模板元中的if-else可以通过type_traits来实现,它不仅仅可以在编译期做判断,还可以做计算、查询、转换和选择。
模板元中的for等逻辑可以通过递归、重载、和模板特化(偏特化)等方法实现。
下面来看看C++11提供的模版元基础库type_traits。
3.type_traits
type_traits是C++11提供的模板元基础库,通过type_traits可以实现在编译期计算、查询、判断、转换和选择,提供了模板元编程需要的一些常用元函数。下面来看看一些基本的type_traits的基本用法。
最简单的一个type_traits是定义编译期常量的元函数integral_constant,它的定义如下:
template< class T, T v > struct integral_constant;
借助这个简单的trait,我们可以很方便地定义编译期常量,比如定义一个值为1的int常量可以这样定义:
using one_type = std::integral_constant<int, 1>;
或者
template<class T> struct one_type : std::integral_constant<int, 1>{};
获取常量则通过one_type::value来获取,这种定义编译期常量的方式相比C++98/03要简单,在C++98/03中定义编译期常量一般是这样定义的:
template<class T> struct one_type { enum{value = 1}; }; template<class T> struct one_type { static const int value = 1; };
可以看到,通过C++11的type_traits提供的一个简单的integral_constant就可以很方便的定义编译期常量,而无需再去通过定义enum和static const变量方式去定义编译期常量了,这也为定义编译期常量提供了另外一种方法。C++11的type_traits已经提供了编译期的true和false,是通过integral_constant来定义的:
typedef integral_constant<bool, true> true_type; typedef integral_constant<bool, false> false_type;
除了这些基本的元函数之外,type_traits还提供了丰富的元函数,比如用于编译期判断的元函数:
这只是列举一小部分的type_traits元函数,type_traits提供了上百个方便的元函数,读者可以参考http://en.cppreference.com/w/cpp/header/type_traits,这些基本的元函数用法比较简单:
#include <iostream> #include <type_traits> int main() { std::cout << "int: " << std::is_const<int>::value << std::endl; std::cout << "const int: " << std::is_const<const int>::value << std::endl; //判断类型是否相同 std::cout<< std::is_same<int, int>::value<<"\n";// true std::cout<< std::is_same<int, unsignedint>::value<<"\n";// false //添加、移除const cout << std::is_same<const int, add_const<int>::type>::value << endl; cout << std::is_same<int, remove_const<const int>::type>::value << endl; //添加引用 cout << std::is_same<int&, add_lvalue_reference<int>::type>::value << endl; cout << std::is_same<int&&, add_rvalue_reference<int>::type>::value << endl; //取公共类型 typedef std::common_type<unsigned char, short, int>::type NumericType; cout << std::is_same<int, NumericType>::value << endl; return 0; }
type_traits还提供了编译期选择traits:std::conditional,它在编译期根据一个判断式选择两个类型中的一个,和条件表达式的语义类似,类似于一个三元表达式。它的原型是:
template< bool B, class T, class F > struct conditional;
用法比较简单:
#include <iostream> #include <type_traits> int main() { typedef std::conditional<true,int,float>::type A; // int typedef std::conditional<false,int,float>::type B; // float typedef std::conditional<(sizeof(long long) >sizeof(long double)), long long, long double>::type max_size_t; cout<<typeid(max_size_t).name()<<endl; //long double }
另外一个常用的type_traits是std::decay(朽化),它对于普通类型来说std::decay(朽化)是移除引用和cv符,大大简化了我们的书写。除了普通类型之外,std::decay还可以用于数组和函数,具体的转换规则是这样的:
先移除T类型的引用,得到类型U,U定义为remove_reference<T>::type。
- 如果is_array<U>::value为 true,修改类型type为remove_extent<U>::type *。
- 否则,如果is_function<U>::value为 true,修改类型type将为add_pointer<U>::type。
- 否则,修改类型type为 remove_cv<U>::type。
std::decay的基本用法:
typedef std::decay<int>::type A; // int typedef std::decay<int&>::type B; // int typedef std::decay<int&&>::type C; // int typedef std::decay<constint&>::type D; // int typedef std::decay<int[2]>::type E; // int* typedef std::decay<int(int)>::type F; // int(*)(int)
std::decay除了移除普通类型的cv符的作用之外,还可以将函数类型转换为函数指针类型,从而将函数指针变量保存起来,以便在后面延迟执行,比如下面的例子。
template<typename F> struct SimpFunction { using FnType = typename std::decay<F>::type;//先移除引用再添加指针 SimpFunction(F& f) : m_fn(f){} void Run() { m_fn(); } FnType m_fn; };
如果要保存输入的函数,则先要获取函数对应的函数指针类型,这时就可以用std::decay来获取函数指针类型了,using FnType = typename std::decay<F>::type;实现函数指针类型的定义。type_traits还提供了获取可调用对象返回类型的元函数:std::result_of,它的基本用法:
int fn(int) {return int();} // function typedef int(&fn_ref)(int); // function reference typedef int(*fn_ptr)(int); // function pointer struct fn_class { int operator()(int i){return i;} }; // function-like class int main() { typedef std::result_of<decltype(fn)&(int)>::type A; // int typedef std::result_of<fn_ref(int)>::type B; // int typedef std::result_of<fn_ptr(int)>::type C; // int typedef std::result_of<fn_class(int)>::type D; // int }
type_traits还提供了一个很有用的元函数std::enable_if,它利用SFINAE(substitude failure is not an error)特性,根据条件选择重载函数的元函数std::enable_if,它的原型是:
template<bool B, class T = void> struct enable_if;
根据enable_if的字面意思就可以知道,它使得函数在判断条件B仅仅为true时才有效,它的基本用法:
template <class T> typename std::enable_if<std::is_arithmetic<T>::value, T>::type foo(T t) { return t; } auto r = foo(1); //返回整数1 auto r1 = foo(1.2); //返回浮点数1.2 auto r2 = foo(“test”); //compile error
在上面的例子中对模板参数T做了限定,即只能是arithmetic(整型和浮点型)类型,如果为非arithmetic类型,则编译不通过,因为std::enable_if只对满足判断式条件的函数有效,对其他函数无效。
可以通过enable_if来实现编译期的if-else逻辑,比如下面的例子通过enable_if和条件判断式来将入参分为两大类,从而满足所有的入参类型:
template <class T> typename std::enable_if<std::is_arithmetic<T>::value, int>::type foo1(T t) { cout << t << endl; return 0; } template <class T> typename std::enable_if<!std::is_arithmetic<T>::value, int>::type foo1(T &t) { cout << typeid(T).name() << endl; return 1; }
对于arithmetic类型的入参则返回0,对于非arithmetic的类型则返回1,通过arithmetic将所有的入参类型分成了两大类进行处理。从上面的例子还可以看到,std::enable_if可以实现强大的重载机制,因为通常必须是参数不同才能重载,如果只有返回值不同是不能重载的,而在上面的例子中,返回类型相同的函数都可以重载。
C++11的type_traits提供了近百个在编译期计算、查询、判断、转换和选择的元函数,为我们编写元程序提供了很大的便利。如果说C++11的type_traits让模版元编程变得简单,那么C++11提供的可变模板参数和tuple则进一步增强了模板元编程。
4.可变模板参数
C++11的可变模版参数(variadic templates)是C++11新增的最强大的特性之一,它对参数进行了高度泛化,它能表示0到任意个数、任意类型的参数。关于它的用法和使用技巧读者可以参考笔者在程序员2015年2月A上的文章:泛化之美--C++11可变模版参数的妙用,这里不再赘述,这里将要展示的如何借助可变模板参数实现一些编译期算法,比如获取最大值、判断是否包含了某个类型、根据索引查找类型、获取类型的索引和遍历类型等算法。实现这些算法需要结合type_traits或其它C++11特性,下面来看看这些编译期算法是如何实现的。
编译期从一个整形序列中获取最大值:
//获取最大的整数 template <size_t arg, size_t... rest> struct IntegerMax; template <size_t arg> struct IntegerMax<arg> : std::integral_constant<size_t, arg> { }; template <size_t arg1, size_t arg2, size_t... rest> struct IntegerMax<arg1, arg2, rest...> : std::integral_constant<size_t, arg1 >= arg2 ? IntegerMax<arg1, rest...>::value : IntegerMax<arg2, rest...>::value > { };
这个IntegerMax的实现用到了type_traits中的std::integral_const,它在展开参数包的过程中,不断的比较,直到所有的参数都比较完,最终std::integral_const的value值即为最大值。它的使用很简单:
cout << IntegerMax<2, 5, 1, 7, 3>::value << endl; //value为7
我们可以在IntegerMax的基础上轻松的实现获取最大内存对齐值的元函数MaxAlign。
编译期获取最大的align:
template<typename... Args> struct MaxAlign : std::integral_constant<int, IntegerMax<std::alignment_of<Args>::value...>::value>{}; cout << MaxAlign<int, short, double, char>::value << endl; //value为8 编译判断是否包含了某种类型: template < typename T, typename... List > struct Contains; template < typename T, typename Head, typename... Rest > struct Contains<T, Head, Rest...> : std::conditional< std::is_same<T, Head>::value, std::true_type, Contains<T, Rest... >> ::type{}; template < typename T > struct Contains<T> : std::false_type{}; 用法:cout<<Contains<int, char, double, int, short>::value<<endl; //输出true
这个Contains的实现用到了type_traits的std::conditional、std::is_same、std::true_type和std::false_type,它的实现思路是在展开参数包的过程中不断的比较类型是否相同,如果相同则设置值为true,否则设置为false。
编译期获取类型的索引:
template < typename T, typename... List > struct IndexOf; template < typename T, typename Head, typename... Rest > struct IndexOf<T, Head, Rest...> { enum{ value = IndexOf<T, Rest...>::value+1 }; }; template < typename T, typename... Rest > struct IndexOf<T, T, Rest...> { enum{ value = 0 }; }; template < typename T > struct IndexOf<T> { enum{value = -1}; };
用法:cout<< IndexOf<int, double, short, char, int, float>::value<<endl; //输出3
这个IndexOf的实现比较简单,在展开参数包的过程中看是否匹配到特化的IndexOf<T, T, Rest...>,如果匹配上则终止递归将之前的value累加起来得到目标类型的索引位置,否则将value加1,如果所有的类型中都没有对应的类型则返回-1;
编译期根据索引位置查找类型:
template<int index, typename... Types> struct At; template<int index, typename First, typename... Types> struct At<index, First, Types...> { using type = typename At<index - 1, Types...>::type; }; template<typename T, typename... Types> struct At<0, T, Types...> { using type = T; }; 用法: using T = At<1, int, double, char>::type; cout << typeid(T).name() << endl; //输出double
At的实现比较简单,只要在展开参数包的过程中,不断的将索引递减至0时为止即可获取对应索引位置的类型。接下来看看如何在编译期遍历类型。
template<typename T> void printarg() { cout << typeid(T).name() << endl; } template<typename... Args> void for_each() { std::initializer_list<int>{(printarg<Args>(), 0)...}; } 用法:for_each<int,double>();//将输出int double
这里for_each的实现是通过初始化列表和逗号表达式来遍历可变模板参数的。
可以看到,借助可变模板参数和type_traits以及模板偏特化和递归等方式我们可以实现一些有用的编译期算法,这些算法为我们编写应用层级别的代码奠定了基础,后面模板元编程的具体应用中将会用到这些元函数。
C++11提供的tuple让我们编写模版元程序变得更灵活了,在一定程度上增强了C++的泛型编程能力,下面来看看tuple如何应用于元程序中的。
5.tuple与模版元
C++11的tuple本身就是一个可变模板参数组成的元函数,它的原型如下:
template<class...Types>
class tuple;tuple在模版元编程中的一个应用场景是将可变模板参数保存起来,因为可变模板参数不能直接作为变量保存起来,需要借助tuple保存起来,保存之后再在需要的时候通过一些手段将tuple又转换为可变模板参数,这个过程有点类似于化学中的“氧化还原反应”。看看下面的例子中,可变模板参数和tuple是如何相互转换的:
//定义整形序列 template<int...> struct IndexSeq{}; //生成整形序列 template<int N, int... Indexes> struct MakeIndexes : MakeIndexes<N - 1, N - 1, Indexes...>{}; template<int... indexes> struct MakeIndexes<0, indexes...>{ typedef IndexSeq<indexes...> type; }; template<typename... Args> void printargs(Args... args){ //先将可变模板参数保存到tuple中 print_helper(typename MakeIndexes<sizeof... (Args)>::type(), std::make_tuple(args...)); } template<int... Indexes, typename... Args> void print_helper(IndexSeq<Indexes...>, std::tuple<Args...>&& tup){ //再将tuple转换为可变模板参数,将参数还原回来,再调用print print(std::get<Indexes>(tup)...); } template<typename T> void print(T t) { cout << t << endl; } template<typename T, typename... Args> void print(T t, Args... args) { print(t); print(args...); }
用法:printargs(1, 2.5, “test”); //将输出1 2.5 test
上面的例子print实际上是输出可变模板参数的内容,具体做法是先将可变模板参数保存到tuple中,然后再通过元函数MakeIndexes生成一个整形序列,这个整形序列就是IndexSeq<0,1,2>,整形序列代表了tuple中元素的索引,生成整形序列之后再调用print_helper,在print_helper中展开这个整形序列,展开的过程中根据具体的索引从tuple中获取对应的元素,最终将从tuple中取出来的元素组成一个可变模板参数,从而实现了tuple“还原”为可变模板参数,最终调用print打印可变模板参数。
tuple在模板元编程中的另外一个应用场景是用来实现一些编译期算法,比如常见的遍历、查找和合并等算法,实现的思路和可变模板参数实现的编译期算法类似,关于tuple相关的算法,读者可以参考笔者在github上的代码:https://github.com/qicosmos/cosmos/tree/master/tuple。
下面来看看模版元的具体应用。
6.模版元的应用
我们将展示如何通过模版元来实现function_traits和Vairant类型。
function_traits用来获取函数语义的可调用对象的一些属性,比如函数类型、返回类型、函数指针类型和参数类型等。下面来看看如何实现function_traits。
template<typename T> struct function_traits; //普通函数 template<typename Ret, typename... Args> struct function_traits<Ret(Args...)> { public: enum { arity = sizeof...(Args) }; typedef Ret function_type(Args...); typedef Ret return_type; using stl_function_type = std::function<function_type>; typedef Ret(*pointer)(Args...); template<size_t I> struct args { static_assert(I < arity, "index is out of range, index must less than sizeof Args"); using type = typename std::tuple_element<I, std::tuple<Args...>>::type; }; }; //函数指针 template<typename Ret, typename... Args> struct function_traits<Ret(*)(Args...)> : function_traits<Ret(Args...)>{}; //std::function template <typename Ret, typename... Args> struct function_traits<std::function<Ret(Args...)>> : function_traits<Ret(Args...)>{}; //member function #define FUNCTION_TRAITS(...) \ template <typename ReturnType, typename ClassType, typename... Args>\ struct function_traits<ReturnType(ClassType::*)(Args...) __VA_ARGS__> : function_traits<ReturnType(Args...)>{}; \ FUNCTION_TRAITS() FUNCTION_TRAITS(const) FUNCTION_TRAITS(volatile) FUNCTION_TRAITS(const volatile) //函数对象 template<typename Callable> struct function_traits : function_traits<decltype(&Callable::operator())>{};
由于可调用对象可能是普通的函数、函数指针、lambda、std::function和成员函数,所以我们需要针对这些类型分别做偏特化。其中,成员函数的偏特化稍微复杂一点,因为涉及到cv符的处理,这里通过定义一个宏来消除重复的模板类定义。参数类型的获取我们是借助于tuple,将参数转换为tuple类型,然后根据索引来获取对应类型。它的用法比较简单:
template<typename T> void PrintType() { cout << typeid(T).name() << endl; } int main() { std::function<int(int)> f = [](int a){return a; }; PrintType<function_traits<std::function<int(int)>>::function_type>(); //将输出int __cdecl(int) PrintType<function_traits<std::function<int(int)>>::args<0>::type>();//将输出int PrintType<function_traits<decltype(f)>::function_type>();//将输出int __cdecl(int) }
有了这个function_traits和前面实现的一些元函数,我们就能方便的实现一个“万能类型”—Variant,Variant实际上一个泛化的类型,这个Variant和boost.variant的用法类似。boost.variant的基本用法如下:
typedef variant<int,char, double> vt; vt v = 1; v = 'a'; v = 12.32;
这个variant可以接受已经定义的那些类型,看起来有点类似于c#和java中的object类型,实际上variant是擦除了类型,要获取它的实际类型的时候就稍显麻烦,需要通过boost.visitor来访问:
View Code
通过C++11模版元实现的Variant将改进值的获取,将获取实际值的方式改为内置的,即通过下面的方式来访问:
typedef Variant<int, double, string, int> cv; cv v = 10; v.Visit([&](double i){cout << i << endl; }, [](short i){cout << i << endl; }, [=](int i){cout << i << endl; },[](const string& i){cout << i << endl; });//结果将输出10
这种方式更方便直观。Variant的实现需要借助前文中实现的一些元函数MaxInteger、MaxAlign、Contains和At等等。下面来看看Variant实现的关键代码,完整的代码请读者参考笔者在github上的代码https://github.com/qicosmos/cosmos/blob/master/Varaint.hpp。
View Code
实现Variant首先需要定义一个足够大的缓冲区用来存放不同的类型的值,这个缓类型冲区实际上就是用来擦除类型,不同的类型都通过placement new在这个缓冲区上创建对象,因为类型长度不同,所以需要考虑内存对齐,C++11刚好提供了内存对齐的缓冲区aligned_storage:
template< std::size_t Len, std::size_t Align = /*default-alignment*/ > struct aligned_storage;
它的第一个参数是缓冲区的长度,第二个参数是缓冲区内存对齐的大小,由于Varaint可以接受多种类型,所以我们需要获取最大的类型长度,保证缓冲区足够大,然后还要获取最大的内存对齐大小,这里我们通过前面实现的MaxInteger和MaxAlign就可以了,Varaint中内存对齐的缓冲区定义如下:
enum { data_size = IntegerMax<sizeof(Types)...>::value, align_size = MaxAlign<Types...>::value }; using data_t = typename std::aligned_storage<data_size, align_size>::type; //内存对齐的缓冲区类型
其次,我们还要实现对缓冲区的构造、拷贝、析构和移动,因为Variant重新赋值的时候需要将缓冲区中原来的类型析构掉,拷贝构造和移动构造时则需要拷贝和移动。这里以析构为例,我们需要根据当前的type_index来遍历Variant的所有类型,找到对应的类型然后调用该类型的析构函数。
void Destroy(const type_index& index, void * buf) { std::initializer_list<int>{(Destroy0<Types>(index, buf), 0)...}; } template<typename T> void Destroy0(const type_index& id, void* data) { if (id == type_index(typeid(T))) reinterpret_cast<T*>(data)->~T(); }
这里,我们通过初始化列表和逗号表达式来展开可变模板参数,在展开的过程中查找对应的类型,如果找到了则析构。在Variant构造时还需要注意一个细节是,Variant不能接受没有预先定义的类型,所以在构造Variant时,需要限定类型必须在预定义的类型范围当中,这里通过type_traits的enable_if来限定模板参数的类型。
template <class T, class = typename std::enable_if<Contains<typename std::remove_reference<T>::type, Types...>::value>::type> Variant(T&& value) : m_typeIndex(typeid(void)){ Destroy(m_typeIndex, &m_data); typedef typename std::remove_reference<T>::type U; new(&m_data) U(std::forward<T>(value)); m_typeIndex = type_index(typeid(U)); }
这里enbale_if的条件就是前面实现的元函数Contains的值,当没有在预定义的类型中找到对应的类型时,即Contains返回false时,编译期会报一个编译错误。
最后还需要实现内置的Vistit功能,Visit的实现需要先通过定义一系列的访问函数,然后再遍历这些函数,遍历过程中,判断函数的第一个参数类型的type_index是否与当前的type_index相同,如果相同则获取当前类型的值。
template<typename F> void Visit(F&& f){ using T = typename Function_Traits<F>::template arg<0>::type; if (Is<T>()) f(Get<T>()); } template<typename F, typename... Rest> void Visit(F&& f, Rest&&... rest){ using T = typename Function_Traits<F>::template arg<0>::type; if (Is<T>()) Visit(std::forward<F>(f)); else Visit(std::forward<Rest>(rest)...); }
Visit功能的实现利用了可变模板参数和function_traits,通过可变模板参数来遍历一系列的访问函数,遍历过程中,通过function_traits来获取第一个参数的类型,和Variant当前的type_index相同的则取值。为什么要获取访问函数第一个参数的类型呢?因为Variant的值是唯一的,只有一个值,所以获取的访问函数的第一个参数的类型就是Variant中存储的对象的实际类型。
7总结
C++11中的一些特性比如type_traits、可变模板参数和tuple让模版元编程变得更简单也更强大,模版元编程虽然功能强大,但也比较复杂,要用好模版元,需要我们转变思维方式,在掌握基本的理论的基础上,再认真揣摩模版元的一些常用技巧,这些技巧是有规律可循的,基本上都是通过重定义、递归和偏特化等手法来实现的,当我们对这些基本技巧很熟悉的时候再结合不断地实践,相信对模版元编程就能做到“游刃有余”了。
本文曾发表于《程序员》2015年3月刊。转载请注明出处。
后记:本文的内容主要来自于我在公司内部培训的一次课程,因为很多人对C++11模板元编程理解得不深入,所以我觉得有必要拿出来分享一下,让更多的人看到,就整理了一下发到程序员杂志了,我相信读者看完之后对模版元会有全面深入的了解。
一点梦想:尽自己一份力,让c++的世界变得更美好!
-
《C++模板元编程实战 一个深度学习框架的初步实现》_李伟.pdf
2019-05-24 10:28:44《C++模板元编程实战:一个深度学习框架的初步实现》以一个深度学习框架的初步实现为例,讨论如何在一个相对较大的项目中深入应用元编程,为系统性能优化提供更多的可能。 -
C++模板元编程 pdf
2017-11-10 17:36:13所谓元编程就是编写直接生成或操纵程序的程序,C++ 模板给 C++ 语言提供了元编程的能力,模板使 C++ 编程变得异常灵活,能实现很多高级动态语言才有的特性 -
C++模板元编程_带目录.pdf
2018-08-13 14:56:47C++模板元编程,ISBN:9787111267423,作者:(美)大卫 等著,荣耀 译 -
C++模板元编程(高清pdf扫描版)
2018-11-26 21:07:27C++模板元编程pdf格式,本书内容方面将介绍Traits和类型操纵、深入探索无函数、序列与迭代器、算法、视图与跨越编译期和运行期边界、Dsel设计演练等技术,还附有预处理无编程、Template关键字等。适合高级C++程序员... -
C++模板元编程.pdf
2017-04-22 09:55:18C++模板元编程.pdf 个人收集电子书,仅用学习使用,不可用于商业用途,如有版权问题,请联系删除! -
C++ 模板元编程
2019-08-07 22:07:43C++ 模板元编程 英文:C++ template metaprogramming meta: “元”, 元数据(meta data) metaprogramming可以理解为对程序语言自身的某种特性,对程序语句进行编程, 更简洁的说,对编程的编程--元编程。元编程有...C++ 模板元编程
英文:C++ template metaprogramming
meta: “元”, 元数据(meta data)
metaprogramming可以理解为对程序语言自身的某种特性,对程序语句进行编程,
更简洁的说,对编程的编程--元编程。元编程有两种手段,一种是使用宏,另一种是使用模板。
今天聊的是模板元编程。C++的模板元编程(TMP)主要是利用模板的特化和偏特化机制,在编译时获取类型信息,
并依照类型信息进行代码分发(dispatch),简化代码编写,提高代码的可维护性以及省略
不必要的运行时代码路径选择。
这里提到了“编译时获取类型信息”,很多人可能觉得很奇怪:一般情况下,我们更感兴趣
的是“运行时获取类型信息”,而在编译时,编译器是肯定知道类型信息的,根本不需要获
取!没错,类型信息编译器是知道的,但我们不知道,更准确是我们的代码在编译时不知
道类型信息,TMP就是获取类型信息的手段。
上code之前,先解释:
__CLRCALL_OR_CDECL 函数的调用约定
函数调用约定,是指当一个函数被调用时,函数的参数会被传递给被调用的函数和返回值
会被返回给调用函数。函数的调用约定就是描述参数是怎么传递和由谁平衡堆栈的,当然
还有返回值。(摘自百度百科)
几种类型
__stdcall,__cdecl,__fastcall,__thiscall,__nakedcall,__pascal,__vectorcall
约束事件
参数传递顺序
1.从右到左依次入栈:__stdcall,__cdecl,__thiscall,__fastcall
2.从左到右依次入栈:__pascal
调用堆栈清理
1.调用者清除栈。
2.被调用函数返回后清除栈。
举例:
template<class Init,class Diff>
inline void __CLRCALL_OR_CDECL advance(Init& Where,Diff Off){
//这里根据第一个参数Where的具体型别,调用不同的模板Advance
Advance(Where,Off,Iter_cat(Where));
}template <class Iter>
inline typename iterator_traits<Iter>::iterator_category
__CLRCALL_OR_CDECL Iter_cat(const Iter&){
//这里返回参数的具体型别,这在程序编译的时候编译器就可确定
typename iterator_traits<Iter>::iterator_category Cat;
return Cat;
}//根据Where型别不同,调用不同的模板特化版本
template<class Init,class Diff>
inline void __CLRCALL_OR_CDECL Advance(Init& Where,Diff Off,
input_iterator_tag){
#if _HAS_ITERATOR_DEBUGGING
#endif
for(;0<Off;--Off){
++Where;
}
}
template<class Init,class Diff>
inline void __CLRCALL_OR_CDECL Advance(Init& Where,Diff Off,
bidirectional_iterator_tag){
for(;0<Off;--Off){
++Where;
}
for(;Off<0;++Off){
--Where;
}
}
#pragma warning(pop)
template<class Init,class Diff>
inline void __CLRCALL_OR_CDECL Advance(Init& Where,Diff Off,
random_access_iterator_tag){
Where += Off;
}
}
观察advance内Advance最后一个参数Iter_cat(Where),Iter_cat是一个辅助函数,它
根据迭代器的iterator_category返回这个iterator_category的实例,而Advance有多
个版本的重载,分别对应input、forward、bidirectional、randomaccess这些
iterator_category,因为有这些重载函数,advance就能通过迭代器自身
iterator_category类型调用到相对应的_Advance实现,更有趣的是_Advance的重载参数
根本就没有形参,它仅仅是为了捕捉类型信息。好,我们把这些连起来:advance调用
Advance,Iter_cat析出迭代器的类型信息,而Advance的最后一个参数捕捉类型信息,
完成编译时代码分发!!!
----好玩不?根据特化和偏特化可以这样玩,屌的很!
另外再对__CLRCALL_OR_CDECL 也就是函数调用约定进行深入了解下为什么需要函数调用约定?
试想这样一个问题,当调用一个由别人编写的函数时,需要注意什么问题。
小明同学编写了一个函数 foo(int a,int b,int c);
当你调用foo(1,2,3),参数如何传递,参数传递的顺序是什么,(当然还会涉及一些其他的问题)
你有3块不同的颜色的砖头(红,绿、蓝)
你可以通过双手(参数如何传递),分别 红 ,绿、 蓝 传递给另一面
你可以通过双手(参数如何传递),分别 蓝 ,绿 , 红 传递给另一面
你可以通过汽车(参数如何传递),分别 红 ,绿、 蓝 传递给另一面
不管通过什么样的方式,传递顺序如何。另一面都接收到了参数,但是我们两者一致要约定好规则(传递顺序等其他规范),这样接收方就不会错误的领会到我的意思不管怎样说,阅读到这里,你应该知道了,函数调用是有约定的,不能瞎搞(虽然这个问题在你编写代码时,没有直接让你处理,但是毕竟该问题是存在的)
约定由编译器实现(遵守不遵守编译器说了算),根据情况(X86,X64)有所不同,所以下面只是参照物
实际情况有哪些?- 参数传递的顺序(从右到左,从左到用)
- 参数的存储容器的选择。(寄存器、栈区)
- 如果使用栈区,谁来释放
记住这几条,然后不同的调用约定,就套用这些实际情况
-------------------------------------------------------------------------------------------------------------------------------------------------
以上内容为本人学习摘录,来源多为网上搜索所得,如您认为侵犯您的版权,请留言告知,不胜感激。
-
C++ 高阶操作:模板元编程
2021-11-02 00:54:54星标/置顶公众号????,硬核文章第一时间送达!链接 | https://www.cnblogs.com/xiangtingshen/p/11613183.html泛型编程大家应该都很熟悉了,...星标/置顶 公众号👇,硬核文章第一时间送达!
链接 | https://www.cnblogs.com/xiangtingshen/p/11613183.html
泛型编程大家应该都很熟悉了,主要就是利用模板实现“安全的宏”,而模板元编程区别于我们所知道的泛型编程,它是一种较为复杂的模板,属于C++的高阶操作了,它最主要的优点就在于把计算过程提前到编译期,能带来可观的性能提升。接下来,请和小编一起来学习吧。
1.概述
模板元编程(Template Meta programming,TMP)是编写生成或操纵程序的程序,也是一种复杂且功能强大的编程范式(Programming Paradigm)。
C++模板给C++提供了元编程的能力,但大部分用户对 C++ 模板的使用并不是很频繁,大致限于泛型编程,在一些系统级的代码,尤其是对通用性、性能要求极高的基础库(如 STL、Boost)几乎不可避免在大量地使用 C++ 模板以及模板元编程。
模版元编程完全不同于普通的运行期程序,因为模版元程序的执行完全是在编译期,并且模版元程序操纵的数据不能是运行时变量,只能是编译期常量,不可修改。
另外它用到的语法元素也是相当有限,不能使用运行期的一些语法,比如if-else、for和while等语句都不能用。
因此,模版元编程需要很多技巧,常常需要类型重定义、枚举常量、继承、模板偏特化等方法来配合,因此模版元编程比较复杂也比较困难。
2.模板元编程的作用
C++ 模板最初是为实现泛型编程设计的,但人们发现模板的能力远远不止于那些设计的功能。
一个重要的理论结论就是:C++ 模板是图灵完备的(Turing-complete),就是用 C++ 模板可以模拟图灵机。
理论上说 C++ 模板可以执行任何计算任务,但实际上因为模板是编译期计算,其能力受到具体编译器实现的限制(如递归嵌套深度,C++11 要求至少 1024,C++98 要求至少 17)。
C++ 模板元编程是“意外”功能,而不是设计的功能,这也是 C++ 模板元编程语法丑陋的根源。
C++ 模板是图灵完备的,这使得 C++代码存在两层次,其中,执行编译计算的代码称为静态代码(static code),执行运行期计算的代码称为动态代码(dynamic code),C++的静态代码由模板实现,编写C++的静态代码,就是进行C++的模板元编程。
具体来说 C++ 模板可以做以下事情:编译期数值计算、类型计算、代码计算(如循环展开),其中数值计算实际意义不大,而类型计算和代码计算可以使得代码更加通用,更加易用,性能更好(也更难阅读,更难调试,有时也会有代码膨胀问题)。
编译期计算在编译过程中的位置请见下图。
使用模板元编程的基本原则就是:将负载由运行时转移到编译时,同时保持原有的抽象层次。
其中负载可以分为两类,一类就是程序运行本身的开销,一类则是程序员需要编写的代码。
前者可以理解为编译时优化,后者则是为提高代码复用度,从而提高程序员的编程效率。
图灵完备:
图灵完备是对计算能力的描述。
简单判定图灵完备的方法就是看该语言能否模拟出图灵机图灵不完备的语言常见原因有循环或递归受限(无法写不终止的程序,如 while(true){}; ), 无法实现类似数组或列表这样的数据结构(不能模拟纸带). 这会使能写的程序有限图灵完备可能带来坏处, 如C++的模板语言, 模板语言是在类型检查时执行, 如果编译器不加以检查,我们完全可以写出使得C++编译器陷入死循环的程序.图灵不完备也不是没有意义, 有些场景我们需要限制语言本身. 如限制循环和递归, 可以保证该语言能写的程序一定是终止的。
3.模板元编程的组成要素
从编程范式上来说,C++模板元编程是函数式编程,用递归形式实现循环结构的功能,用C++ 模板的特例化提供了条件判断能力,这两点使得其具有和普通语言一样通用的能力(图灵完备性)。
模版元程序由元数据和元函数组成,元数据就是元编程可以操作的数据,即C++编译器在编译期可以操作的数据。
元数据不是运行期变量,只能是编译期常量,不能修改,常见的元数据有enum枚举常量、静态常量、基本类型和自定义类型等。
元函数是模板元编程中用于操作处理元数据的“构件”,可以在编译期被“调用”,因为它的功能和形式 和 运行时的函数类似,而被称为元函数,它是元编程中最重要的构件。
元函数实际上表现为C++的一个类、模板类或模板函数,它的通常形式如下:
template<int N, int M> struct meta_func { static const int value = N+M; }
调用元函数获取value值:
cout<<meta_func<1, 2>::value<<endl;
meta_func的执行过程是在编译期完成的,实际执行程序时,是没有计算动作而是直接使用编译期的计算结果。元函数只处理元数据,元数据是编译期常量和类型,所以下面的代码是编译不过的:
int i = 1, j = 2; meta_func<i, j>::value; //错误,元函数无法处理运行时普通数据
模板元编程产生的源程序是在编译期执行的程序,因此它首先要遵循C++和模板的语法,但是它操作的对象不是运行时普通的变量,因此不能使用运行时的C++关键字(如if、else、for),可用的语法元素相当有限,最常用的是:
enum、static const //用来定义编译期的整数常量; typedef/using //用于定义元数据;[类型别名] T/Args... //声明元数据类型;【模版参数:类型形参,非类型形参】 Template //主要用于定义元函数;【模版类,特化,偏特化】 :: //域运算符,用于解析类型作用域获取计算结果(元数据)。【获取元数据,元类型】
实际上,模板元中的if-else可以通过type_traits来实现,它不仅仅可以在编译期做判断,还可以做计算、查询、转换和选择。
模板元中的for等逻辑可以通过递归、重载、和模板特化(偏特化)等方法实现。
4.模板元编程的控制逻辑
第一个 C++ 模板元程序由Erwin Unruh 在 1994 年编写,这个程序计算小于给定数 N 的全部素数(又叫质数),程序并不运行(都不能通过编译),而是让编译器在错误信息中显示结果(直观展现了是编译期计算结果,C++ 模板元编程不是设计的功能,更像是在戏弄编译器。从此,C++模板元编程的能力开始被人们认识到。
在模版元程序的具体实现时,由于其执行完全是在编译期,所以不能使用运行期的一些语法,比如if-else、for和while等语句都不能用。这些控制逻辑需要通过特殊的方法来实现。
4.1 if判断
模板元编程中实现条件if判断,参考如下代码:
#include <iostream> template<bool c, typename Then, typename Else> class IF_ {}; //基础类模版 template<typename Then, typename Else> class IF_<true, Then, Else> { public: typedef Then reType; }; //类模版的偏特化; 如果第一个模版非类型参数为true,IF_<true, Then, Else>::reType的值为模版的第二个类型参数Then template<typename Then, typename Else> class IF_<false,Then, Else> { public: typedef Else reType; }; //类模版的偏特化 int main() { const int len = 4; // 定义一个指定字节数的类型 typedef IF_<sizeof(short)==len, short, IF_<sizeof(int)==len, int, IF_<sizeof(long)==len, long, IF_<sizeof(long long)==len, long long, void>::reType>::reType>::reType>::reType int_my; std::cout << sizeof(int_my) << '\n'; } /*分析最里面的一层: * IF_<sizeof(long long)==len, long long, void>::reType * 如果sizeof(long long) == 4, 上面的表达式返回long long, 否则返回void */
程序输出结果:4。
实际上,从C++11开始,可以通过type_traits来实现。因为type_traits提供了编译期选择特性:std::conditional,它在编译期根据一个判断式选择两个类型中的一个,和条件表达式的语义类似,类似于一个三元表达式。它的原型是:
template< bool B, class T, class F > struct conditional;
所以上面的代码可以改写为如下代码:
#include <iostream> #include <type_traits> int main() { const int len = 4; // 定义一个指定字节数的类型 typedef std::conditional<sizeof(short)==len, short, std::conditional<sizeof(int)==len, int, std::conditional<sizeof(long)==len, long, std::conditional<sizeof(long long)==len, long long, void>::type>::type>::type>::type int_my; std::cout << sizeof(int_my) << '\n'; }
程序同样编译输出4。
4.2循环展开
编译期的循环展开( Loop Unrolling)可以通过模板特化来结束递归展开,达到运行期的for和while语句的功能。下面看一个编译期数值计算的例子。
#include <iostream> template<int N> class sum { public: static const int ret = sum<N-1>::ret + N; }; template<> class sum<0> { public: static const int ret = 0; }; int main() { std::cout << sum<5>::ret <<std::endl; return 0; }
程序输出:15。
当编译器遇到sumt<5>时,试图实例化之,sumt<5> 引用了sumt<5-1>即sumt<4>,试图实例化sumt<4>,以此类推,直到sumt<0>,sumt<0>匹配模板特例,sumt<0>::ret为 0,sumt<1>::ret为sumt<0>::ret+1为1,以此类推,sumt<5>::ret为15。
值得一提的是,虽然对用户来说程序只是输出了一个编译期常量sumt<5>::ret,但在背后,编译器其实至少处理了sumt<0>到sumt<5>共6个类型。
从这个例子我们也可以窥探 C++ 模板元编程的函数式编程范型,对比结构化求和程序:for(i=0,sum=0; i<=N; ++i) sum+=i; 用逐步改变存储(即变量 sum)的方式来对计算过程进行编程,模板元程序没有可变的存储(都是编译期常量,是不可变的变量),要表达求和过程就要用很多个常量:sumt<0>::ret,sumt<1>::ret,...,sumt<5>::ret。
函数式编程看上去似乎效率低下(因为它和数学接近,而不是和硬件工作方式接近),但有自己的优势:描述问题更加简洁清晰,没有可变的变量就没有数据依赖,方便进行并行化。
4.3switch/case分支
同样可以通过模板特化来模拟实现编译期的switch/case分支功能。参考如下代码:
#include <iostream> using namespace std; template<int v> class Case { public: static inline void Run() { cout << "default case" << endl; } }; template<> class Case<1> { public: static inline void Run() { cout << "case 1" << endl; } }; template<> class Case<2> { public: static inline void Run() { cout << "case 2" << endl; } }; int main() { Case<2>::Run(); }
程序输出结果:
case 2
5.特性、策略与标签
利用迭代器,我们可以实现很多通用算法,迭代器在容器与算法之间搭建了一座桥梁。求和函数模板如下:
#include <iostream> #include <vector> template<typename iter> typename iter::value_type mysum(iter begin, iter end) { typename iter::value_type sum(0); for(iter i=begin; i!=end; ++i) sum += *i; return sum; } int main() { std::vector<int> v; for(int i = 0; i<100; ++i) v.push_back(i);v.push_back(i); std::cout << mysum(v.begin(), v.end()) << '\n'; }
程序编译输出:4950。
我们想让 mysum() 对指针参数也能工作,毕竟迭代器就是模拟指针,但指针没有嵌套类型 value_type,可以定义 mysum() 对指针类型的特例,但更好的办法是在函数参数和 value_type 之间多加一层特性(traits)。
template<typename iter> class mytraits //标准容器通过这里获取容器元素的类型 { public: typedef typename iter::value_type value_type; }; template<typename T> class mytraits<T*> //数组类型的容器,通过这里获取数组元素的类型 { public: typedef T value_type; }; template<typename iter> typename mytraits<iter>::value_type mysum(iter begin, iter end) { typename mytraits<iter>::value_type sum(0); for(iter i=begin; i!=end; ++i) sum += *i; return sum; } int main() { int v[4] = {1,2,3,4}; std::cout << mysum(v, v+4) << '\n'; return 0; }
程序输出:10。
其实,C++ 标准定义了类似的 traits, std::iterator_trait(另一个经典例子是 std::numeric_limits) 。
traits特性对类型的信息(如 value_type、 reference)进行包装,使得上层代码可以以统一的接口访问这些信息。
C++ 模板元编程会涉及大量的类型计算,很多时候要提取类型的信息(typedef、 常量值等),如果这些类型信息的访问方式不一致(如上面的迭代器和指针),我们将不得不定义特例,这会导致大量重复代码的出现(另一种代码膨胀),而通过加一层特性可以很好的解决这一问题。
另外,特性不仅可以对类型的信息进行包装,还可以提供更多信息,当然,因为加了一层,也带来复杂性。特性是一种提供元信息的手段。
策略(policy)一般是一个类模板,典型的策略是 STL 容器的分配器(如std::vector<>,完整声明是template<class T, class Alloc=allocator<T>> class vector;)(这个参数有默认参数,即默认存储策略),策略类将模板的经常变化的那一部分子功能块集中起来作为模板参数,这样模板便可以更为通用,这和特性的思想是类似的。
标签(tag)一般是一个空类,其作用是作为一个独一无二的类型名字用于标记一些东西,典型的例子是 STL 迭代器的五种类型的名字。
input_iterator_tag output_iterator_tag forward_iterator_tag bidirectional_iterator_tag random_access_iterator_tag
实际上,
std::vector<int>::iterator::iterator_category就是random_access_iterator_tag, 可以使用type_traits的特性is_same来判断类型是否相同。
#include <iostream> #include <vector> #include <type_traits> int main() { std::cout << is_same<std::vector<int>::iterator::iterator_category, std::random_access_iterator_tag >::value << std::endl; return 0; }
程序输出:1。
有了这样的判断,还可以根据判断结果做更复杂的元编程逻辑(如一个算法以迭代器为参数,根据迭代器标签进行特例化以对某种迭代器特殊处理)。标签还可以用来分辨函数重载。
6.小结
C++模板元编程是图灵完备的且是函数式编程,主要特点是代码在编译期执行,可用于编译期数值计算,能够获得更有效率的运行码。模板的使用,也提高了代码泛化。与此同时,模板元编程也存一定缺点,主要有:
(1)模板元编程产生的代码较为复杂,难易阅读,可读性较差;
(2)大量模板的使用,编译时容易导致代码膨胀,提高了编译时间;
(3)对于C++来说,由于各编译器的差异,大量依赖模板元编程(特别是最新形式的)的代码可能会有移植性的问题。所以,对于模板元编程,我们需要扬其长避其短,合理使用模板元编程。
往期推荐
关注公众号「高效程序员」👇,一起优秀!
回复 “入群” 进技术交流群,回复 “1024” 获取海量学习资源。
-
《 C++模板》和《C++模板元编程》两本
2018-03-20 15:30:15两本学习C++模板的书,先学习C++模板再学习模板元编程,中高级难度 -
c++模板元编程详解
2018-05-31 17:39:23所谓元编程就是编写直接生成或操纵程序的程序,C++ 模板给 C++ 语言提供了元编程的能力,模板使 C++ 编程变得异常灵活,能实现很多高级动态语言才有的特性(语法上可能比较丑陋,一些历史原因见下文)。普通用户对 ... -
浅谈C++模板元编程
2021-01-20 05:54:23所谓元编程就是编写直接生成或操纵程序的程序,C++ 模板给 C++ 语言提供了元编程的能力,模板使 C++ 编程变得异常灵活,能实现很多高级动态语言才有的特性(语法上可能比较丑陋,一些历史原因见下文)。模板元编程的... -
论文研究-函数式程序模板元编程的元建模实现方法.pdf
2019-09-08 00:30:41针对函数式程序模板元编程的通用性问题,以应用类型系统ATS(Applied Type System)为例,提出了一种基于元建模的模板元编程实现方法。基于ATS模板元编程给出从枚举类型Datatype到Function的生成实例;通过元建模... -
模板元编程技术
2017-03-10 19:46:43模板元编程技术 -
C++模板元编程的一点体会
2020-12-22 19:48:22c++ 模板元编程的力量远比第一眼印象里所能想像得要强大,当然,这个结论并不明显,很多时候人们也拿模板当作减少重复代码的工具简单使用,很少有人会像写 STL, boost, loki 那样子正儿八经完全以模板为根基... -
Super-Template-Tetris, Tetris作为 C 模板元编程.zip
2019-09-18 04:36:50Super-Template-Tetris, Tetris作为 C 模板元编程 超级模板俄罗斯方块Tetris作为 C 模板元编程的实现。 所有游戏逻辑在编译时实现。 运行时程序只需要打印出游戏世界并将它的序列化以便下一次播放。帖子讨论实现问题... -
C++11模板元编程-std::enable_if示例详解
2021-01-21 15:22:07C++11中引入了std::enable_if函数,函数原型如下: template ...由上可知,只有当第一个模板参数为true时,enable_if会包含一个type=T的公有成员,否则没有该公有成员。 头文件: #include <type_t -
c++模板元编程
2015-03-20 10:18:41c++开源项目 将c++模板编程推向了一个新的高峰,同时这也是模板元编程的非常好的一份源代码 -
C++模板元编程 中文版 源码
2012-04-18 08:43:57C++模板元编程 中文版 随书关盘 全部源码 -
模板元编程概述
2015-03-31 11:51:08详细描述C/C++的模板元编程的相关内容 -
现代C++模板元编程基础
2019-01-17 00:08:46C++的模板元编程是函数式编程,所以函数是一等公民。一切在编译期间执行的函数都可以称为元函数。元函数有struct和constexpr两种定义方式,前者是一直使用的,后者是C++11以及以后版本的关键字。constexpr字面意思是... -
C++模板元编程简介
2021-05-02 17:10:391.概述 模板元编程(Template Metaprogramming,TMP)是编写生成或操纵程序的程序,也是一种复杂且功能强大的编程范式...模版元编程完全不同于普通的运行期程序,因为模版元程序的执行完全是在编译期,并且模版 -
泛型编程与模板元编程介绍(概念、优点)
2019-08-29 17:19:40今天来写点有点难度的东东:泛型编程与模板元编程。 写这种生僻的东东我是比较排斥的,但谁让我看到boost库中专门对这两个东东分了个大类别呢。开始吧~ 概念 不知道大家有没有过这种体验,学数学时整本书上的概念都... -
tlx模版库使用b+tree容器模版以及一些模版元编程技术
2020-02-07 22:24:42tlx模版库使用b+tree容器模版以及一些模版元编程技术,包括一系列基础设施以及测试用例, LRUCache容器等等,我正在考虑将其用于游戏开发的项目