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。

(全文完)

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++ templates are a powerful feature of the C++ programming language that allow generic programming. Templates enable the creation of functions and classes that can work with different data types without the need for separate implementations for each data type. Templates are defined using the keyword "template" followed by a list of template parameters enclosed in angle brackets "< >". The template parameters can be either type parameters or non-type parameters, depending on whether they represent a data type or a value. For example, a type parameter might be used to specify the data type of a container class, while a non-type parameter might be used to specify the size of an array. Here is an example of a simple function template that returns the maximum of two values: ```c++ template<typename T> T max(T a, T b) { return a > b ? a : b; } ``` In this example, the "typename" keyword is used to indicate that T is a type parameter. The function can be used with any data type for which the ">" operator is defined. Templates can also be used to define class templates, which are similar to regular classes but can work with different data types. Here is an example of a simple class template for a stack: ```c++ template<typename T> class Stack { public: void push(T value); T pop(); private: std::vector<T> data_; }; template<typename T> void Stack<T>::push(T value) { data_.push_back(value); } template<typename T> T Stack<T>::pop() { T value = data_.back(); data_.pop_back(); return value; } ``` In this example, the class template is defined with a single type parameter T. The member functions push and pop are defined outside the class definition using the scope resolution operator "::". Templates are a powerful tool that can greatly simplify code and make it more reusable. However, they can also be complex and difficult to debug. It is important to use templates judiciously and to thoroughly test them with a variety of data types.

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值