1.概述

  关于C++11模板元的基本用法和常用技巧,我在程序员2015年2月B《C++11模版元编程》一文(后称前文)中已经做了详细地介绍,那么C++11模版元编程用来解决什么实际问题呢,在实际工程中又该如何应用呢?本文将侧重介绍C++11模板的一些具体应用,向读者展示模版元编程的具体应用。

  我们将展示如何通过C++11模版元来实现function_traits、Vairant类型和泛型bind绑定器。function_traits侧重于如何萃取可调用对象的一些元信息,Variant则是一种能接受多种类型数据的“万能”类型,bind则是一个泛化的绑定器,下面来看看这些具体的例子。

2.function_traits

  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<>::type>();//将输出int //打印函数的返回类型
  PrintType<function_traits<decltype(f)>::return_type>(); //将输出int   //打印函数指针类型
PrintType<function_traits<decltype(f)>::pointer>(); //将输出int (__cdecl*)(int)
}

  可以看到这个function_traits通过类型萃取,可以很方便地获取可调用对象(函数、函数指针、函数对象、std::function和lambda表达式)的一些元信息,功能非常强大,这个function_traits经常会用到是更高层模版元程序的基础。比如Variant类型的实现就要用到这个function_traits,下面来看看Variant的实现。

3.Variant

  借助上面的function_traits和前文实现的一些元函数,我们就能方便的实现一个“万能类型”—Variant,Variant实际上一个泛化的类型,这个Variant和boost.variant的用法类似,需要预定义一些类型作为可接受的类型。boost.variant的基本用法如下:

typedef variant<int,char, double> vt;
vt v = ;
v = 'a';
v = 12.32;

  这个variant可以接受已经定义的那些类型,看起来有点类似于c#和java中的object类型,实际上variant是擦除了类型,要获取它的实际类型的时候就稍显麻烦,需要通过boost.visitor来访问:

struct VariantVisitor : public boost::static_visitor<void>
{
void operator() (int a)
{
cout << "int" << endl;
} void operator() (short val)
{
cout << "short" << endl;
} void operator() (double val)
{
cout << "double" << endl;
} void operator() (std::string val)
{
cout << "string" << endl;
}
}; boost::variant<int,short,double,std::string> v = ;
boost::apply_visitor(visitor, v); //将输出int

  通过C++11模版元实现的Variant将改进值的获取,将获取实际值的方式改为内置的,即通过下面的方式来访问:

typedef Variant<int, double, string, int> cv;
cv v = ;
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

template<typename... Types>
class Variant{
enum{
data_size = IntegerMax<sizeof(Types)...>::value,
align_size = MaxAlign<Types...>::value
};
using data_t = typename std::aligned_storage<data_size, align_size>::type;
public:
template<int index>
using IndexType = typename At<index, Types...>::type; Variant(void) :m_typeIndex(typeid(void)){}
~Variant(){ Destroy(m_typeIndex, &m_data); } Variant(Variant<Types...>&& old) : m_typeIndex(old.m_typeIndex){
Move(old.m_typeIndex, &old.m_data, &m_data);
} Variant(const Variant<Types...>& old) : m_typeIndex(old.m_typeIndex){
Copy(old.m_typeIndex, &old.m_data, &m_data);
} 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));
} template<typename T>
bool Is() const{
return (m_typeIndex == type_index(typeid(T)));
} template<typename T>
typename std::decay<T>::type& Get(){
using U = typename std::decay<T>::type;
if (!Is<U>())
{
cout << typeid(U).name() << " is not defined. " << "current type is " <<
m_typeIndex.name() << endl;
throw std::bad_cast();
} return *(U*)(&m_data);
} template<typename F>
void Visit(F&& f){
using T = typename Function_Traits<F>::template arg<>::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<>::type;
if (Is<T>())
Visit(std::forward<F>(f));
else
Visit(std::forward<Rest>(rest)...);
}
private:
void Destroy(const type_index& index, void * buf){
std::initializer_list<int>{(Destroy0<Types>(index, buf), )...};
} template<typename T>
void Destroy0(const type_index& id, void* data){
if (id == type_index(typeid(T)))
reinterpret_cast<T*>(data)->~T();
} void Move(const type_index& old_t, void* old_v, void* new_v) {
std::initializer_list<int>{(Move0<Types>(old_t, old_v, new_v), )...};
} template<typename T>
void Move0(const type_index& old_t, void* old_v, void* new_v){
if (old_t == type_index(typeid(T)))
new (new_v)T(std::move(*reinterpret_cast<T*>(old_v)));
} void Copy(const type_index& old_t, void* old_v, void* new_v){
std::initializer_list<int>{(Copy0<Types>(old_t, old_v, new_v), )...};
} template<typename T>
void Copy0(const type_index& old_t, void* old_v, void* new_v){
if (old_t == type_index(typeid(T)))
new (new_v)T(*reinterpret_cast<const T*>(old_v));
}
private:
data_t m_data;
std::type_index m_typeIndex;//类型ID
};

  实现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), )...};
} 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<>::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<>::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中存储的对象的实际类型。

