对类模板特化和函数模板重载的一点理解

模板个人理解是对类型的编程,解决类型的转换、推导和生成等问题,以类型为前提做功能型实现。对类型编程与普通编程中间也存在一定的类比性。普通编程中通常有变量、表达式、控制解构和函数。这些在对类型编程(即模板)中都有,using定义类型名称,类型的组合构造即为类型表达时,控制结构-条件对应特化或重载、循环通过递归等。只不过这里的输入是类型,输入点在实例化阶段,调用在编译阶段,还要编译器的筛选处理,思维转换确实比较难。想任何东西都是这样,总要有个感性的认识,类比转换到自己熟悉的东西,然后扩展新的内容。有时候并不是别人讲的不对或不好,而是我们没有站在他问题讲述的角度或认知的高度。自己也是半拉子,但总要思考,思考多了说不定就顿悟了。谈点自己的理解:
1. 模板类和模板函数的差别:
a. 模板类以类型列表做相关函数及成员的定义,具备定义的成员函数、内部类“访问、复用”类型实参的能力;可进行特化、偏特化定义;
b. 模板函数仅能对输入的类型实参在当前函数题内使用,支持重载,但不支持特化。
c. 函数模板的模板的形参能够通过函数实参进行推导,而类模板形参不能够推导,C++ 17中引入了类模板形参的推导机制,但目前使用实例偏少。

2. 特化或偏特化理论及实践:
A. 类模板特化的推导
个人理解类模板特化中类名称后类型表述通常是组合类型,该组合类型可以是:
a. 基于模板形参扩展,如T&,T等;
b. 多种输入形参的组合,如C::
,Ret(Args…)等;
c. 基于类模板新类型,该类模板通常只起中间辅助作用,于特化版本的实现无太大作用,如TypeList< Head, Tail…>;
d. 特定类型,如int,std::string等;
其实不管是上述何种类型,都是对于通用模板形参中满足某种特征的类型表述,如指针类型特征,函数成员类型特征等。特化版本的实现也可以直观的表述为针对满足上述特征的类型处理。而类型特征的甄别、输入,则需要借助编译器类型推导的力量。所以说特化本身就是条件化筛选,而条件就是类型特征。转换到操作上则是,从目的出发定义通用模板,定义通用模板的职责,而后从通用模板的职责(或功能)定义,确定在通用职责可以分几类或只针对一类解决,是指针?是常量?还是其他,然后构造特化版本的组合类型。以筛选成功前提,补充特化版本的实现。若在实现上 有走不通的地方,则组合类型构造有问题,需要考虑优化。通过反复的推敲,最终得到最优的实现。
模板使用上,可能由于思维的惯性,刚开始解决问题无意识地想要一个模板解决问题,直捣黄龙。但往往受阻,此时应该跳脱出来,将问题分解转化,找到模板技术的衔接点。如我已知某个成员函数类型,想要推导出其所属的类是哪一个,转换为特性就是类成员函数的特征,Ret C::(Args…),由此就可以得到类型所属类C。可以多找些应用实例,按照上述思路推导其目的、通用模板的定义、针对型解决的问题、特征定义的技巧,不断的体会、总结。补充两个示例,供参考学习:
1)特定类型

template <typename T>
struct DataProc
{
	void dataProcess();
}

template<>
struct DataProc<int>
{
	void dataProcess()
	{
		// int型数据特殊处理
	}
}
  1. 多种输入形参的组合
//例-1
template <typename... Ts> struct TypeList;

// 源模板
template <typename, typename> struct TypeListConjunction;
//特化版本
template <typename... Ts1, typename... Ts2>
struct TypeListConjunction<TypeList<Ts1...>, TypeList<Ts2...>>
{
	//目的:类型列表拼接
	typedef TypeList<Ts1..., Ts2...> Value;
}

//例-2
// 通用类模板的定义
template <typename Signature>
struct FunctionPtr;

