CPP-Templates-2nd--第十九章 萃取的实现 19.1-19.3

目录

19.1 一个例子:对一个序列求和

19.1.2 值萃取(Value Traits)(Static 成员函数或者static成员)

19.1.3 参数化的萃取

19.2 萃取还是策略以及策略类(Traits versus Policies and Policies Classes)

19.2.1 萃取和策略:有什么区别?(Traits and Policies: What’s the Difference?)

19.2.2 成员模板还是模板模板参数?(Member Templates versus Template Template Parameter)

19.2.3 结合多个策略以及/或者萃取(Combining Multiple Policies and/or Traits) 

19.2.4 通过普通迭代器实现累积(Accumulation with General Iterators)

19.3 类型函数(Type Function)

19.3.1 元素类型(Element Type)

19.3.2 转换萃取(Transformation Traits)

删除引用

添加引用

移除限制符

退化(Decay)

19.3.3 预测型萃取(Predicate Traits)

IsSameT

true_type 和 false_type

19.3.4 返回结果类型萃取(Result Type Traits)

declval

补充:true_type 和 false_type有什么用处,为什么要特别定义他们?

布尔常量只有两个吗?


 

参考:GitHub - Walton1128/CPP-Templates-2nd--: 《C++ Templates 第二版》中文翻译,和原书排版一致,第一部分(1至11章)以及第18,19,20,21、22、23、24、25章已完成,其余内容逐步更新中。 个人爱好,发现错误请指正 

时间证明大部分被引入的额外的模板参数都有合理的默认值。在一些情况下,额 外的模板参数可以完全由很少的、主要的模板参数决定,我们接下来会看到,这一类额外的 模板参数可以被一起省略掉。

19.1 一个例子:对一个序列求和

#ifndef ACCUM_HPP
#define ACCUM_HPP
template<typename T>
T accum(T const* beg, T const* end)
{
	T total{}; // assume this actually creates a zero value
	while (beg != end) {
		total += *beg;
		++beg;
	}
	return total;
}
#endif //ACCUM_HPP
#include "accum1.hpp"
#include <iostream>
int main()
{
	// create array of 5 integer values
	int num[] = { 1, 2, 3, 4, 5 };
	// print average value
	std::cout << "the average value of the integer values is " << accum(num,
		num + 5) / 5 << '\n';
	// create array of character values
	char name[] = "templates";
	int length = sizeof(name) - 1;
	// (try to) print average character value
	std::cout << "the average value of the characters in \"" << name <<
		"\" is " << accum(name, name + length) / length << '\n';
}

输出:

the average value of the integer values is 3

the average value of the characters in “template” is -5

问题在于我们的模板是被 char 实例化的,其数值范围即使是被用来存储相对较小的数值的 和也是不够的。很显然,为了解决这一问题我们应该引入一个额外的模板参数 AccT,并将 其用于返回值 total 的类型。但是这会给模板的用户增加负担:在调用这一模板的时候,他 们必须额外指定一个类型。

一个可以避免使用额外的模板参数的方式是,在每个被用来实例化 accum()的 T 和与之对应 的应该被用来存储返回值的类型之间建立某种联系。这一联系可以被认为是 T 的某种属性。 正如下面所展示的一样,可以通过模板的偏特化建立这种联系:

template<typename T>
struct AccumulationTraits;
template<>
struct AccumulationTraits<char> {
using AccT = int;
};
template<>
struct AccumulationTraits<short> {
using AccT = int;
};
template<>
struct AccumulationTraits<int> {
using AccT = long;
};
template<>
struct AccumulationTraits<unsigned int> {
using AccT = unsigned long;
};
template<>
struct AccumulationTraits<float> {
using AccT = double;
};

AccumulationTraits 模板被称为萃取模板,因为它是提取了其参数类型的特性。(通常而言可 以有不只一个萃取,也可以有不只一个参数)。