4.bind

  C++11中新增的std::bind是一个很灵活且功能强大的绑定器,std::bind用来将可调用对象与其参数进行绑定。绑定后的结果可以使用std::function进行保存,并延迟调用到任何我们需要的时候。下面是它的基本用法:

void output(int x, int y)
{
std::cout << x << " " << y << std::endl;
} int main(void)
{
std::bind(output, , )(); // 输出: 1 2
std::bind(output, std::placeholders::_1, )(); // 输出: 1 2
std::bind(output, , std::placeholders::_1)(); // 输出: 2 1
}

  std::placeholders::_1是一个占位符,代表这个位置将在函数调用时,被传入的第一个参数所替代。因为有了占位符的概念,std::bind的使用非常灵活,我们可以用它来替代任意位置的参数,延迟到后面再传入实际参数。下图是bind的一个原理图,更多的原理图读者可以参考:http://blog.think-async.com/2010/04/bind-illustrated.html

  从上图中可以看到bind把参数和占位符保存起来了,然后在后面调用的时候再按照顺序去替换占位符,最终实现延迟执行。

我们可以通过模板元来实现一个简单的bind,实现bind需要解决两个问题:

1.将tuple展开为可变模板参数

  bind绑定可调用对象时,需要将可调用对象的形参(可能含占位符)保存起来,保存到tuple中了。到了调用阶段,我们就要反过来将tuple展开为可变参数,因为这个可变参数才是可调用对象的形参,否则就无法实现调用了。这里我们会借助于一个整形序列来将tuple变为可变参数,在展开tuple的过程中我们还需要根据占位符来选择合适实参,即占位符要替换为调用实参。这里要用到前文中实现的MakeIndexes。

2.根据占位符来选择合适的实参

  这个地方比较关键,因为tuple中可能含有占位符,我们展开tuple时,如果发现某个元素类型为占位符,则从调用的实参生成的tuple中取出一个实参,用来作为变参的一个参数;当某个类型不为占位符时,则直接从绑定时生成的形参tuple中取出参数,用来作为变参的一个参数。最终tuple被展开为一个变参列表,这时,这个列表中没有占位符了,全是实参,就可以实现调用了。这里还有一个细节要注意,替换占位符的时候,如何从tuple中选择合适的参数呢,因为替换的时候要根据顺序来选择。这里是通过占位符的模板参数I来选择,因为占位符place_holder<I>的实例_1实际上place_holder<1>, 占位符实例_2实际上是palce_holder<2>,我们是可以根据占位符的模板参数来获取其顺序的。

  下面来看看bind实现的关键代码,完整的代码读者可以参考我github上的代码:https://github.com/qicosmos/cosmos/blob/master/Bind.hpp

