C++ Template (二):初步元编程

前言

  在上一篇博客C++ Template (一):模板基础中,简单介绍了模板的定义,实例化,特化以及参数包的使用,在一些简单的场景中,已经可以通过这些知识去大展手脚了。但是想真正发挥Template的威力,还有很长的路要走。在本篇博文中会介绍Template为C++添加的平行宇宙 模板元编程 Template metapromming (后文简称TMP)。首先要说明TMP是图灵完备的,这也就是为什么说TMP是C++的平行宇宙,根据TMP的特点,Template成功的在命令式编程语言(CPP)中添加了一个门函数式编程语言。Template的威力也在TMP中得到了极致的展现。限于博主能力,在本篇博文中只能初步涉猎TMP冰山一角。

一、元编程

  元编程是英文metaprogramming,是指 "a program about a program" , 我一般理解成“可以操作,产生程序的程序”,Template一开始的引入并不是为元编程准备的,而是为C++提供一种泛型的机制,提高代码复用的能力,而只是恰好发现其具有元编程的能力,再随后C++迭代中,C++标准委员也有心在这方面添砖加瓦,使得TMP在C++新的特性引入后更加灵活和具有威力。同时TMP的一切过程都是发生在编译器的,对于运行期的代码,TMP是不可变的,通过这一特性,可以将运行期的运行代价转嫁到编译期的过程中,从而提高程序的执行效率,当然这样做也是有得有失的,需要权衡利弊。

二、基本概念

  • <h5>1. 元数据</h5>
      元编程操作的数据称“元数据”,同时也是C++在编译期可以操作数据,相比于普通C++程序,TMP 不仅具有操作数据的能力,还具有操作数据类型的能力,换句话说,不论是数据本身还是其型别都是TMP中的一种数据 ,也就是“元数据”。
      元数据可分为整数元数据,值型元数据(int, double等POD值类型),函数元数据,类元数据(class, struct等用户自定义的数据),在使用的时候我们可以通过如下的形式去声明元数据。

  • <h6>1.1  enum, static 定义非型别类型的元数据<h6>

template<int N, int M>
struct add
{
    static int value = N + M;
    // enum {value = N + M};
}
    
  • <h6>1.2  typedef,using 定义型别类型的元数据</h6>
template<typename T>
struct identity
{
    using type = T;
    // typedef T type;
}
  • <h5>2. 元函数<h5>
      元函数是元编程中处理元数据的构件,虽然称呼为元函数,但其实主要表现的形式以类的形式(struct, class)出现的,在编译期中可以像运行期的函数一样被调用。
如代码段 1: 
先定义了元函数remove_const 和 remove_volatile,分别实现了移除const和volatile的功能,
随后在remove_cv组合调用了remove_const和remove_volatile去实现了同时去除const和volatile
的功能,在这一过程中,元函数像运行期的函数一样,可以在编译期被随意的组合调用,只不过元函
数的调用,是通过域运算符体现的。元函数中的每个元数据都可以被作为元函数的返回结果被其他元
函数调用。
namespace iwtbam {
    template<typename T>
    struct remove_const
    {
       using type = T;
    };
    
   template<typename T>
   struct remove_const<const T>
   {
       using type = T;
   };
   
   template<typename T>
   struct remove_volatile
   {
       using type = T;
   };
   
   template<typename T>
   struct remove_volatile<volatile>
   {
       using type = T;
   };
   
   template<typename T>
   struct remove_cv
   {
       using type = typename remove_const<
                               typename remove_volatile<T>::type>::type;
   };
}

<center><h6>代码段:1</h6></center>
  在C++11之后,<strong>using constexpr</strong> 关键字的也带来其他形式的元函数

constexpr 指定符声明可以在编译时求得函数或变量的值。constexpr修饰函数要求函数无副作用
 constexpr int inc(int val)
 {
     return val + 10;
 }

 template<int N>
 constexpr int add = inc(N);

 template<typename T>
 using identity_t = T;
  • <h5>3. 元函数转发</h5>
      元函数转发是指<strong>TMP</strong>通过public 继承的方式,将模板参数传递给父类完成元函数的“调用”,获得父类元函数中元数据的方法。
