C++ template Metaprograming

前言

前几天我看了Cppcon,Walter E.Brown关于metaprograming的演讲,确实精彩。感觉在模板元编程入门的教程上,没有多少可以出其右。这篇博客主要是对该演讲的总结,同时也可以作为metaprograming的入门博客,破除大家对模板元编程的“恐惧心理”,正如Brown教授所说:人们害怕模板元编程,仅仅是因为人们不熟悉它。
什么是metaprograming ?

  • 利用编译期来进行编程的手段。

  • 模板元编程是元编程的一种。利用模板的实例化机制,来实现编译器计算。

模板元编程之作用:

  • 提高代码的灵活性。
  • 提高运行期的效率。

模板元编程即将运行期的工作移到编译期完成。这是需要代价的,即编译期时间增加。换来的就是运行期的效率。

notice:

  • 在模板元编程中,牢记:你的程序的run-time == compile-time,没有1)mutability ,2)虚函数,因为虚函数基于运行期,3)RTTI 等等一系列会发生在运行期的东西。

在模板元编程中:

  • 没有可变性,
  • 没有可变的,
  • 所有东西都不可变!!!(我认为这点是最重要的,所以单列出来)

很多人认为模板元编程很难,那是因为他们不熟悉。everything都是从不熟悉到熟悉。
在接下来的文章中,没有运行期。

  • no run-time
  • no run-time
  • no run-time(我必须让我(你)牢记这点 !!!)

template metafunction

template <int N>
	struct abs {
		static_assert(N != INT_MIN); //静态断言,C++17
		static int constexpr value = N > 0 ? N : -N; //利用初始化作为meta函数的返回值;
	};												 //这里没有赋值,赋值发生在运行期;只有初始化;
  • 这是一个最简单不过的模板类,但是它会像函数一样工作,所以我们称之为模板元函数。
  • 模板接收一个N,而这个N会被在编译器推导出来。
  • static_assert是C++17引入的静态断言,因为INT_MIN的绝对值越界了。
  • 这里我们的最后一句,定义了一个static的value变量并初始化。这实际上就是我们的返回值,用变量初始化来代替返回值。注意,这里没有赋值。赋值发生在运行期,而初始化发生在编译器。

模板元函数调用语法:

int constexpr n = 10;
abs<n>::value;
  • 在上面,abs是模板元函数名称,n是参数,value则是返回值。
  • so,它像函数一样工作。

既然它可以像函数一样工作,调用其他的模板元函数。那么,yes,它也应该可以调用自己,即递归。

	//gcd,著名的欧氏算法
	//既然模板元函数可以像函数一样工作,那么为什么不能调用自己呢?
	//利用递归实现模板元编程;
	template <unsigned M, unsigned N> 
	struct gcd {
		static unsigned constexpr value = gcd<N, M% N>::value; //利用递归计算gcd
	};			

	template <unsigned N> //正如普通的递归一样,需要一个初始条件。
	struct gcd<N, 0> {		//在模板元编程中,使用模板特化来实现。
		static_assert(N != 0);
		static unsigned constexpr value = N;
	};
  • 我们在主模板中接收两个模板参数,我们默认支持unsigned的non-type模板参数。然后在gcd这个类里面进行递归。这就好像你告诉编译器,我要的是value,至于怎么样得到value,方法我已经告诉你了,剩下的就是你的事情了,of course,编译器会做正确的事情。
  • 正如我们普通的函数递归一样,需要一个初始值。我们利用模板特化(partial specialization)来实现对初始值的处理。

C++引入了constexpr函数,那么与模板元编程有什么区别?

  • constexpr还是一个函数,只不过可能发生在编译器罢了,还是通过函数的方式调用。
  • but,模板元编程使用的是struct/class。这会给我们提供更多的工具,而且是public的。这在函数里是不可能的,因为函数内部不会公开。
    struct可以提供:
  • public的成员type声明。
  • public的成员变量声明。
  • public成员方法声明等等。
  • 你可以拥有关于模板和类的所有工具,而这些都是函数不具备的。

type traits

上面只是开胃小菜,接下来对模板元编程的使用,可能会让你大开眼界。

模板元函数使用类型参数