template <int I>
struct Placeholder{}; Placeholder<> _1; Placeholder<> _2; Placeholder<> _3; Placeholder<> _4; Placeholder<>_5; Placeholder<> _6; Placeholder<> _7;Placeholder<> _8; Placeholder<> _9; Placeholder<> _10; template <typename T, class Tuple>
inline auto select(T&& val, Tuple&)->T&&{
return std::forward<T>(val);
} template <int I, class Tuple>
inline auto select(Placeholder<I>&, Tuple& tp) -> decltype(std::get<I - >(tp)){
return std::get<I - >(tp);
} template <typename R, typename F, typename... P>
inline typename std::enable_if<is_pointer_noref<F>::value, R>::type invoke(F&& f, P&&... par){
return (*std::forward<F>(f))(std::forward<P>(par)...);
} template<typename Fun, typename... Args>
struct Bind_t{
typedef typename decay<Fun>::type FunType;
typedef std::tuple<typename decay<Args>::type...> ArgType; typedef typename function_traits<FunType>::return_type result_type;
public:
template<class F, class... BArgs>
Bind_t(F& f, BArgs&... args) : m_func(f), m_args(args...){} template<typename F, typename... BArgs>
Bind_t(F&& f, BArgs&&... par) : m_func(std::move(f)), m_args(std::move(par)...){} template <typename... CArgs>
result_type operator()(CArgs&&... args){
return do_call(MakeIndexes<std::tuple_size<ArgType>::value>::type(),
std::forward_as_tuple(std::forward<CArgs>(args)...));
} template<typename ArgTuple, int... Indexes >
result_type do_call(IndexTuple< Indexes... >& in, ArgTuple& argtp){
return invoke<result_type>(m_func, select(std::get<Indexes>(m_args),argtp)...);
} private:
FunType m_func;
ArgType m_args;
}; template <typename F, typename... P>
inline Bind_t<F, P...> Bind(F&& f, P&&... par){
return Bind_t<F, P...>(std::forward<F>(f), std::forward<P>(par)...);
} template <typename F, typename... P>
inline Bind_t<F, P...> Bind(F& f, P&... par){
return Bind_t<F, P...>(f, par...);
}
测试代码:
void TestFun1(int a, int b, int c)
{
} void TestBind1()
{
Bind(&TestFun1, _1, _2, _3)(, , );
Bind(&TestFun1, , , _1)();
Bind(&TestFun1, _1, , )();
Bind(&TestFun1, , _1, )();
}

  由于只是展示bind实现的关键技术,很多的实现细节并没有处理,比如参数是否是引用、右值、cv符、绑定非静态的成员变量都还没处理,仅仅用来展示如何综合运用一些模版元技巧和元函数,并非是重复发明轮子,只是展示bind是如何实现, 实际项目中还是使用c++11的std::bind为好。

5总结

  可以看到C++11模板元编程能解决很复杂的问题,能实现一些几乎不可能完成的功能,比如实现了“万能”类型Variant,和泛化的绑定器bind,这些东西的实现如果不通过模版元的话,几乎是无法想象的。虽然这些应用看起来比较复杂,但是它将复杂性彻底的隐藏起来了,通过眼花缭乱的模版元技巧来解决复杂的问题,并最终给用户提供简单、强大和灵活的接口,这也是模版元编程最有魅力也最令人着迷的地方。模版元编程的应用很广,本文只是展示了一小部分的应用,如果读者还希望了解更多的应用,读还可以参考boost的mpl库和C++11的源码。

备注:本文是我发表在《程序员》2015.3月A,转载请注明出处。