#ifndef ACCUM_HPP
#define ACCUM_HPP
#include "accumtraits2.hpp"
template<typename T>
auto accum (T const* beg, T const* end)
{
// return type is traits of the element type
using AccT = typename AccumulationTraits<T>::AccT;
AccT total{}; // assume this actually creates a zero value
while (beg != end) {
total += *beg;
++beg;
}
return total;
}
#endif //ACCUM_HPP

此时程序的输出就和我们所预期一样了:

the average value of the integer values is 3

the average value of the characters in “template” is 108

考虑到我们为算法加入了很好的检查机制,总体而言这些变化不算太大。而且,如果要将 accum()用于新的类型的话,只要对 AccumulationTraits 再进行一次显式的偏特化,就会得到 一个 AccT。值得注意的是,我们可以为任意类型进行上述操作:基础类型,声明在其它库 中的类型,以及其它诸如此类的类型。

19.1.2 值萃取(Value Traits)(Static 成员函数或者static成员)

到目前为止我们看到的萃取,代表的都是特定“主”类型的额外的类型信息。在本节我们将 会看到,这一“额外的信息”并不仅限于类型信息。还可以将常量以及其它数值类和一个类 型关联起来。

在最原始的 accum()模板中,我们使用默认构造函数对返回值进行了初始化,希望将其初始 化为一个类似零(zero like)的值:

AccT total{}; // assume this actually creates a zero value …
return total

很显然,这并不能保证一定会生成一个合适的初始值。因为 AccT 可能根本就没有默认构造函数。

 萃取可以再一次被用来救场。对于我们的例子,我们可以为 AccumulationTraits 添加一个新 的值萃取(value trait,似乎翻译成值特性会更好一些):

template<typename T>
struct AccumulationTraits;
template<>
struct AccumulationTraits<char> {
using AccT = int;
static AccT const zero = 0;
};
template<>
struct AccumulationTraits<short> {
using AccT = int;
static AccT const zero = 0;
};
template<>
struct AccumulationTraits<int> {
using AccT = long;
static AccT const zero = 0;
};

在这个例子中,新的萃取提供了一个可以在编译期间计算的,const的zero成员。此时,accum() 的实现如下:

#ifndef ACCUM_HPP
#define ACCUM_HPP
#include "accumtraits3.hpp"
template<typename T>
auto accum (T const* beg, T const* end)
{
// return type is traits of the element type
using AccT = typename AccumulationTraits<T>::AccT;
AccT total = AccumulationTraits<T>::zero; // init total by trait value
while (beg != end) {
total += *beg;
++beg;
}
return total;
} #
endif // ACCUM_HPP

这一实现的一个不足之处是,C++只允许我们在类中对一个整形或者枚举类型的 static const 数据成员进行初始化。

Constexpr 的 static 数据成员会稍微好一些,允许我们对 float 类型以及其它字面值类型进行 类内初始化:

template<>
struct AccumulationTraits<float> {
using Acct = float;
static constexpr float zero = 0.0f;
};

一个比较直接的解决方案是,不再类中定义值萃取(只做声明):

这样虽然可以工作,但是却有些麻烦(必须在两个地方同时修改代码),这样可能还会有些 低效,因为编译期通常并不知晓在其它文件中的变量定义。

在 C++17 中,可以通过使用 inline 变量来解决这一问题:

template<>
struct AccumulationTraits<BigInt> {
using AccT = BigInt;
inline static BigInt const zero = BigInt{0}; // OK since C++17
};

在 C++17 之前的另一种解决办法是,对于那些不是总是生成整型值的值萃取,使用 inline 成 员函数。同样的,如果成员函数返回的是字面值类型,可以将该函数声明为 constexpr 的。

比如,我们可以像下面这样重写 AccumulationTraits:

template<typename T>
struct AccumulationTraits;
template<>
struct AccumulationTraits<char> {
using AccT = int;
static constexpr AccT zero() {
return 0;
}
};
template<>
struct AccumulationTraits<short> {
using AccT = int;
static constexpr AccT zero() {
return 0;
}
};
template<>
struct AccumulationTraits<int> {
using AccT = long;
static constexpr AccT zero() {
return 0;
}
};
template<>
struct AccumulationTraits<unsigned int> {
using AccT = unsigned long;
static constexpr AccT zero() {
return 0;
}
};
template<>
struct AccumulationTraits<float> {
using AccT = double;
static constexpr AccT zero() {
return 0;
}
};

然后针我们自定义的类型对这些萃取进行扩展:

template<>
struct AccumulationTraits<BigInt> {
using AccT = BigInt;
static BigInt zero() {
return BigInt{0};
}
};

在应用端,唯一的区别是函数的调用语法(不像访问一个 static 数据成员那么简洁):

AccT total = AccumulationTraits<T>::zero(); // init total by trait function

很明显,萃取可以不只是类型。在我们的例子中,萃取可以是一种能够提供所有在调用 accum()时所需的调用参数的信息的技术。这是萃取这一概念的关键:萃取为泛型编程提供 了一种配置(configure)具体元素(通常是类型)的手段。

19.1.3 参数化的萃取

 在前面几节中,在 accum()里使用的萃取被称为固定的(fixed),这是因为一旦定义了解耦 合萃取,在算法中它就不可以被替换。但是在某些情况下,这一类重写(overriding)行为 却又是我们所期望的。

为了解决这一问题,可以为萃取引入一个新的模板参数 AT,其默认值由萃取模板决定:

#ifndef ACCUM_HPP
#define ACCUM_HPP
#include "accumtraits4.hpp"
template<typename T, typename AT = AccumulationTraits<T>>
auto accum (T const* beg, T const* end)
{
typename AT::AccT total = AT::zero();
while (beg != end) {
total += *beg;
++beg;
}
return total;
}
#endif //ACCUM_HP

 采用这种方式,一部分用户可以忽略掉额外模板参数,而对于那些有着特殊需求的用户,他 们可以指定一个新的类型来取代默认类型。但是可以推断,大部分的模板用户永远都不需要 显式的提供第二个模板参数,因为我们可以为第一个模板参数的每一种(通过推断得到的) 类型都配置一个合适的默认值。

19.2 萃取还是策略以及策略类(Traits versus Policies and Policies Classes)

下面是一个在 accum()中引入这样一个策略的例子:

#ifndef ACCUM_HPP
#define ACCUM_HPP
#include "accumtraits4.hpp"
#include "sumpolicy1.hpp"
template<typename T,
typename Policy = SumPolicy,
typename Traits = AccumulationTraits<T>>
auto accum (T const* beg, T const* end)
{
using AccT = typename Traits::AccT;
AccT total = Traits::zero();
while (beg != end) {
Policy::accumulate(total, *beg);
++beg;
}
return total;
}
#endif //ACCUM_HPP

在这一版的 accum()中,SumPolicy 是一个策略类,也就是一个通过预先商定好的接口,为算 法实现了一个或多个策略的类。SumPolicy 可以被实现成下面这样:

#ifndef SUMPOLICY_HPP
#define SUMPOLICY_HPP
class SumPolicy {
public:
template<typename T1, typename T2>
static void accumulate (T1& total, T2 const& value) {
total += value;
}
};
#endif //SUMPOLICY_HPP

 如果提供一个不同的策略对数值进行累积的话,我们可以计算完全不同的事情。比如考虑下 面这个程序,它试图计算一组数值的乘积:

#include "accum6.hpp"
#include <iostream>
class MultPolicy {
public:
template<typename T1, typename T2>
static void accumulate (T1& total, T2 const& value) {
total *= value;
}
};
int main()
{
// create array of 5 integer values
int num[] = { 1, 2, 3, 4, 5 };
// print product of all values
std::cout << "the product of the integer values is " <<
accum<int,MultPolicy>(num, num+5) << ’\n’;
}