// 类模板特化版本
template <typename Ret, typename... Args>
struct FunctionPtr<Ret(Args...)>
{
	// 目的1:推导函数返回值类型
	using FR = Ret;
	// 目的2:Args的解包操作,获取函数形参列表
	using ArgList = TypeList<Args...>
};

#include <QDebug>
template <typename F>
void CreateFunctionPtr(const F& f)
{
	using Fptr = FunctionPtr<F>;  // 匹配特化版本,完成类型推导

	Fptr ptr_function;
	qDebug() << "function ret type: " << IsInt<typename Fptr::FR>::ret;
}

B. 函数模板重载
函数模板的重载其实跟模板特化是类似的,都将问题分类、特征化处理。 也有通用函数模板的定义,只不过换了种说法,最终都是要借助编译器的类型推导和匹配能力。

// 定义模板函数,测试其类型转发和解包
template <typename F>
void CreateFunctionPtr(const F& f)
{
	using Fptr = FunctionPtr<F>;

	Fptr ptr_function;
	qDebug() << "function ret type: " << IsInt<typename Fptr::FR>::ret;
}

// 函数模板的特化版本
template<typename Ret, typename... Args>
void CreateFunctionPtr(Ret(ptr_function)(Args...))
{
	qDebug() << "go here";
	// 可以将推导的类型用于模板类或模板函数的实例化;
}

//测试代码
void test(int a){}

void TestTemplateDeduction()
{
	CreateFunctionPtr(test); //  调用处完成函数定义的推导
}

3. 特化使用之于继承与非继承方式
在模板特化的使用上有继承和非继承方式两种,先看个示例代码:

template <typename T, T... Values>
struct ValueList
{
	using ValT = T;
};

template <typename List>
struct FrontT;

template <typename T, T Head, T... Tails>
struct FrontT<ValueList<T, Head, Tails...>>
{
	static const T value = Head;
};

template <typename List>
struct PopFrontT;

template <typename T, T Head, T... Tails>
struct PopFrontT<ValueList<T, Head, Tails...>>
{
	using Type = ValueList<T, Tails...>;
};

//#1 继承基类中调用
// template <typename List, size_t N>
// struct NthElementImpl : public NthElementImpl<typename PopFrontT<List>::Type, N - 1>
// {
// };

//#2 定义中的调用
template <typename List, size_t N>
struct NthElementImpl
{
	static const typename List::ValT value = NthElementImpl<typename PopFrontT<List>::Type, N - 1>::value;
};

//#3 调用FrontT基类实现
template <typename List>
struct NthElementImpl<List, 0> : public FrontT<List>
{	
};

// 模板定义1:
// template <size_t N, typename T, T... Values>
// struct NthElement : public NthElementImpl<ValueList<T, Values...>, N>
// {
// };

// 模板定义2
template <typename VList, size_t N>
struct NthElement : public NthElementImpl<VList, N>
{
};