在普通函数中,无法将一种类型作为参数,没错,不是value,是type,将type作为parameter/argument。constexpr函数无法完成这件事。

  • but,模板元编程可以。
  • 实际上,一直以来都有一个运算符可以实现将type作为参数,就是sizeof。在sizeof(type),计算出type所占的字节数。
  • 现在我们可以自己编写类型参数。
//类型元函数,type traits,计算数组的维数
	template <class T> // 主模板处理特殊情况: 不是数组,那么没有维数
	struct rank {
		static int constexpr value = 0;
	};

	template <class U, size_t N>	//Yes,利用特化模板进行递归!!
	struct rank<U[N]> {				//类型模板元函数;
		static int constexpr value = 1 + rank<U>::value;
	};

	//调用
	rank<int [10][20][30]>::value; //Yes!! 参数是一种类型!!!
  • 当我第一次看到这个栗子,oh my god,还能这样!!
  • 该栗子用来计算数组的维数。
  • 我们使用主模板处理不是数组的情况,那么维数为0.
  • 如果是数组,那么将匹配特化情况,value等于1加上剩下的维数,编译器会帮助我们递归求解。
  • 栗子的数组维数为3。

这个栗子告诉我们两件事:

  • 模板元函数的参数可以是一种type。
  • 递归不必在主模板中,你可以在特化模板中递归。(important!!!)

使用类型作为 ”返回值“

既然模板元函数可以将type作为参数,自然的,我们会想:能否将type作为“返回值”?

//模板元编程真正优势的地方就是类型模板元函数,
	//使用类型作为模板元函数的“返回值”
	template <class T>
	struct remove_const {
		using type = T;
	};

	template <class U>
	struct remove_const<U const> { //给我一个U const,我返回给你U。not const U,去除最上层的const,即最右边的const;
		using type = U;				//这里没有mutable,只是返回非const,没有可变。
	};

	//简化
	template <class T>
	using remove_const_t = typename remove_const<T>::type;
	//调用
	remove_const<int const * const>::type; // ? 该类型为什么?
	remove_const_t<int const *const>;
  • okey,remove_const来自标准库头文件<type_traits>,作用是去掉顶部的const符号。注意,所谓的去除只是你给我一种含有顶部const的类型,我返回给你非const的类型,没有可变性。
  • 主模板接收任意类型,返回给你该类型T。
  • 在特化中,如果该类型含有顶部const,那么我只返回给你T。
  • 注意,这是U const,而非const U。顶部const指的是最右边的const。考虑调用,这个type的类型为int const*,即返回的类型没有右边的const。
  • 我还多写了一个关于别名的模板,_t,这样可以使用起来更加简单,不用带上::type,但是这不是免费的。后边还会再提到这一点。

C++库模板元函数约定1

如果你的模板元函数中有一个返回值别名是类型,那么它的名字是type。

  • but,人们有许多惯例,但你最好还是遵守这个规定,因为这样才好融入模板元编程这个集体。

example:

//type_is or Identity
	template <class T>
	struct type_is {
		using type = T;  //名字是type,因为是惯例
	};

	template <class T>
	using type_is_t = typename type_is<T>::type;
  • type_is这个模板元函数,very easy。
  • 返回你给我的类型,你给我什么类型,我就给你什么类型。
  • type_is不是标准库的成员,实际上某些标准库有他们自己的名字,比如VS下叫做_Identity,前面带有横线表面这是用于库内部,不会open给用户。
  • 虽然这个常值模板元函数是如此简单,但是却如此的有用,yes,简单的事物往往这样。

also,我们可以使用type_is帮助我们遵守规则1,通过继承。

//使用继承 重写remove_volatile
	template <class T>
	struct remove_volatile : type_is<T> {};

	template <class T>
	struct remove_volatile<T volatile> : type_is<T> {}; //是 T volatile,not volatile T
  • remove_volatile也来自<type_traits>,它的实现和remove_const一样,只不过将const替换成volatile。但是这次我们采用新的写法,继承。
  • 在主模板中,对于任意类型T,我们继承type_is,于是我们就有了一个成员type = T。
  • 在特化模板中,如果顶部带有volatile,即T volatile,那么我们就继承type_is。
  • amazing!

编译期的“if”

想象一下,如果你要执行某些操作,而这些操作需要在编译期做出像if一样的选择。那么如何实现?

int constexpr p = - 
  • 假设有一个p,接下来的选择需要依赖p的正负。
  • p的正负将导致两种不同的选择。他们可能是:
class T class F
function call: F, G
Base class: A, B

-选择不同的类型,选择不同的函数调用,选择不同的基类去继承。
在标准库中有实现,名字叫做conditional

//给我一个条件,如果为真,选择类型1; 如果为假,选择类型2;
	//两个内置类型; 两个函数对象 or 继承两个base类
	template <bool, class T, class>
	struct conditional : type_is<T> {};

	template <class T, class F>
	struct conditional<false, T, F> : type_is<F> {};
  • 给我一个bool类型的contant,给我两种类型。
  • 主模板默认返回T。
  • 而特化模板,若bool常量为false,那么返回F。

编译期选择

_t

在C++14之后,如果模板元函数拥有一个type的返回值,那么你可以使用_t来简便的调用它。
像conditional_t。

一种关于conditional的变种

这个栗子,让你大吃一惊。
想象一下,在普通函数中,如果返回值是int,那么我们必须返回一个int(不考虑发生错误or其他特殊情况)。我们无法拒绝返回一个值。but,模板元函数可以。

//拒绝一个返回结果 && SFINAE
	template <bool ,class T = void>
	struct enable_if : type_is<T> {};

	template <class T>    //else, I give your nothing !
	struct enable_if<false, T>{};
  • 主模板接收一个bool变量和一种类型,然后我将该类型返回给你。T的缺省参数为void,这不是必要的,但实践证明这很有用。
  • 在特化中,如果bool变量是false,那么我什么也不给你!!!
  • 这可能会让人不禁疑惑,这能干什么?
enable_if_t<false, ...>  //this will failure
  • 上面的调用由于false,所以enable_if没有一个type的返回值,所以会失败,但是这并不是错误。
  • so,让我们欢迎SFINAE。

SFINAE

SFINAE,全程Substitution failure is not an error,替换失败并不是一个错误。

-okey,让我们考虑模板的实例化。编译期在碰到要实例化的对象时,会先构造一个集合,里面是所有等待匹配的模板。然后编译器选择一个模板,尝试用实参替换模板参数,当它替换失败,编译器会说,o,这是错误的,但是编译期会吞掉这个错误,继续去寻找下一个模板。
and,这个替换错误的模板被默默丢弃,它的实例化从来没有出现过。
so,让我们来使用SFINAE。

  • 我们知道,函数重载与返回值无关,but,让我们利用SFINAE。
  • 假设我们有一个函数F,它返回一个整数,or一个浮点数,这是F的两个不同模板。
template <class T>
enable_if_t<is_integral<T>::value, int> F(T val){...}

template <class U>
enable_if_t<is_floating_point<U>::value, float> F(U val){...}

F(1);
  • okey,当我们使用该调用时,如果匹配下面那个,但是enable_if会匹配到特化,所以会返回nothing。所以,编译器会呕吐不止,它只能匹配第一个。
  • amazing !!!
  • C++20支持concept,我们可以这样写
template <integral T>
int F(T val){...}
  • 不用enable_if,编译器会帮助我们进行SFINAE。

C++库模板元编程约定2 && integral_constant

如果你的模板元函数有一个关于值的返回,那么它的名字是value。

  • 类似type_is,在C++标准中,我们还有一个属于模板元函数的工具:
template <class _Ty, _Ty _Val>
struct integral_constant {
    static constexpr _Ty value = _Val;

    using value_type = _Ty;
    using type       = integral_constant;

    constexpr operator value_type() const noexcept {
        return value;
    }

    _NODISCARD constexpr value_type operator()() const noexcept {
        return value;
    }
};
  • 这个模板元函数,会提供一个关于_Ty类型的常量。而你为了遵守约定2,最好还是继承该struct。
  • 这个模板元函数拥有一个value,即_Val,一个type,还有一个转换函数,隐式的将_Val转换成integral_constant类型。
  • 有一个operator(),支持像函数一样调用。

接下来,我们使用integral_constant来重构rank模板元函数

template <class T>
struct rank : integral_constant<int, 0> {};

template <class U, int N>
struct rank<U[N]> : integral_constant<int, 1 + rank<U>::value> {};

template <class U>
struct rank<U[]> : integral_constant<int, 1 + rank<U>::value> {};
  • 在主模板中,如果是非数组,我们继承integral_constant,即继承1个int 0的value。
  • 在第一种特化,我们同样继承integral_constant,然后递归。
  • and,有一种特殊的无界数组,我们同样如此。