不是所有的事情都要用 萃取和策略才能够解决的。比如,C++标准库中的 std::accumulate()就将其初始值当作了第三 个参数。

19.2.1 萃取和策略:有什么区别?(Traits and Policies: What’s the Difference?)

我们倾向于对策略类这一名词的使用做如下限制:它们应该是一些编码了某种与其 它模板参数大致独立的行为的类。

策略和萃取有很多相似之处,只是它们更侧重于行为,而不是类型。

萃取类:一个用来代替模板参数的类。作为一个类,它整合了有用的类型和常量;作为一 个模板,它为实现一个可以解决所有软件问题的“额外的中间层”提供了方法。

总体而言,我们更倾向于使用如下(稍微模糊的)定义:

 萃取代表的是一个模板参数的本质的、额外的属性。

 策略代表的是泛型函数和类型(通常都有其常用地默认值)的可以配置的行为。

为了进一步阐明两者之间可能的差异,我们列出了如下和萃取有关的观察结果:

 萃取在被当作固定萃取(fixed traits)的时候会比较有用(比如,当其不是被作为模板 参数传递的时候)。

 萃取参数通常都有很直观的默认参数(很少被重写,或者简单的说是不能被重写)。

 萃取参数倾向于紧密的依赖于一个或者多个主模板参数。

 萃取在大多数情况下会将类型和常量结合在一起,而不是成员函数。

 萃取倾向于被汇集在萃取模板中。

对于策略类,我们有如下观察结果:

 策略类如果不是被作为模板参数传递的话,那么其作用会很微弱。

 策略参数不需要有默认值,它们通常是被显式指定的(虽有有些泛型组件通常会使用默 认策略)。

 策略参数通常是和其它模板参数无关的。

 策略类通常会包含成员函数。

 策略可以被包含在简单类或者类模板中。

19.2.2 成员模板还是模板模板参数?(Member Templates versus Template Template Parameter)

为了实现累积策略(accumulation policy),我们选择将 SumPolicy 和 MultPolicy 实现为有成 员模板的常规类。另一种使用类模板设计策略类接口的方式,此时就可以被当作模板模板参 数使用:

#ifndef SUMPOLICY_HPP
#define SUMPOLICY_HPP
template<typename T1, typename T2>
class SumPolicy {
public:
static void accumulate (T1& total, T2 const& value) {
total += value;
}
};
#endif //SUMPOLICY_HPP
#ifndef ACCUM_HPP#define ACCUM_HPP
#include "accumtraits4.hpp"
#include "sumpolicy2.hpp"
template<typename T,
template<typename,typename> class Policy = SumPolicy,
typename Traits = AccumulationTraits<T>>
auto accum (T const* beg, T const* end)
{
using AccT = typename Traits::AccT;
AccT total = Traits::zero();
while (beg != end) {
Policy<AccT,T>::accumulate(total, *beg);
++beg;
}
return total;
}

通过模板模板参数访问策略类的主要优势是,让一个策略类通过一个依赖于模板参数的类型 携带一些状态信息会更容易一些(比如 static 数据成员)。(在我们的第一个方法中,static 数据成员必须要被嵌入到一个成员类模板中。)

但是,模板模板参数方法的一个缺点是,策略类必须被实现为模板,而且模板参数必须和我 们的接口所定义的参数一样。这可能会使萃取本身的表达相比于非模板类变得更繁琐,也更 不自然。

19.2.3 结合多个策略以及/或者萃取(Combining Multiple Policies and/or Traits) 

没太看明白。

19.2.4 通过普通迭代器实现累积(Accumulation with General Iterators)

19.3 类型函数(Type Function)

最初的示例说明我们可以基于类型定义行为。

传统上我们在 C 和 C++里定义的函数可以被更 明确的称为值函数(value functions):它们接收一些值作为参数并返回一个值作为结果。

对于模板,我们还可以定义类型函数(type functions):它们接收一些类型作为参数并返回 一个类型或者常量作为结果。