C++11模版元编程的应用的更多相关文章

  1. C++11模版元编程

    1.概述 模版元编程(template metaprogram)是C++中最复杂也是威力最强大的编程范式,它是一种可以创建和操纵程序的程序.模版元编程完全不同于普通的运行期程序,它很独特,因为模版元程 ...

  2. C++11用于元编程的类别属性

    [C++11用于元编程的类别属性] 许多算法能作用在不同的数据类别; C++ 模板支持泛型,这使得代码能更紧凑和有用.然而,算法经常会需要目前作用的数据类别的信息.这种信息可以通过类别属性 (type ...

  3. C++ 模板元编程 学习笔记

    https://blog.csdn.net/K346K346/article/details/82748163 https://www.jianshu.com/p/b56d59f77d53 https ...

  4. 【48】认识template元编程

    1.TMP(template metaprogramming),模版元编程有两个效力:第一,它让某些事情更容易:第二,可将工作从运行期转移到编译期.

  5. 用 C++ 模板元编程实现有限的静态 introspection

    C++ 中的奇技淫巧大部分来源于模板技术,尤其是模版元编程技术(Template Meta-Programming, TMP).TMP 通过将一部分计算任务放在编译时完成,不仅提高了程序的性能,还能让 ...

  6. C++11新特性之六——元编程

    C++11新特性之六——元编程

  7. C++模板元编程(C++ template metaprogramming)

    实验平台:Win7,VS2013 Community,GCC 4.8.3(在线版) 所谓元编程就是编写直接生成或操纵程序的程序,C++ 模板给 C++ 语言提供了元编程的能力,模板使 C++ 编程变得 ...

  8. c++ 模板元编程的一点体会

    趁着国庆长假快速翻了一遍传说中的.大名鼎鼎的 modern c++ design,钛合金狗眼顿时不保,已深深被其中各种模板奇技淫巧伤了身...论语言方面的深度,我看过的 c++ 书里大概只有 insi ...

  9. C++ 元编程 —— 让编译器帮你写程序

    目录 1 C++ 中的元编程 1.1 什么是元编程 1.2 元编程在 C++ 中的位置 1.3 C++ 元编程的历史 2 元编程的语言支持 2.1 C++ 中的模板类型 2.2 C++ 中的模板参数 ...

随机推荐

  1. windows下端口映射(端口转发)

    windows下端口映射(端口转发) 转载: https://blog.csdn.net/i1j2k3/article/details/70228043 本文是对网文的归纳整理,算不上原创,摸索过程亲 ...

  2. canvas学习-----1px线条模糊问题

    canvas有时候会出现1像素的线条模糊不清且好像更宽的情况,如下图: 这样的线条显然不是我们想要的. 这篇文章的目的就是弄清楚里面的原理,以及解决它. 大家都知道屏幕上最小的显示尺寸就是1像素,虽然 ...

  3. django-访问控制

    django自带的用户认证系统提供了访问控制的的功能.   1.只允许登录的用户登录   django的用户可分为两类,一是可认证的用户,也就是在django.contrib.auth.models. ...

  4. Python中应用SQL及SQLAlchemy(一)

    以SQLit3为例: import sqlite3 conn = sqlite3.connect('db.sqlite3') #获取游标对象 cur = conn.cursor() #执行一系列SQL ...

  5. 安卓工作室 android studio 汉化后,报错。 设置界面打不开。Can't find resource for bundle java.util.PropertyResourceBundle, key emmet.bem.class.name.element.separator.label

    安卓工作室 android studio 汉化后,报错. 设置界面打不开. Android studio has been sinified and reported wrong.The setup ...

  6. CentOS+Nginx+PHP 前端部署

    都说Nginx比Apache性能优越,一直没有时间装测试,今天终于有时间装上试试性能了,其实Nginx的安装非常简单,具体流水步骤记录如下: 1.系统环境: ===================== ...

  7. Redis管道理解

    Redis管道理解 简介 管道并不是Redis本身提供的功能,通常是客户端提供的功能: 管道就是打包多条无关命令批量执行,以减少多个命令分别执行消耗的网络交互时间(TCP网络交互),可以显著提升Red ...

  8. C# 的枚Enum

    简短的解释: enum 关键字用来声明枚举,一种包含一组被称为枚举数列表的 enum myType{ a, b, c,} int num = 1;Console.Write((myType)num); ...

  9. 喵哈哈村的魔法考试 Round #21 (Div.2) 题解

    $ \sum{i=0}^{n-1}\sum{j=i}^{n-1}\mid Ai - Aj \mid $ 小学生在上课 题目大意:给你一个正整数N,问你1 ~ (n-1) 所有在模N下的逆的和(只计算存 ...

  10. 查看 Mac/Linux 某端口占用情况

    Mac/Linux 平台下,通用命令: lsof -i:8080  (8080 为 端口号,根据需要,替换为其他端口号) 可以查看该端口被什么程序占用,并显示 pid,方便 kill 掉 Linux如 ...