代码段:2
通过元函数转发,改写代码段1
 template<typename T>
 struct identity
 {
     using type = T;
 };

 template<typename T>
 struct remove_const:identity<T>{};
 
 template<typename T>
 struct remove_const<const T>:identity<T>{};

 template<typename T>
 struct remove_volatile:identity<T>{};

 template<typename T>
 struct remove_volatile<volatile T>:identity<T>{};

 template<typename T>
 struct remove_cv:
     remove_const<typename remove_volatile<T>::type>{};

<h4>三、TMP中的控制流程</h4>
  刚接触一门语言的,了解其中的控制流程是很关键的,由于<strong>TMP</strong>的控制流程本质上都可以通过模板的特化和元函数转发来实现。

  • <h5>3.1 顺序</h5>
      像代码段1和代码段2,把自己的逻辑顺序的堆砌起来即可。
  • <h5>3.2 分支</h5>
  • <h6>3.2.1 特化</h6>
      通过模板的特化去实现不同的分支达到运行期switch, if...else...的效果
   利用特化的效果,我门在编译期的分别实现了类似运行期的 switch 和 if 效果的元函数  
   static_switch,static_if, 这里面有个小技巧,在TMP中经常会使用不同类的去做为
   tag,比如代码段3中的branch_1, branch_2, branch_3.
namespace iwtbam {

   
   struct branch_1;
   struct branch_2;
   struct branch_3;

   template<typename T>
   struct identity
   {
       using type = T;
   };
   
   template<int N>
   struct int_
   {
       static const int value = N;
   };

   template<typename T>
   struct static_switch:int_<0>{};

   template<>
   struct static_switch<branch_1>:int_<1>{};

   template<>
   struct static_switch<branch_2>:int_<2>{};

   template<>
   struct static_switch<branch_3>:int_<3>{};

   template<bool value, typename T1, typename T2>
   struct static_if:identity<T1>{};

   template<typename T1, typename T2>
   struct static_if<false, T1, T2>:identity<T2>{};

   template<bool value, typename T1, typename T2>
   using static_if_t = typename static_if<value, T1, T2>::type; 
}

<h6><center>代码段:3</center></h6>
  C++ 中的type_traits中也为我们提供了分支流程的元函数比如std::conditional等,但像这样的元函数的本质上还是模板的特化的,其实<strong>TMP</strong>根本的技巧在博主看来说是模板的特化也不为过。

  • <h6>3.2.2 SFINAE</h6>
      SFINAE 是一个很意思的东西,总感觉放到一个小标题委屈了。SFINAE即Substitution Failure Is Not An Error, 译为匹配失败并非错。在模板匹配的过程编译器会选择匹配成功的进行实例化,其中匹配失败的特化版本不进行报错。通过这个特性来实现分支选择。
代码段:4
在这段代码中使用type_traits中的enable_if_t元函数构造了针对整数类型和非整数类型的两个fun的
函数版本。这两版本的相会对立,一个匹配成功,一个便会失败,但编译器不会为匹配失败的版本报告错
误。
#include <iostream>
#include <type_traits>

using namespace std;

template<typename T>
enable_if_t<is_integral<T>::value, T> fun(T v)
{
    return v + 1;
}

template<typename T>
enable_if_t<!is_integral<T>::value, T> fun(T v)
{
    return v * 2;
}

int main()
{
    cout << fun(10) << endl;
    cout << fun(12.0) << endl;
    return 0;
}
<h6><center>代码段:4</center><h6>

  这让我想起来我原先的一个demo中,构建了一个类的继承的体系,在这个体系中,每个类都有一个create函数,负责该类实例创建的工作,并且为了兼容不在这个体系的类,我定义一个Alloc的类,为所有类的实例创建的提供统一的接口,在Alloc类中去利用一个has_create元函数去检测该类是否具有create函数,为其选择不同的创建方式。

代码段:5中
用来检测是否具有create函数的元函数has_create,便是利用SFINAE的特性来完成的(SFINAE真的是)
一个功能强大的的特性
template<typename T>
class has_create
{
private:

    template<typename U>
    static auto check(int)-> decltype(U::create());

    template<typename U>
    static char check(...);

public:
    using value_type = bool;
    constexpr static bool value = std::is_same<T*, decltype(check<T>(0))>::value;
};

template<typename T, bool val = has_create<T>::value>
struct Alloc
{
    template<typename... Args>
    static T* create(Args... args)
    {
        return T::create(args...);
    }
};

template<typename T>
struct Alloc<T, false>
{
    template<typename... Args>
    static T* create(Args... args)
    {
        return new T{args...};
    }
};