一些关于integral_constant的简单别名

template<bool Test>
using bool_constant = integral_constant<bool, Test>;

using true_type = bool_constant<true>;
using false_type = bool_constant<false>;

关于模板元函数的调用语法

调用语法

  • 我们有最经典的::value调用。
  • 在integral_constant中,加入了转换函数,operator T,所以我们可以用大括号调用,先实例化一个对象,然后隐式转换成bool类型的变量。
  • 我们还有operator(),直接返回bool变量。
  • 最后,在C++17中,我们可以使用_v(像_t)来表示对value的返回。

is_void

判断一种类型是否为void,注意,void const,void volatile, const volatile void 也是void的一种。

template <class T>
struct is_void : false_type {};

template <class T>
struct is_void<void> : true_type {};

template <class T>
struct is_void<void const> : true_type {};

template <class T>
struct is_void<void volatile> : true_type {};

template <class T>
struct is_void<void const volatile> : true_type {};

  • 这里我们的主模板处理非void的情况,那么继承false_type。
  • 然后分别对四种void特化。
  • 我们注意到,如果特殊的void过多,我们就得每个都特化。还好是4种,但是如果是is_integral呢?
  • 我们有int,unsigned,long,long long,和const,volatile一组合,就会很多。
  • 我们需要一种抽象层。

利用is_same && remove_cv 来实现is_void:
is_same用来判断两种类型是否相同。

template <class T, class U>
struct is_same : false_type {};

template <class T>
struct is_same<T, T> : true_type {}; 

这里的相同是指完全的相同。

  • 所以,第二个版本的is_void来了:
template <class T>
using is_void = is_same<remove_cv_t<T>, void>;

模板元编程 && variable template

  • 假设有这样一个模板元函数,给你一种类型T,和一串任意长度的类型序列,可以为0个,可以为n个。判断T是否在这一串中。
  • 我们使用变参模板来编写。
  • 变参模板有一个非常经典的编写模式,使用变参模板大都依据这个模式来编写:
template <class T, class ... R>
	struct is_one_of; //声明

	template <class T>  // 如果列表为空,则返回false
	struct is_one_of<T> : std::false_type {};

	template <class T, class ... Rest> //如果列表第一种类型是T,返回true
	struct is_one_of<T, T, Rest...> : std::true_type {}; //T,T,Rest...三个,而T,...R是两个

	template <class T1, class T2, class ... Rest> //else,递归
	struct is_one_of<T1, T2, Rest...> : is_one_of<T1, Rest...> {};
  • 在C++标准中,也是这样编写的。声明+特化。
  • 在这个栗子中,也许会打破你的认知。Yes,一个模板可以没有主模板的定义!!
  • and,在变参模板的模板特化中,你的类名后面的特化列表,可以从数量上与主模板的模板参数列表不同,因为变参模板代表若干个数量。
  • so,我们有第三个版本的is_void.
template <class T>
   using is_void = is_one_of<T,
					         void, 
							 void const,
							 void volatile,
							 void const volatile>;

done.

  • 关于变参模板,我以后应该会写一篇博客专门讲它。变参模板也很amazing。

Unevaluated Operands

使用sizeof,typeid,decltype 和 noexcept不会真正的计算,而是评估一个你给它的东西。这意味着:

  • 命名这些东西不会产生新的代码,使用他们只会增加编译期的负担,在运行期没有负担。
  • 你可以仅仅拥有声明,不需要定义。
  • decltype用于推导一种类型,但是不会去真正的计算这种类型。
struct fool{
	int test(); //only 声明
};
decltype(fool().test()) a;
  • 这好像在说,哦,编译器,你来帮我看看fool().test()的返回值是什么,这似乎发生了一次对象构造,然后去调用函数,但是,编译器不会去真正的调用,它仅仅是观察它,然后将返回值给你。yes。
  • But,如果fool没有默认的构造函数,上述语句就会出错。

std::declval

-我们需要declval帮助我们。declval是一个模板函数,只有声明,没有实现。它会返回类型T的右值引用。

  • 调用declval好像在说,你给我一种类型,我返回一个关于这种类型的右值引用。好像返回了一个实例化对象,but没有真正的构造。
struct foo{
	fool() = delete;
	int test();
}