类模板依然可以被用作类型函数。此时类型函数的参数是模板参数,其结果被提取为 成员类型或者成员常量。

比如,sizeof 运算符可以被作为如下接口提供:

#include <cstddef>
#include <iostream>
template<typename T>
struct TypeSize {
static std::size_t const value = sizeof(T);
};
int main()
{
std::cout << "TypeSize<int>::value = " << TypeSize<int>::value << ’
\n’;
}

这看上去可能没有那么有用,因为我们已经有了一个内置的 sizeof 运算符,但是请注意此处 的 TypeSize是一个类型,它可以被作为类模板参数传递。或者说,TypeSize 是一个模板, 也可以被作为模板模板参数传递。

将它们用作萃 取类

19.3.1 元素类型(Element Type)

我们希 望得到这样一个类型函数,当给的一个容器类型时,它可以返回相应的元素类型。这可以通 过偏特化实现:

#include <vector>
#include <list>
template<typename T>
struct ElementT; // primary template
template<typename T>
struct ElementT<std::vector<T>> { //partial specialization for
std::vector
using Type = T;
};
template<typename T>
struct ElementT<std::list<T>> { //partial specialization for std::list
using Type = T;
};…
template<typename T, std::size_t N>
struct ElementT<T[N]> { //partial specialization for arrays of known bounds
using Type = T;
};
template<typename T>
struct ElementT<T[]> { //partial specialization for arrays of unknown bounds
using Type = T;
};

那么类型函数的作用体现在什么地方呢?它允许我们根据容器类型参数化一个模板,但是又 不需要提供代表了元素类型和其它特性的参数。比如,相比于使用

template<typename T, typename C>
T sumOfElements (C const& c);

这一需要显式指定元素类型的模板(sumOfElements list),我们可以定义这样一个模板:

template<typename C>
typename ElementT<C>::Type sumOfElements (C const& c);

其元素类型是通过类型函数得到的。

注意观察萃取是如何被实现为已有类型的扩充的;也就是说,我们甚至可以为基本类型和封 闭库的类型定义类型函数。

在上述情况下,ElementT 被称为萃取类,因为它被用来访问一个已有容器类型的萃取(通 常而言,在这样一个类中可以有多个萃取)。因此萃取类的功能并不仅限于描述容器参数的 特性,而是可以描述任意“主参数”的特性。

为了方便,我们可以伟类型函数创建一个别名模板。比如,我们可以引入:

template<typename T>
using ElementType = typename ElementT<T>::Type;

这可以让 sumOfEkements 的定义变得更加简单:

template<typename C>
ElementType<C> sumOfElements (C const& c);
19.3.2 转换萃取(Transformation Traits)

除了可以被用来访问主参数类型的某些特性,萃取还可以被用来做类型转换,比如为某个类 型添加或移除引用、const 以及 volatile 限制符。

删除引用

C++标准库提供了一个相应的 std::remove_reference<>萃取,详见附录 D.4。

添加引用

我们也可以给一个已有类型添加左值或者右值引用:

template<typename T>
struct AddLValueReferenceT {
using Type = T&;
};
template<typename T>
using AddLValueReference = typename AddLValueReferenceT<T>::Type;
template<typename T>
struct AddRValueReferenceT {
using Type = T&&;
};
template<typename T>
using AddRValueReference = typename AddRValueReferenceT<T>::Type;

引用折叠的规则在这一依然适用:

std::add_lvalue_reference<T&>::type is T&

std::add_lvalue_reference<T&&>::type is T&

std::add_rvalue_reference<T&>::type is T&

std::add_rvalue_reference<T&&>::type is T&&

比如对于 AddLValueReference, 返回的类型是 int&,因为我们不需要对它们进行偏特化实现。

最方便的别名模板可以被简化成下面这样:

template<typename T>
using AddLValueReferenceT = T&;
template<typename T>
using AddRValueReferenceT = T&&

 使用 别名模板:

int main() {
    int x = 5;
    AddLValueReferenceT<int> lref = x;  // int& lref = x;
    AddRValueReferenceT<int> rref = 10;  // int&& rref = 10;

    return 0;
}
移除限制符

如果一个类型中 存在 const 限制符,我们可以将其移除:

template<typename T>
struct RemoveConstT {
using Type = T;
};
template<typename T>
struct RemoveConstT<T const> {
using Type = T;
};
template<typename T>
using RemoveConst = typename RemoveConstT<T>::Type;

而且,转换萃取可以是多功能的,比如创建一个可以被用来移除 const 和 volatile 的 RemoveCVT 萃取:

#include "removeconst.hpp"
#include "removevolatile.hpp"
template<typename T>
struct RemoveCVT : RemoveConstT<typename RemoveVolatileT<T>::Type>
{
};
template<typename T>
using RemoveCV = typename RemoveCVT<T>::Type;

RemoveCVT 中有两个需要注意的地方。第一个需要注意的地方是,它同时使用了 RemoveConstT 和相关的 RemoveVolitleT,首先移除类型中可能存在的 volatile,然后将得到 了类型传递给 RemoveConstT。第二个需要注意的地方是,它没有定义自己的和 RemoveConstT 中 Type 类似的成员,而是通过使用元函数转发(metafunction forwarding)从 RemoveConstT 中继承了 Type 成员。这里元函数转发被用来简单的减少 RemoveCVT 中的类型成员。

C++ 标 准 库 也 提 供 了 与 之 对 应 的 std::remove_volatile<> , std::remove_const<> , 以 及 std::remove_cv<>。

退化(Decay)
#include <iostream>
#include <typeinfo>
#include <type_traits>
template<typename T>
void f(T)
{}
template<typename A>
void printParameterType(void (*)(A))
{
std::cout << "Parameter type: " << typeid(A).name() << ’\n’;
std::cout << "- is int: " <<std::is_same<A,int>::value << ’\n’;
std::cout << "- is const: " <<std::is_const<A>::value << ’\n’;
std::cout << "- is pointer: " <<std::is_pointer<A>::value << ’\n’;
}
int main()
{
printParameterType(&f<int>);
printParameterType(&f<int const>);
printParameterType(&f<int[7]>);
printParameterType(&f<int(int)>);
}

在程序的输出中,除了 int 参数保持不变外,其余 int const,int[7],以及 int(int)参数分别退 化成了 int,int*,以及 int(*)(int)。

我们可以实现一个与之功能类似的萃取。为了和 C++标准库中的 std::decay 保持匹配,我们 称之为 DecayT。

template<typename T>
struct DecayT : RemoveCVT<T>
{
};

template<typename T>
struct DecayT<T[]> {
using Type = T*;
};
template<typename T, std::size_t N>
struct DecayT<T[N]> {
using Type = T*;
};


template<typename R, typename… Args>
struct DecayT<R(Args…)> {
using Type = R (*)(Args…);
};
template<typename R, typename… Args>
struct DecayT<R(Args…, …)> {
using Type = R (*)(Args…, …);
};
19.3.3 预测型萃取(Predicate Traits)

到目前为止,我们学习并开发了适用于单个类型的类型函数:给定一个类型,产生另一些相 关的类型或者常量。但是通常而言,也可以设计基于多个参数的类型函数。这同样会引出另 外一种特殊的类型萃取--类型预测(产生一个 bool 数值的类型函数)。

IsSameT

IsSameT 将判断两个类型是否相同:

template<typename T1, typename T2>
struct IsSameT {
static constexpr bool value = false;
};
template<typename T>
struct IsSameT<T, T> {
static constexpr bool value = true;
};

这里的主模板说明通常我们传递进来的两个类型是不同的,因此其 value 成员是 false。但是, 通过使用偏特化,当遇到传递进来的两个相同类型的特殊情况,value 成员就是 true 的。

对于产生一个常量的萃取,我们没法为之定义一个别名模板,但是可以为之定义一个扮演可 相同角色的 constexpr 的变量模板(模板变量)

