C++11动态模板参数和type_traits

2023-03-05 14:31:54 浏览数 (2)

C 11标准里有动态模板参数已经是众所周知的事儿了。但是当时还有个主流编译器还不支持。 但是现在,主要的编译器。VC(Windows),GCC(Windows,Linux),Clang(Mac,IOS)都已经支持了。所以就可以准备用于生产环境了。 type_traits没啥好说的。主要是一些静态检测。主要还是要看动态模板参数和他们两的结合使用上。 动态模版参数标准文档见: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2242.pdf 和 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2555.pdf 虽然贴出来了。估计是没人看得。所以就直接说重点。

动态模板

还有一个更众所周知的。C里面的动态参数可以用…来表示。 比如: int printf(const char, …);* 动态参数可以用va_list,在运行时获取。

但是在C 编程里。提倡使用模板来简化处理相同类型的功能和把一些功能由运行期转到编译期(这也是C 比C效率高的原因)。但是使用模板有时候会碰到需要支持多个参数的情况。比如bind函数,tuple等。

遇到的问题

如果有兴趣的话可以看看VC11和目前的boost的bind或者tuple的实现。支持1到10个参数,还要对仿函数、成员函数、普通函数进行特化。再加上一些type_traits的支撑功能,你会看到很多很多类似的结构体和函数。唯一的区别只是参数个数不一样而已。这造成的结果就是很多很多的重复代码。维护起来工作量非常大而且易出错。

动态模板参数就是为了解决这个问题。并且有一点很重要的是,因为模板是编译期判定的,所以动态模板参数也必须在编译期可以判定出来

动态模板参数

最简单的比如这个形式:

代码语言:javascript复制
template<typename... T>
void print(T... t){
    printf("%d,%d,%dn", t...);
}

这个函数接受多个参数并传入到printf函数中。 当然这个输出要求t…至少是三个int类型。并不完美。我们可以把它写得更优雅一些。

代码语言:javascript复制
template<typename... T>
void print_real(const T&... t)
{
    print_unpack(t...);
}

template<>
void print_real()
{
    puts("end");
}

template<typename TM, typename... T>
void print_unpack(const TM& tm, const T&... t)
{
    std::cout << tm << std::endl;
    print_real<T...>(t...);
}
// print_real(3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 9);

这样,print_real函数就可以打印出任意个各种类型的数据。

语法支持

实际上,动态模板参数不仅仅是这个用法。按照标准文档说明。它至少能用于

  • 表达式
    • 解引用表达式
    • 批量自增和自减
    • sizeof表达式
    • sizeof…表达式(这个表达式返回的是动态模板的参数个数)
    • new和delete操作符
  • type declare(类型声明,比如上文例子中的 const T&…)
  • 类继承
  • 特殊成员函数(如构造函数)
  • 临时模板
  • 模板嵌套
  • typeid

其实支持的还比较有限。但是基于它已经可以实现出比较复杂的功能。 接下来我们来尝试用动态模板参数简单地实现boost和c 11里的tuple(多元组)。

实现简单多元组(tuple)

tuple是stl中pair的补充。目标是支持任意个参数数据的组合。我们可以用动态模板参数避免枚举参数个数的问题。

代码语言:javascript复制
template<typename... T>
struct TUPLE;

template<>
struct TUPLE<> {
};

template<typename TM, typename... TL>
struct TUPLE<TM, TL...>: TUPLE<TL...>  {
    TM v;
};

template<std::size_t INDEX, typename T, typename... TL>
struct GET_TYPE :  GET_TYPE<INDEX - 1, TL...> {
};

template<typename T, typename... TL>
struct GET_TYPE<0, T, TL...> {
    typedef TUPLE<T, TL...> tuple_type;
    typedef T value_type;
};

template<std::size_t INDEX, typename... TL>
typename GET_TYPE<INDEX, TL...>::value_type& 
    GET(TUPLE<TL...>& t) {
    typedef GET_TYPE<INDEX, TL...> conv_type;
    typedef typename conv_type::tuple_type tuple_type;
    typedef typename conv_type::value_type value_type;

    return ((tuple_type*) & t)->v;
}

这样,一个简化版的tuple就完成了。 这上面使用特化来分离和提取参数,通过继承来生成多个对象数据。 这套接口可以通过GET<0>([TUPLE]), GET([TUPLE]), GET([TUPLE])等等可以拿到对应位置的数据。 实际上,支持C 11动态模板参数的STL里的tuple也是这种实现方法,只不过额外还会有一些功能性函数和解决权限问题的函数而已。

