C++11新特性复习与速记3

    这一节主要看一个比较晦涩的特性,变长模板

    C++11标准库中提供了一个tuple模板,通过它我们可以声明任意多个不同类型的元素的集合。例如

    std::tuple<int, char, std::string> collections;

    该collections变量可以容纳int, char, std::string三种类型的数据。当然我们也可以更为简单的利用C++11的make_tuple模板函数来创造一个tuple模板类型。这里学习一下tuple的实现方式。

1. 变长模板的语法,模板参数包和函数参数包

    template<typename ... Elements> class tuple;

    可以看到,我们在标识符Elements之前使用了省略号(...)来表示该参数是变长的。在C++11中,Elements被称为是一个“模板参数包(template parameter pack)”。

    一个模板参数包在模板推导时会被认为是模板的单个参数(虽然它可以打包任意数量的实参)。为了使用模板参数包,我们需要使用一个名为“包拓展”的表达式来完成解包。例如:

    template<typename T1, template T2> class B{};

    template<typename ... A> class TemplateClass : private B<A ... >{};

    这里我们为类模板TemplateClass声明了一个参数包A,而使用该参数包A...则是在TemplateClass的私有基类B<A...>中。当然,这里的疑问是B模板总是接受两个参数,倘若我们声明TemplateClass<X, Y, Z>必定会导致模板推导的错误。那么如何才能使模板能够接受任意多个的模板参数并均能实例化呢?

    实际上,在C++11中,实例化tuple模板的方式就给出了一种使用模板参数包的答案。其思路是使用数学归纳法,转化为计算机能够实现的手段则是递归。

    简化的tuple实现大致类似这样:

template<typename ... Elements> class tuple;

template<typename Head, typename ... Tail>
class tuple<Head, Tail...> : private tuple<Tail ...>{//递归的偏特化定义
    Head head;
};

template<> class tuple<> {};//边界条件

    设想我们实例化一个形如tuple<double, int, char>的类型时,会引起基类的递归构造,这样的递归直到tuple的参数包为0个的时候会结束。还原过程,编译期将从tuple<>出发,构造出tuple<char>,继而构造出tuple<int, char>,tuple<double, int, char>类型。

    当然了,变长的函数参数也可以声明为函数参数包(function parameter pack)。例如:

    template<typename ... T> void func(T ... args);

    C++11标准要求函数参数包必须唯一 且是函数的最后一个参数。可以看一下这个Printf的例子:

void Printf(const char* s) {
	while (*s) {
		if (*s == '%' && *++s != '%') {
			throw runtime_error("invalid format string: missing arguments");
		}
		cout << *s++;
	}
}

template<typename T, typename ...Args>
void Printf(const char *s, T value, Args...args) {
	while (*s) {
		if (*s == '%' && *++s != '%') {
			cout << value;
			return Printf(++s, args...);
		}
		cout << *s++;
	}
	throw runtime_error("extra arguments provided to Printf");
}

2. 进阶

    在上面TemplateClass的例子中,我们看见参数包在类的基类描述列表中进行了展开。而按照C++11的标准,参数包可以在下面7个位置进行展开:

表达式
初始化列表,使用{}进行列表初始化的地方
基类描述列表
类成员初始化列表
模板参数列表
通用属性列表
lambda函数的捕捉列表

    对于包拓展而言,其解包与其声明的形式息息相关。比如声明了Arg为参数包,那么我们可以使用Arg&& ... 这样的包拓展表达式,其解包后等价于Arg1&&, Arg2&&, ... , ArgN&&(设Arg1是参数包的第一个参数而ArgN是最后一个)。

    再看这个例子:

template<typename ... A> class T : privateB<A> ... {};

template<typename ... A> class T : private B<A ... > {};

    倘若我们实例化T<X, Y>,对于前者,会被解包为class T<X, Y> : private B<X>, private B<Y> {}; 而后者会被解包为class T<X, Y> : private B<X, Y>{}。

    类似的情况也会发生在函数声明上面。

template<typename ... T>
void DummyWrapper(T...t) {}

template<typename T>
T print(T t) {
	cout << t;
	return t;
}

template<typename ... T>
void Print(T...t) {
	cout << sizeof...(t) << endl;
	DummyWrapper(print(t)...);
}

    这个例子中DummyWrapper(print(t)...)的包拓展会被解包为print(Arg1),print(Arg2),... ,print(ArgN)。但如果我们测试Print("a", "b", "c"),我用g++编译得到的输出结果并非a, b, c,而是倒序的c, b, a。

    可以使用sizeof...操作符来计算参数包中的参数个数。

    最后是一个与完美转发结合的例子,Build函数看起来复杂,理解其原理还是很好读懂的。

struct A {
	A() {}
	A(const A& a) {
		cout << "A Copy Constructed " << __func__ << endl;
	}
	A(A && a) {
		cout << "A Move Constructed " << __func__ << endl;
	}

	void Func() {
		cout << "A" << endl;
	}
};

struct B {
	B() {}
	B(const B& b) {
		cout << "B Copy Constructed " << __func__ << endl;
	}
	B(B && b) {
		cout << "B Move Constructed " << __func__ << endl;
	}

	void Func() {
		cout << "B" << endl;
	}
};

template<typename ... T> struct MultyTypes;//声明
template<typename First, typename ...T>
struct MultyTypes<First, T...> : public MultyTypes<T...> {//递归偏特化
	First first;
	MultyTypes<First, T...>(First f, T...t) :
		first(f), MultyTypes<T...>(t...) {}
};

template<> struct MultyTypes<> {};//边界条件

template<template <typename ...> class VariadicType, typename ...Args>
VariadicType<Args...> Build(Args&& ... args) {//转发
	return VariadicType<Args...>(std::forward<Args>(args)...);
}

int main()
{
	A a;
	B b;

	Build<MultyTypes>(b, a);//在构造的时候不会调用任何的拷贝构造函数或者移动构造函数,\
							构造之后的类型只包含了对之前定义的变量a和b的引用

	system("pause");
	return 0;
}

猜你喜欢

转载自blog.csdn.net/saasanken/article/details/79974354