template<typename T1, typename T2>
constexpr bool isSame = IsSameT<T1, T2>::value
true_type 和 false_type

通过为可能的输出结果 true 和 false 提供不同的类型,我们可以大大的提高对 IsSameT 的定 义。事实上,如果我们声明一个 BoolConstant 模板以及两个可能的实例 TrueType 和 FalseType:、

template<bool val>
struct BoolConstant {
using Type = BoolConstant<val>;
static constexpr bool value = val;
};
using TrueType = BoolConstant<true>;
using FalseType = BoolConstant<false>;

就可以基于两个类型是否匹配,让相应的 IsSameT 分别继承自 TrueType 和 FalseType:

#include "boolconstant.hpp"
template<typename T1, typename T2>
struct IsSameT : FalseType{};
template<typename T>
struct IsSameT<T, T> : TrueType{};

通常而言,产生 bool 值的萃取都应该通过从诸如 TrueType 和 FalseType 的类型进行派生来支 持标记派发。但是为了尽可能的进行泛化,应该只有一个类型代表 true,也应该只有一个类 型代表 false,而不是让每一个泛型库都为 bool 型常量定义它自己的类型。

19.3.4 返回结果类型萃取(Result Type Traits)

由于语言本身允许我们对一个 char 型数值和一个整形数值求和,我们 自然也很希望能够对 Array 也执行这种混合类型(mixed-type)的操作。这样我们就要处理 该如何决定相关模板的返回值的问题:

template<typename T1, typename T2>
Array<???> operator+ (Array<T1> const&, Array<T2> const&);

可以解决上述问题的方式就是返回值类型模板:

template<typename T1, typename T2>
Array<typename PlusResultT<T1, T2>::Type>
operator+ (Array<T1> const&, Array<T2> const&);

如果有便捷别名模板可用的话,还可以将其写称这样:

template<typename T1, typename T2>
Array<PlusResult<T1, T2>>
operator+ (Array<T1> const&, Array<T2> const&);

其中的 PlusResultT 萃取会自行判断通过+操作符对两种类型(可能是不同类型)的数值求和 所得到的类型:

template<typename T1, typename T2>
struct PlusResultT {
using Type = decltype(T1() + T2());
};
template<typename T1, typename T2>
using PlusResult = typename PlusResultT<T1, T2>::Type;

这一萃取模板通过使用 decltype 来计算表达式 T1()+T2()的类型,将决定结果类型这一艰巨的 工作(包括处理类型增进规则(promotion rules)和运算符重载)留给了编译器。

事实上我们所期望的是将返回值类型中的引用和限制符移除之后 所得到的类型,正如我们在上一小节所讨论的那样:

template<typename T1, typename T2>
Array<RemoveCV<RemoveReference<PlusResult<T1, T2>>>>
operator+ (Array<T1> const&, Array<T2> const&)

这一萃取的嵌套形式在模板库中很常见,在元编程中也经常被用到。

但是上述形式的 PlusResultT 却对元素类型 T1 和 T2 施加了一个我们所不期 望的限制:由于表达式 T1() + T2()试图对类型 T1 和 T2 的数值进行值初始化,这两个类型必 须要有可访问的、未被删除的默认构造函数(或者是非 class 类型)。

declval

好在我们可以很简单的在不需要构造函数的情况下计算+表达式的值,方法就是使用一个可 以为一个给定类型 T 生成数值的函数。为了这一目的,C++标准提供了 std::declval<>,在第 11.2.3 节有对其进行介绍。在中其定义如下:

namespace std {
template<typename T>
add_rvalue_reference_t<T> declval() noexcept;
}

表达式 declval<>可以在不需要使用默认构造函数(或者其它任意操作)的情况下为类型 T 生成一个值。