如果研究一下stl里关于bind函数的实现,你会发现还有一个有意思的地方。

动态模版参数与std::bind

解释这个有意思的动态模板应用之前还要先了解下bind的实现原理(可以参见《std和boost的function与bind实现剖析》,已经知道的话就直接跳过吧)。 到了这里,各位知道bind函数有两个list,一个是绑定时构造,另一个是执行时构造。我们看一下绑定时参数列表的构造和保存。

代码语言:javascript复制
template<typename _Functor, typename... _Bound_args>
class _Bind<_Functor(_Bound_args...)>
: public _Weak_result_type<_Functor>
{
    typedef _Bind __self_type;
    typedef typename _Build_index_tuple<sizeof...(_Bound_args)>::__type
    _Bound_indexes;

    _Functor _M_f;
    tuple<_Bound_args...> _M_bound_args;

    // Call unqualified
    template<typename _Result, typename... _Args, std::size_t... _Indexes>
    _Result
    __call(tuple<_Args...>&& __args, _Index_tuple<_Indexes...>)
    {
      return _M_f(_Mu<_Bound_args>()
              (get<_Indexes>(_M_bound_args), __args)...);
    }

//...
};

以上代码摘自GCC 4.8.1,VC下也类似。

以上代码摘自GCC 4.8.1,VC下也类似。 正如这里面所看到的。bind的数据保存也用了tuple。但是这里有一个问题,执行时要把绑定时的list按顺序解引用。这怎么实现呢? 可以看到上面代码里的__call函数,有没有注意到第二个参数是一个**_Indexes…,而且上面有一个typedef typename _Build_index_tuple<sizeof…(_Bound_args)>::__type _Bound_indexes;?没错秘诀就在这里。由于代码篇幅过长,这里不再贴出代码。 它是怎么使_Indexes…**的值是从0到tuple的最大值的呢?我们bind函数传入参数的时候并没有传入数字一类的东西。这里该type_traits出场了。 我们把这其中的核心的部分提取一下。请看下面的代码:

代码语言:javascript复制
#include <cstdio>
#include <iostream>
#include <typeinfo>
#include <memory>
#include <functional>

template<int... _Index>
struct IndexArgsVarList{};

template<std::size_t N, int... _Index>
struct BuildArgsIndex:
    BuildArgsIndex<N - 1, _Index..., sizeof...(_Index)>
{
};

template<int... _Index>
struct BuildArgsIndex<0, _Index...>
{
    typedef IndexArgsVarList<_Index...> type;
};

template<int... _Index>
void print_real()
{
    print_unpack<_Index...>();
}

template<>
void print_real()
{
    puts("end");
}

template<int _My, int... _Index>
void print_unpack()
{
    printf("%dn", _My);
    print_real<_Index...>();
}

template<int... _Index>
void print(IndexArgsVarList<_Index...>)
{
    print_real<_Index...>();
}

int main() {
    print(BuildArgsIndex<10>::type());
    return 0;
}

这个会输出什么? 答案很简单就是 0到9然后一个end。 这和bind函数的index提取的原理是一致的,即:

  • 首先使用sizeof…操作符获取动态模板的参数个数
  • 然后利用继承使这个计数降低,并自定义一个动态类型,并且是个数累加
  • 之后同样使用sizeof…操作符获取到index值
  • 最后在解引用的时候使用_Index…,必然是由0到目标个数的一次累加

这时候,_Index就可以用到tuple的get函数里了。实现了我们需要的功能。

动态模板参数的缺陷

凡事有利必有弊。动态模板参数也不例外。 虽然他可以让我们减少很多的重复性的建设工作,但是首先最显而易见的一点就是:代码阅读难度更高了;其次,从上面的例子里很容易看出来,生成了很多临时的并不需要的类和函数。比如tuple有5个参数,那么4个子参数的tuple,3个子参数的tuple,一直到1个子参数的tuple都被生成了,而其实我们并不使用它。 这带来最直接的开销就是类型和函数的总量变大,编译速度降低,而且也给IDE的语法分析带来了一定的复杂度。另一个隐性的开销就是,常量表、符号表也会变大,结果就是二进制变大了。 不过在这个内存都不太在意的时代,代码导致的二进制变大的影响微乎其微。

不过这项功能也确实带来了很多设计上的简约和实现方法上的变革。 其实最重要的是:无论是什么工具或者功能和特性,只用在该用的地方,并且要用得好才是王道。

0 人点赞