<h6><center>代码段:5</center><h6>

  • <h6>3.2.3 if constexpr</h6>
      <strong>if constexpr</strong>是C++17中新添加的特性,可以完成编译器分支的工作,并且它相比于模板的特化具有一个显著的优点,减少了模板实例的个数。
在代码段:6中
利用if constexpr 实现的分支更符合运行期的的样子。
#include <iostream>
#include <type_traits>
using namespace std;

template<typename T>
auto fun(T v)
{
    if constexpr(is_integral<T>::value)
        return v + 1;
    else
        return v * 2;
}

int main()
{
    cout << fun(2) << endl;
    cout << fun(2.0) << endl;
    return 0;
}

<h6><center>代码段:6</center></h6>

  • <h5>3.3 循环</h5>
      TMP的循环通过一种“递归”的形式去实现,然后通过特化的版本为循环添加终止条件。
代码段:7中
实现一个编译期求和的元函数,其中的循环就是通过递归的形式去实现的。
#include <iostream>
using namespace std;

template<int N>
constexpr size_t sum = N + sum<N-1>;

template<>
constexpr size_t sum<1> = 1;

int main()
{
    cout << sum<7> << endl;
    cout << sum<9> << endl;
    cout << sum<5> << endl;
    return 0;
}

<center><h6>代码段:7</h6></center>

<h4>四,注意实例化的数量</h4>
   <strong>C++ Template</strong>会为每个被调用的模板函数,模板类提供一个实例以方便复用的问题,但是对于<strong>TMP</strong>,有时候会造成一定的困扰,实例化的数量太多,导致编译时间过长的问题,甚至编译失败的问题。

为了说明这个问题,先观察下代码段:7中的符号表。
在代码段:7中 只是计算一个求和的元函数就产生了9个实例,只产生9个的原因还在部分实例可以
复用。
在这里插入图片描述

<center><h6>图片1:代码段7符号表</h6></center>

在代码段8:中
对于 warp<5>::value<5>,会产生warp<5>::imp的一系列实例,再调用warp<4>::value<5>,
确不能复用之前的代码, 虽然都是imp的函数,第二次的却是属于warp<4>嵌套的imp,所以又
会产生一系列的实例。对于这种情况堆积,就很容易导致编译时间过长,甚至会失败的可能。
#include <iostream>

using namespace std;

template<int N>
struct warp
{
    template<int M, typename TDummy = void>
    struct imp
    {
        constexpr static size_t value =  M + imp<M-1>::value;
    };

    template<typename TDummy>
    struct imp<1, TDummy>
    {
        constexpr static size_t value = 1;
    };

    template<int M>
    constexpr static int value = imp<M>::value;
};

int main()
{
   auto w = warp<5>::value<5>;
   auto w2 = warp<4>::value<5>;
   return 0;
}

<center><h6>代码段:8</h6></center>

  所以再编写<strong>TMP</strong>过程中,应该注意这种模板的嵌套,和利用 <strong> 或(||),与 (&&)短路求值的特点</strong>去减少实例化的个数。在介绍<strong>TMP</strong>算法的书籍中,一般会将实例化的个数作为<strong>TMP</strong>算法复杂度的衡量标准。

  • <h4>后记<h4>
      很仓储的收尾,感觉很难表达出自己的想表达的东西,如果以后有时间再重新排版和把剩余想写的内容添加上来把。很想写出来一点关于<strong>TMP</strong>的自己的东西,但是很遗憾写的却像是搬运,博主写的主要是<strong>《C++11/14高级编程:Boost 程序库探秘》</strong>和 <strong>《C++模板元编程实战》</strong>书开始介绍的元编程的一部分。第四部分很想通过<strong>符号表</strong>更直观的展示,耦合度高的模板嵌套所带来实例个数的暴增,由于自身对于符号表的不了解,也变成了失败的尝试, 感觉还是太欠缺内功, 无法很好的表达出自己的想法。如果有想了解<strong>模板</strong>和其应用的, 很推荐去看看一下<strong>《STL源码解析》,《C++11/14高级编程:Boost 程序库探秘》,《C++设计新思想》等丛书</strong>,如果发现对<strong>TMP</strong>很有兴趣可以看看<strong>《C++ template metaprogramming》</strong>和<strong>Boost</strong> 中<strong>Spirit</strong>等组件的源码。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354