decltype(fool().test()); //wrong !
decltype(std::declval<foo>().test()); //right !
  • 你也许会惊奇,why?编译器会去查看declval函数,然后发现它返回foo&&,然后,yes,编译器会假装返回了一个对象,然后去调用test函数。

  • declval可以代替std:result_of。

  • declval没有定义,只有声明。你唯一可以用到它的地方就是求unevaluated的上下文中。

  • okey,让我们回到decltype。拥有求未定值的操作符,我们可以实现许多令人惊奇的事情,yes,在编译期间。

template <class T>
	struct is_copy_assignable {
	private:
		template <class U, class = decltype(std::declval<U&>() = std::declval<U const&>())>
		static true_type _try_assignment(U&&);

		static false_type _try_assignment(...);
	public:
		using type = decltype(_try_assignment(std::declval<T>()));
	};
  • 这个模板元函数用来判断一个类是否具有operator=运算符。
  • 要么返回true_type,要么返回false_type。
  • _try_assigment第一个版本是模板成员,第一个参数是U。而第二个参数假装调用operator=函数,将U const&赋值给U&,但是,如果U没有operator=运算符,那么编译器会呕吐。不用担心,我们还有第二个版本的_try_assignment,使用…,接收任意参数。
  • so,我们的type假装调用_try_assignment,并假装构造一个T的实例化传给它,如果它有operator=,那么将会匹配第一个,因为它是最佳匹配。如果没有operator=,那么由于SFINAE,编译器会吞下错误,选择第二个匹配。
  • 这很好了,但是还不够完善,因为我们还期望返回值是T&。

Before C++11:

  • 我们没有decltype,我们只有sizeof,so,我们往往typedef出不同sizes的类型,然后使用sizeof来实现。

void_t

接下来,是最让人震惊的实现,void_t。void_t现在进入了C++17的标准,它的实现如下:

class <class ...>
using void_t = void;
  • 无论你给我什么,我都返回给你void。这也太简单了,确实,就是这么简单。
  • 那么,有什么用呢?or,在void的外边覆盖上一层,有什么用呢?
  • 事实上,我们想要的是该模板元函数返回给我们确定的东西,无论你传入什么,我都会返回给你确定的,可预测的东西,即void。but,传入的type必须是表现良好的!!!
  • 不一定是void_t,也可以是int_t,or别的东西,我只需要它给我的东西是确定的,可预测的。

我们想知道T里面是否有成员type,让我们使用void_t来完成。

template <class T, class = void> //void is 必要的!!
struct has_member_type : false_type {};

template <class U>
struct has_member_type<U, void_t<typename U::type>> : true_type {};
  • perfect !
  • 主模板默认继承false_type。
  • 如果一种类型含有成员type,那么将会匹配特化版本,返回true_type.
  • 如果没有成员type,那么如果匹配特化版本,由于U没有type成员,编译器会抱怨,由于SFINAE,编译器匹配主模板。
  • 请注意,主模板的缺省void必须是void,不能变成其他的type。(*)

重写is_copy_assignable

so,让我们利用void_t来重写。

template <class T>
using _try_assignment_t = decltype(declval<T&>() = declval<T const&>());

template <class T, class = void>
struct is_copy_assignable : false_type {];

template <class U>
struct is_copy_assignable<U, void_t<_try_assignment_t<T>>> :
 is_same<_try_assignment_t<T>, T&> {};
  • wow!
  • 主模板还是默认false。
  • 在模板特化中,如果类型U有operator=函数,那么就会选择特化版本。且,我们也规定了最好的返回类型,就是T&,如果operator=的返回类型是T&,true_type会被返回。or,false_type会被返回。
  • 你也许还会注意到,这个和上面的has_member_type相似,请注意,这不是巧合。
  • 如果你想测试一种类型是否具有移动赋值运算符怎么办?
  • Just修改名字 && 将declval<T const&>()修改成 declval<T&&>()。 done。
  • 如果你想测试你的类型是否拥有某一项功能,只需要利用decltype假装你有这项功能,然后将这个表达式放入using别名句子中,然后就会得到结果。
  • 我只能说,震惊!!

总结

懒得写了,偷懒截张图。
总结

  • 模板元编程已经演变成了一种编程的方式,like面向对象,尝试去了解它对模板的运用与C++的理解都很very good。

(全文完)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值