它有两个很有意思的属性:

 对于可引用的类型,其返回类型总是相关类型的右值引用,这能够使 declval 适用于那 些不能够正常从函数返回的类型,比如抽象类的类型(包含纯虚函数的类型)或者数组 类型。因此当被用作表达式时,从类型 T 到 T&&的转换对 declval()的行为是没有影 响的:其结果都是右值(如果 T 是对象类型的话),对于右值引用,其结果之所以不会 变是因为存在引用塌缩(参见第 15.6 节)。

 在 noexcept 异常规则中提到,一个表达式不会因为使用了 declval 而被认成是会抛出异 常的。当 declval 被用在 noexcept 运算符上下文中时,这一特性会很有帮助(参见第 19.7.2 节)。

有了 declval,我们就可以不用在 PlusResultT 中使用值初始化了:

#include <utility>
template<typename T1, typename T2>
struct PlusResultT {
using Type = decltype(std::declval<T1>() + std::declval<T2>());
};
template<typename T1, typename T2>
using PlusResult = typename PlusResultT<T1, T2>::Type

返回值类型萃取提供了一种从特定操作中获取准确的返回值类型的方式,在确定函数模板的 返回值的类型的时候,它会很有用。

补充:true_type 和 false_type有什么用处,为什么要特别定义他们?

来自gpt:

`TrueType` 和 `FalseType` 是特殊的类型定义,通常用作模板元编程中的标记类型(tag types)或者作为类型萃取(type traits)的基础。

这些类型的定义有以下用处:

1. 作为标记类型:`TrueType` 和 `FalseType` 可以用作函数重载或模板特化的标记类型。通过将它们作为函数参数或模板参数的类型,可以在编译时根据类型的真假值选择不同的函数重载或模板特化。这样可以实现基于条件的编译。

2. 作为类型萃取:`TrueType` 和 `FalseType` 通常用于定义类型特征(type traits)。类型特征是一种编译时的元数据,它提供有关类型的信息,例如类型是否是整数类型、是否是指针类型等。通过定义特定类型的类型特征,可以在编译时对类型进行查询和判断。

为什么要特别定义 `TrueType` 和 `FalseType` 呢?

特别定义 `TrueType` 和 `FalseType` 主要是为了提供更直观和语义化的标记类型。它们的命名传达了布尔常量的含义,使代码的可读性更高。

此外,它们还提供了一种标准化的方式来表示真假值。在模板元编程中,使用 `TrueType` 和 `FalseType` 可以使代码更加一致和易于理解。这种约定俗成的命名和表示方式使得代码更易于阅读、维护和扩展。

综上所述,`TrueType` 和 `FalseType` 作为标记类型和类型萃取的基础,提供了一种标准化的、语义化的表示方式,使得模板元编程中的代码更加清晰、易读和可扩展。

布尔常量只有两个吗?

在传统的布尔逻辑中,布尔常量确实只有两个:`true` 和 `false`。它们分别表示真和假。

然而,在模板元编程中,为了提供更多的灵活性和表达能力,可以使用自定义的布尔常量类型。这些自定义的布尔常量类型通常是通过模板结构体或模板类来实现的。

在示例代码中,`BoolConstant` 结构体就是用于表示自定义的布尔常量。它具有一个静态成员变量 `value`,用于存储布尔值。通过定义自定义的布尔常量类型,我们可以在编译时进行复杂的条件判断和类型推导。

例如,可以定义 `TrueType` 和 `FalseType` 类型别名如下:

```cpp
using TrueType = BoolConstant<true>;
using FalseType = BoolConstant<false>;
```

这样,我们就可以使用 `TrueType` 和 `FalseType` 在模板元编程中表示更多的布尔常量。这些自定义的布尔常量类型可以用于模板特化、类型萃取、条件编译等场景,以实现更加灵活和复杂的编译时逻辑。

总结起来,传统的布尔逻辑中只有两个布尔常量 `true` 和 `false`,但在模板元编程中,可以使用自定义的布尔常量类型来表示更多的布尔值,以增强编译时的灵活性和表达能力。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值