首先认清模板的原始输入是(size_t N, typename T, T… values),模板的最终目的是从T类型的常数集合中找到索引为N的元素值。所以中间构造了两个辅助实现的Pop和Front类模板,在辅助模板的调用中,原始输入的直接透传,不利于功辅助函数的实现和表达,所以我们引入了中间类型ValueList,其职责是将封装常量参数集,作为辅助函数调用的转发类型。
接下来看下NthElement的实现,算法上是基于辅助模板的逐个弹出,直至第N-1个元素弹出后的余集,取Head。那么在辅助模板的调用上可以是类定义内的调用,也可以基类的调用。#3在调用关系体现上最为明显。不管是哪种调用方式站在NthElementImpl,都是NthElement 对原始参数组转化处理后对NthElementImpl的间接调用。这种转换过程,类似模板特化,可以类型扩展处理、包装等,转换后输出类型作为下类模板调用合法参数的输入。
那么二者的差别在于,基类继承调用方式,只适用于简单的参数转发处理过程,若中间转换处理过程非常复杂,则不太适合用继承方式。当然对于复杂过程封装为简单过程调用也是可以用继承方式的。但继承有另外的好处是能够将基类模板中定义的所有可见成员暴露给子类的调用方,不需要类型定义体中增加类型做中转(通过增加using或constexpr常量定义)。当然缺点在于, 继承是侵入式的,无法做可见控制,父类有什么,子类完全可以开放透传,包括子类职责上不需要体现的,所以看实际使用从二者中选择即可。
4. 模板设计中的推导
站在模板源码分析的角度,每引入一个类模板特化或模板函数重载,每引入一个中间函数模板或者类模板,都是为了类型的处理,以获取必要的类型信息,只不过这种中间过程由可以分为正向推导和反向推导(不知道这种正向和反向的表述是否合理,只是向表达二者是有差异的)。站在模板设计者的角度,则若中间过程走不同,必要的类型信息哪不到就要想到,这里可能要引入中间辅助模板,以获取必要类型信息。举个示例代码:

template<typename F, typename… Elements, unsigned… Indices>
auto applyImpl(F f, Tuple<Elements…> const& t,
	Valuelist<unsigned, Indices…>)
	->decltype(f(get<Indices>(t)))
{
	return f(get<Indices>(t));
}
template<typename F, typename… Elements,
	unsigned N = sizeof(Elements)>
	auto apply(F f, Tuple<Elements…> const& t)
	->decltype(applyImpl(f, t, MakeIndexList<N>()))
{
	return applyImpl(f, t, MakeIndexList<N>());
}

apply到appImpl调用前,MakeIndexList调用创建一个长度为sizeof…(Elements)的常量索引列表,所以有了一次中间过程调用,得到了中间参数类型。applyImpl的中的get调用也是一样的道理,只不过是配合参数的解包过程一起使用,对解包中的每个元素做了中间处理。这些过程都是正向的,输入、输出比较明确。还有一种则类似反向的,如我们常见:

template <typename F>
struct Invoke;

template <typename... Ts>
struct TypeList;

template <typename Ret, typename Param...>
struct Invoke<Ret(Param...)>
{
	using R = Ret;
	using ParamList = TypeList<Param...>
};

通过模板的特化,能够对F做解析,从而获取制定类型的F,其返回值和入参类型,其实有点反向的意思。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
函数模板重载是指在同一作用域内定义多个同名函数模板,但这些函数模板的参数列表或参数类型不同,从而实现了函数模板重载函数模板重载的方法有两种: 1. 普通函数函数模板重载 可以在函数模板的定义前或后定义一个普通函数,这个普通函数函数模板同名但参数列表或参数类型不同,就可以实现重载。例如: ```cpp // 普通函数 void swap(int& a, int& b) { int temp = a; a = b; b = temp; } // 函数模板 template<typename T> void swap(T& a, T& b) { T temp = a; a = b; b = temp; } ``` 2. 函数模板之间的重载 可以在同一作用域内定义多个函数模板,这些函数模板同名但参数列表或参数类型不同,就可以实现重载。例如: ```cpp // 函数模板1 template<typename T> void print(T data) { cout << data << endl; } // 函数模板2 template<typename T1, typename T2> void print(T1 data1, T2 data2) { cout << data1 << " " << data2 << endl; } ``` 需要注意的是,函数模板重载的时候,需要注意避免歧义的情况。例如: ```cpp // 函数模板1 template<typename T> void foo(T data) { // ... } // 函数模板2 template<typename T> void foo(T& data) { // ... } ``` 这里的函数模板1和函数模板2都是同名的foo,参数列表也都是一样的,只是第二个参数加了一个引用符号。在这种情况下,编译器无法确定应该调用哪个函数模板,会报错。为避免这种情况,可以采用特化或默认参数的方式来实现函数模板重载

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值