C---高级元编程-全-
C++ 高级元编程(全)
一、模板
“c++ 支持多种风格。”
比雅尼·斯特劳斯特鲁普,透视 ISO C++
编程是通过用计算机的一种通用语言与机器对话来向它教授一些东西的过程。你越接近机器习语,单词就越不自然。
每种语言都有自己的表达能力。对于任何给定的概念,都有一种语言,它的描述更简单、更简洁、更详细。在汇编语言中,我们必须对任何(可能是简单的)算法给出极其丰富和精确的描述,这使得回读非常困难。另一方面,C++ 的美妙之处在于,在与机器语言足够接近的同时,这种语言携带了足够多的工具来丰富自己。
C++ 允许程序员用不同的风格来表达相同的概念,好的 C++ 看起来更自然。
首先你将看到模板和样式之间的联系,然后你将深入研究 C++ 模板系统的细节。
给定这个 C++ 片段:
double x = sq(3.14);
你能猜出 sq 是什么吗?它可能是一个宏:
#define sq(x) ((x)*(x))
一个功能:
double sq(double x)
{
return x*x;
}
一个功能模板:
template <typename scalar_t>
inline scalar_t sq(const scalar_t& x)
{
return x*x;
}
一个类型(一个类的未命名实例,衰减为双精度):
class sq
{
double s_;
public:
sq(double x)
: s_(x*x)
{}
operator double() const
{ return s_; }
};
一个全局对象:
class sq_t
{
public:
typedef double value_type;
value_type operator()(double x) const
{
return x*x;
}
};
const sq_t sq = sq_t();
不管 sq(3.14)是如何实现的,大多数人只要看着它就能猜出 sq(3.14)是干什么的。然而,视觉对等 并不意味着互换。例如,如果 sq 是一个类,向函数模板传递一个正方形将会触发一个意外的参数推导:
template <typename T> void f(T x);
f(cos(3.14)); // instantiates f<double>
f(sq(3.14)); // instantiates f<sq>. counterintuitive?
此外,您会期望尽可能高效地对每种可能的数字类型进行平方,但是不同的实现在不同的情况下可能会有不同的表现:
std::vector<double> v;
std::transform(v.begin(), v.end(), v.begin(), sq);
如果您需要转换一个序列,大多数编译器将从 sq 的最后一个实现中获得性能提升(如果 sq 是一个宏,则会出现错误)。
TMP 的目的是编写代码:
- 对人类用户来说视觉上清晰,因此没有人需要看下面。
- 从编译器的角度来看,在大多数/所有情况下都是有效的。
- 自适应程序的其余部分。 1
自适应意味着“可移植”(独立于任何特定的编译器)和“不强加约束”。sq 的实现要求它的参数从某个抽象基类派生,这不能称为自适应。
C++ 模板的真正威力是风格。比较以下等价行:
double x1 = (-b + sqrt(b*b-4*a*c))/(2*a);
double x2 = (-b + sqrt(sq(b)-4*a*c))/(2*a);
所有模板参数的计算和推导都是在编译时执行的,因此不会产生运行时开销。如果函数 sq 写得正确,第 2 行至少和第 1 行一样有效,同时也更容易阅读。
使用 sq 很优雅:
- 它使代码可读或不言自明
- 它不会带来速度损失
- 它使程序对未来的优化开放
事实上,在平方的概念已经从简单的乘法中分离出来之后,你可以很容易地插入专门化:
template <typename scalar_t>
inline scalar_t sq(const scalar_t& x)
{
return x*x;
}
template <>
inline double sq(const double& x)
{
// here, use any special algorithm you have!
}
1.1.C++ 模板
经典 C++ 语言承认两种基本类型的模板— 函数模板和类模板 2 :
下面是一个功能模板 :
template <typename scalar_t>
scalar_t sq(const scalar_t& x)
{
return x*x;
}
下面是一个类模板 :
template
<
typename scalar_t, // type parameter
bool EXTRA_PRECISION = false, // bool parameter with default value
typename promotion_t = scalar_t // type parameter with default value
>
class sum
{
// ...
};
当您为其所有参数提供合适的值时,模板会在编译期间生成实体。函数模板将产生函数,而类模板将产生类。从 TMP 的角度来看,最重要的想法可以总结如下:
- 您可以利用类模板在编译时执行计算。
- 函数模板可以从参数中自动推导出它们的参数。如果调用 sq(3.14),编译器会自动算出 scalar_t 是 double,生成函数 sq < double >,并在调用处插入。
两种模板实体都开始在尖括号中声明一个参数列表 。参数可以包括类型(用关键字 typename 或 class 声明)和非类型:整数和指针。 3
请注意,当参数列表很长,或者您只想单独注释每个参数时,您可能希望缩进它,就好像它是花括号内的代码块一样。
参数实际上可以有一个默认值:
sum<double> S1; // template argument is 'double', EXTRA_PRECISION is false
sum<double, true> S2;
模板可以被看作是一个元函数,它将一组参数映射到一个函数或一个类。例如,sq 模板
template <typename scalar_t>
scalar_t sq(const scalar_t& x);
将类型 T 映射到函数:
T T (*)(const T&)
换句话说,sq
相反,类模板
template <typename char_t = char>
class basic_string;
将类型 T 映射到类:
T basic_string<T>
有了类,的显式专门化可以限制元功能的域。你有一个通用模板,然后一些专门化;它们中的每一个都可能有也可能没有主体。
// the following template can be instantiated
// only on char and wchar_t
template <typename char_t = char>
class basic_string;
// note: no body
template < >
class basic_string<char>
{ ... };
template < >
class basic_string<wchar_t>
{ ... };
char_t 和 scalar_t 称为模板参数 。当使用 basic_string < char >和 sq < double >时,char 和 double 被称为模板参数 ,即使 double(sq 的模板参数)和 x(函数 sq < double >的参数)可能会有些混淆。
当您向模板提供模板参数(类型和非类型)时,模板被实例化,因此如果需要,编译器会为模板产生的实体产生机器码。
注意,不同的参数产生不同的实例,即使实例本身是相同的:sq
使用函数模板时,编译器通常会计算出参数。我们说参数将绑定到模板参数。
template <typename scalar_t>
scalar_t sq(const scalar_t& x) { return x*x; }
double pi = 3.14;
sq(pi); // the compiler "binds" double to scalar_t
double x = sq(3.14); // ok: the compiler deduces that scalar_t is double
double x = sq<double>(3.14); // this is legal, but less than ideal
所有模板参数必须是编译时常量。
- 类型参数将接受所有已知的类型。
- 非类型参数按照 most 自动铸造/提升规则工作。 5
以下是一些典型的错误:
template <int N>
class SomeClass
{
};
int main()
{
int A = rand();
SomeClass<A> s; // error: A is not a compile time constant
const int B = rand();
SomeClass<B> s; // error: B is not a compile time constant
static const int C = 2;
SomeClass<C> s; // OK
}
经典 C++ 中编译时常量的最佳语法是 static const[[integer type]]name = value;。
如前所述,如果常量是局部的,在函数体中可以省略静态前缀。然而,它既无害又清晰(您可以通过搜索“static const”而不是单独搜索“const”来找到项目中的所有编译时常量)。 6
传递给模板的参数可以是(编译时)计算的结果。每个有效的整数运算都可以在编译时常数上进行计算:
- 被零除会导致编译器错误。
- 禁止函数调用。 7
- 产生非整数/非指针类型的中间对象的代码是不可移植的,除非在 sizeof: (int)(N*1.2)内部,这是非法的。请改用(N+N/5)。static_cast
(0)也可以。 8
SomeClass<(27+56*5) % 4> s1;
SomeClass<sizeof(void*)*CHAR_BIT> s1;
只有当计算完全静态时,被零除才会导致编译器错误。要了解不同之处,请注意这个程序会编译(但不会运行)。
template <int N>
struct tricky
{
int f(int i = 0)
{
return i/N; // i/N is not a constant
}
};
int main()
{
tricky<0> t;
return t.f();
}
test.cpp(5) : warning C4723: potential divide by 0
另一方面,将前面的清单与下面的清单进行比较,其中被零除发生在编译期间(在两种不同的上下文中):
int f()
{
return N/N; // N/N is a constant
}
test.cpp(5) : error C2124: divide or mod by zero
.\test.cpp(5) : while compiling class template member function
'int tricky<N>::f(void)'
with
[
N=0
]
并且具有:
tricky<0/0> t;
test.cpp(12) : error C2975: 'N' : invalid template argument for 'tricky',
expected compile-time constant expression
更准确地说,编译时常数可以是:
-
整数文字,例如 27、CHAR_BIT 和 0x05
-
sizeof 和类似的具有整数结果的非标准语言操作符(例如,alignof where present)
-
非类型模板参数(在“外部”模板的上下文中)
template <int N> class AnotherClass { SomeClass<N> myMember_; };
-
整数类型的静态常数
template <int N, int K> struct MyTemplate { static const int PRODUCT = N*K; }; SomeClass< MyTemplate<10,12>::PRODUCT > s1;
-
一些标准的宏,比如 LINE(其实是有一定自由度的;通常,它们是 long 类型的常量,除了在依赖于实现的“编辑并继续”调试版本中,编译器必须使用引用。在这种情况下,使用宏将导致编译错误。) 9
SomeClass<__LINE__> s1; // usually works...
参数可以依赖于前一个参数:
template
<
typename T,
int (*FUNC)(T) // pointer to function taking T and returning int
>
class X
{
};
template
<
typename T, // here the compiler learns that 'T' is a type
T VALUE // may be ok or not... the compiler assumes the best
>
class Y
{
};
Y<int, 7> y1; // fine
Y<double, 3> y2; // error: the constant '3' cannot have type 'double'
类(和类模板)也可能有模板成员函数 :
// normal class with template member function
struct mathematics
{
template <typename scalar_t>
scalar_t sq(scalar_t x) const
{
return x*x;
}
};
// class template with template member function
template <typename scalar_t>
struct more_mathematics
{
template <typename other_t><sup class="calibre7">10</sup>
static scalar_t product(scalar_t x, other_t y)
{
return x*y;
}
};
double A = mathematics().sq(3.14);
double B = more_mathematics<double>().product(3.14, 5);
1.1.1. 类型名称
使用了关键字 typename :
- 作为类的同义词,在声明类型模板参数时
- 每当编译器看不出标识符是类型名时
对于“不明显”的示例,请考虑以下片段中的 my class
template <typename T>
struct MyClass
{
typedef double Y; // Y may or may not be a type
typedef T Type; // Type is always a type
};
template < >
struct MyClass<int>
{
static const int Y = 314; // Y may or may not be a type
typedef int Type; // Type is always a type
};
int Q = 8;
template <typename T>
void SomeFunc()
{
MyClass<T>::Y * Q; // what is this line? it may be:
// the declaration of local pointer-to-double named Q;
// or the product of the constant 314, times the global variable Q
};
y 是一个依赖名,因为它的含义依赖于未知参数 T。
所有直接或间接依赖于未知模板参数的都是依赖名。如果一个依赖名引用一个类型,那么它必须用 typename 关键字引入。
template <typename X>
class AnotherClass
{
MyClass<X>::Type t1_; // error: 'Type' is a dependent name
typename MyClass<X>::Type t2_; // ok
MyClass<double>::Type t3_; // ok: 'Type' is independent of X
};
请注意,在第一种情况下,typename 是必需的,在最后一种情况下是禁止的:
template <typename X>
class AnotherClass
{
typename MyClass<X>::Y member1_; // ok, but it won't compile if X is 'int'.
typename MyClass<double>::Y member2_; // error
};
在声明非类型模板参数时,typename 可能会引入依赖类型:
template <typename T, typename T::type N>
struct SomeClass
{
};
struct S1
{
typedef int type;
};
SomeClass<S1, 3> x; // ok: N=3 has type 'int'
出于好奇,经典的 C++ 标准规定,如果语法 typename T1::T2 在实例化期间产生一个非类型,那么程序就是病态的。然而,它没有指定相反的情况:如果 T1::T2 作为非类型具有有效的含义,那么如果必要的话,它可以在以后被重新解释为类型。例如:
template <typename T>
struct B
{
static const int N = sizeof(A<T>::X);
// should be: sizeof(typename A...)
};
在实例化之前,B“认为”它将在非类型上调用 sizeof 特别是,sizeof 是非类型上的有效运算符,因此代码是合法的。然而,X 可以在以后解析为一个类型,并且代码无论如何都是合法的:
template <typename T>
struct A
{
static const int X = 7;
};
template <>
struct A<char>
{
typedef double X;
};
尽管 typename 的目的是禁止所有这类歧义,但它可能无法涵盖所有的极端情况。 11
1.1.2.尖括号
即使所有参数都有默认值,也不能完全省略尖括号:
template <typename T = double>
class sum {};
sum<> S1; // ok, using double
sum S2; // error
模板参数可能有不同的含义:
- 有时它们确实应该是通用的,例如 std::vector
或 std::set 。可能有一些关于 T 的概念性假设——比如可构造的、可比较的...——不损害普遍性。 - 有时参数被假定属于一个固定的集合。在这种情况下,类模板只是两个或更多类似类的公共实现。 12
在后一种情况下,您可能希望提供一组不带尖括号的常规类,因此您可以从模板基中派生它们,或者只使用 typedef 13 :
template <typename char_t = char>
class basic_string
{
// this code compiles only when char_t is either 'char' or 'wchar_t'
// ...
};
class my_string : public basic_string<>
{
// empty or minimal body
// note: no virtual destructor!
};
typedef basic_string<wchar_t> your_string;
一个流行的编译器扩展(正式成为 C++0x 的一部分)是,两个或多个相邻的“尖括号”将被解析为“模板结束”,而不是“提取操作符”。无论如何,对于旧的编译器,添加额外的空格是一个好习惯:
std::vector<std::list<double>> v1;
// ^^
// may be parsed as "operator>>"
std::vector<std::list<double> > v2;
// ^^^
// always ok
1.1.3.通用构造函数
当处理两个完全相同类型的对象时,不会调用模板复制构造函数和赋值函数:
template <typename T>
class something
{
public:
// not called when S == T
template <typename S>
something(const something<S>& that)
{
}
// not called when S == T
template <typename S>
something& operator=(const something<S>& that)
{
return *this;
}
};
something<int> s0;
something<double> s1, s2;
s0 = s1; // calls user defined operator=
s1 = s2; // calls the compiler generated assignment
自定义模板成员有时被称为通用复制构造函数 和通用赋值 。注意,通用运算符取的是某个东西< X >,而不是 X。
C++ 标准 12.8 说:
- 因为模板构造函数永远不是复制构造函数,所以这种模板的存在不会抑制复制构造函数的隐式声明
- 模板构造函数与其他构造函数(包括复制构造函数)一起参与重载决策,如果模板构造函数比其他构造函数提供更好的匹配,则它可用于复制对象
事实上,在基类中使用非常通用的模板操作符可能会引入错误,如下例所示:
struct base
{
base() {}
template <typename T>
base(T x) {}
};
struct derived : base
{
derived() {}
derived(const derived& that)
: base(that) {}
};
derived d1;
derived d2 = d1;
d2 = d1 的赋值导致堆栈溢出。
隐式复制构造函数必须调用基类的复制构造函数,所以在 12.8 版本中,它不能调用通用构造函数。如果编译器为 derived 生成了一个复制构造函数,它就会调用基复制构造函数(这是隐式的)。可惜给了 derived 的一个复制构造函数,它包含了一个显式的函数调用,即 base(that)。因此,遵循通常的重载决策规则,它匹配 T=derived 的通用构造函数。因为这个函数通过值获取 x,所以它需要执行 x 的一个副本,因此这个调用是递归的。 14
1.1.4.函数类型和函数指针
注意函数类型和指向函数类型的指针之间的区别:
template <double F(int)>
struct A
{
};
template <double (*F)(int)>
struct B
{
};
它们大多是等价的:
double f(int)
{
return 3.14;
}
A<f> t1; // ok
B<f> t2; // ok
通常函数衰减到函数指针就像数组衰减到指针一样。但是函数类型是无法构造的,所以它会导致代码中出现看似无害的故障:
template <typename T>
struct X
{
T member_;
X(T value)
: member_(value)
{
}
};
X<double (int)> t1(f); // error: cannot construct 'member_'
X<double (*)(int)> t2(f); // ok: 'member_' is a pointer
这个问题在返回函子的函数中最为明显(读者可以考虑 std::not1 或者参见第 4.3.4 节)。在 C++ 中,通过引用获取参数的函数模板可以防止衰减:
template <typename T>
X<T> identify_by_val(T x)
{
return X<T>(x);
}
template <typename T>
X<T> identify_by_ref(const T& x)
{
return X<T>(x);
}
double f(int)
{
return 3.14;
}
identify_by_val(f); // function decays to pointer-to-function:
// template instantiated with T = double (*)(int)
identify_by_ref(f); // no decay:
// template instantiated with T = double (int)
对于指针,带有显式参数的函数模板的行为就像普通函数一样:
double f(double x)
{
return x+1;
}
template <typename T>
T g(T x)
{
return x+1;
}
typedef double (*FUNC_T)(double);
FUNC_T f1 = f;
FUNC_T f2 = g<double>;
然而,如果它们是类模板的成员,并且它们的上下文依赖于一个尚未指定的参数,那么它们需要在它们的名字 15 之前有一个额外的模板关键字:
template <typename X>
struct outer
{
template <typename T>
static T g(T x)
{
return x+1;
}
};
template <typename X>
void do_it()
{
FUNC_T f1 = outer<X>::g<double>; // error!
FUNC_T f2 = outer<X>::template g<double>; // correct
}
内部模板类需要 typename 和 template:
template <typename X>
struct outer
{
template <typename T>
struct inner {};
};
template <typename X>
void do_it()
{
typename outer<X>::template inner<double> I;
}
一些编译器在这方面并不严谨。
1.1.5.非模板基类
如果一个类模板有不依赖于它的参数的成员,将它们移动到一个普通的类中可能会很方便:
template <typename T>
class MyClass
{
double value_;
std::string name_;
std::vector<T> data_;
public:
std::string getName() const;
};
应该变成:
class MyBaseClass
{
protected:
~MyBaseClass() {}
double value_;
std::string name_;
public:
std::string getName() const;
};
template <typename T>
class MyClass : MyBaseClass
{
std::vector<T> data_;
public:
using MyBaseClass::getName;
};
派生可以是公共的、私有的,甚至是受保护的。 16 这将降低编译复杂度,并潜在地减少二进制代码的大小。当然,如果模板被实例化多次,这种优化是最有效的。
1.1.6.模板位置
类/函数模板的主体必须在每个实例化点对编译器可用,所以通常的头文件/cpp 文件分离不成立,所有内容都打包在一个文件中,扩展名为 hpp。
如果只有声明可用,编译器将使用它,但链接器将返回错误:
// sq.h
template <typename T>
T sq(const T& x);
// sq.cpp
template <typename T>
T sq(const T& x)
{
return x*x;
}
// main.cpp
#include "sq.h" // note: function body not visible
int main()
{
double x = sq(3.14); // compiles but does not link
如果您只想发布模板的一些实例,单独的头文件会很有用。例如,sq 的作者可能希望分发带有 sq
在 C++ 中,可以在不使用模板实体的情况下,在翻译单元中显式地强制实例化模板实体。这是通过特殊语法实现的:
template class X<double>;
template double sq<double>(const double&);
将这一行添加到 sq.cpp 将“导出”sq
这个特性通常与算法标签一起使用。假设你有一个函数模板,比如加密或压缩,它的算法细节必须保密。模板参数 T 代表一个小集合中的一个选项(比如 T =快速、正常、最好);显然,该算法的用户不应该添加他们自己的选项,所以您可以强制实例化少量的实例——加密
注意 C++0x 给语言增加了模板的外部实例化。如果在 template 之前使用关键字 extern,编译器将跳过实例化,链接器将从另一个翻译单元借用模板体。
另见下文第 1.6.1 节。
1.2.专门化和论证演绎
根据定义,当名称出现在名称空间、类或函数体的花括号之间时,我们说名称在 名称空间级别 为,在类级别* ,或在主体级别*,如下例所示:
class X // here, X is at namespace level
{
public:
typedef double value_type; // value_type is at class level
X(const X& y) // both X and y are at class level
{
}
void f() // f is at class level
{
int z = 0; // body level
struct LOCAL {}; // LOCAL is a local class
}
};
函数模板——成员或非成员——可以通过查看它们的参数列表自动推导出模板参数。大致来说, 17 编译器会挑选与参数最匹配的专用函数。如果可行,最好是完全匹配,但也可能发生转换。
如果你可以用调用 G 来代替对 F 的任何调用,那么函数 F 比 G 更专门化(在相同的参数上),但反之则不然。此外,非模板函数被认为比同名模板函数更专门化。
有时候超载和特殊化看起来非常相似:
template <typename scalar_t>
inline scalar_t sq(const scalar_t& x); // (1) function template
inline double sq(const double& x); // (2) overload
template <>
inline int sq(const int& x); // (3) specialization of 1
但它们并不完全相同;考虑以下反例:
inline double sq(float x); // ok, overloaded sq may
// have different signature
template <> // error: invalid specialization
inline int sq(const int x); // it must have the same signature
重载和专门化之间的基本区别在于,函数模板充当单个实体,而不管它有多少个专门化。例如,紧接在(3)之后的调用 sq(y)将迫使编译器在实体(1)和(2)之间进行选择。如果 y 是 double,那么(2)是首选,因为它是一个正常的函数;否则,(1)基于 y 的类型被实例化:只有在这一点上,如果 y 碰巧是 int,编译器才会注意到 sq 有一个专门化并选择(3)。
请注意,两个不同的模板可能会占据主导地位:
template <typename T>
void f(const T& x)
{
std::cout << "I am f(reference)";
}
或者:
template <typename T>
void f(const T* x)
{
std::cout << "I am f(pointer)";
}
另一方面,当存在重载模板时,编写专门化可能需要您显式指定参数:
template <typename T> void f(T) {}
template <typename T> void f(T*) {}
template <>
void f(int*) // ambiguous: may be the first f with T=int*
{} // or the second with T=int
template <>
void f<int>(int*) // ok
{}
记住,模板专门化只在名称空间级别是合法的(即使大多数编译器会容忍它):
class mathematics
{
template <typename scalar_t>
inline scalar_t sq(const scalar_t& x) { ... }; // template member function
template <>
inline int sq(const int& x) { ... }; // illegal specialization!
};
标准的方法是从类内部调用全局函数模板:
// global function template: outside
template <typename scalar_t>
inline scalar_t gsq(const scalar_t& x) { ... };
// specialization: outside
template <>
inline int gsq(const int& x) { ... };
class mathematics
{
// template member function
template <typename scalar_t>
inline scalar_t sq(const scalar_t& x)
{
return gsq(x);
}
};
有时您可能需要显式指定模板参数,因为它们与函数实参无关(实际上,它们被称为非 可演绎 ):
class crc32 { ... };
class adler { ... };
template <typename algorithm_t>
size_t hash_using(const char* x)
{
// ...
}
size_t j = hash_using<crc32>("this is the string to be hashed");
在这种情况下,您必须首先放置不可推导的类型和参数,这样编译器就可以计算出所有剩余的类型和参数:
template <typename algorithm_t, typename string_t>
int hash_using(const string_t& x);
std::string arg("hash me, please");
int j = hash_using<crc32>(arg); // ok: algorithm_t is crc32
// and string_t is std::string
参数演绎显然只对函数模板成立,对类模板不成立。
明确地提供一个论点,而不是依赖于演绎,通常不是一个好主意,除非在一些特殊的情况下,这将在下面描述。
-
必要时进行消歧 :
template <typename T> T max(const T& a, const T& b) { ... } int a = 7; long b = 6; long m1 = max(a, b); // error: ambiguous, T can be int or long long m2 = max<long>(a, b); // ok: T is long
-
当类型不可演绎时 18 :
template <typename T> T get_random() { ... } double r = get_random<double>();
-
当你想让一个函数模板看起来像一个内置的 C++ cast 操作符 :
template <typename X, typename T> X sabotage_cast(T* p) { return reinterpret_cast<X>(p+1); } std::string s = "don't try this at home"; double* p = sabotage_cast<double*>(&s);
-
同时执行造型和函数模板调用 :
double y = sq<int>(6.28) // casts 6.28 to int, then squares the value
-
当一个算法有一个参数,它的默认值是依赖于模板的(通常是一个函子) 19 :
template <typename LESS_T> void nonstd_sort (..., LESS_T cmp = LESS_T()) { // ... } // call function with functor passed as template argument nonstd_sort< std::less<...> > (...); // call function with functor passed as value argument nonstd_sort (..., std::less<...>());
一个模板名(比如 std::vector)和它生成的类名(比如 std::vector < int >)不一样。在类级别,它们是等效的:
template <typename T>
class something
{
public:
something() // ok: don't write something<T>
{
// at local level, 'something' alone is illegal
}
something(const something& that); // ok: 'something&' stands for
// 'something<T>&'
template <typename other_t>
something(const something<other_t>& that)
{
}
};
通常,没有尖括号的单词 something 单独代表一个模板,它本身就是一个定义明确的实体。在 C++ 中,有模板——模板参数 。您可以声明一个模板,其参数不仅是类型,而且是匹配给定模式的类模板:
template <template <typename T> class X>
class example
{
X<int> x1_;
X<double> x2_;
};
typedef example<something> some_example; // ok: 'something' matches
注意,class 和 typename 在这里是不等价的:
template <template <typename T> typename X> // error
类模板可以全部或部分专门化。在通用模板之后,我们列出了专用版本:
// in general T is not a pointer
template <typename T>
struct is_a_pointer_type
{
static const int value = 1;
};
// 2: full specialization for void*
template <>
struct is_a_pointer_type<void*>
{
static const int value = 2;
};
// 3: partial specialization for all pointers
template <typename X>
struct is_a_pointer_type<X*>
{
static const int value = 3;
};
int b1 = is_a_pointer_type<int*>::value; // uses 3 with X=int
int b2 = is_a_pointer_type<void*>::value; // uses 2
int b3 = is_a_pointer_type<float>::value; // uses the general template
部分专门化可以是递归的:
template <typename X>
struct is_a_pointer_type<const X>
{
static const int value = is_a_pointer_type<X>::value;
};
下面的例子被称为*指针悖论* :
#include <iostream>
template <typename T>
void f(const T& x)
{
std::cout << "My arg is a reference";
}
template <typename T>
void f(const T* x)
{
std::cout << " My arg is a pointer";
}
事实上,下面的代码会像预期的那样打印出来:
const char* s = "text";
f(s);
f(3.14);
My arg is a pointer
My arg is a reference
现在改为写:
double p = 0;
f(&p);
你应该读指针;取而代之的是,你得到了对第一个重载的调用。编译器是正确的,因为类型 double通过一个普通的隐式转换(即添加 const-ness)匹配 const T,但它完美地匹配 const T T&,设置 T=double*。
1.2.1.扣除
函数模板可以推导出它们的参数,将参数类型与它们的签名相匹配:
template <typename T>
struct arg;
template <typename T>
void f(arg<T>);
template <typename X>
void g(arg<const X>);
arg<int*> a;
f(a); // will deduce T = int*
arg<const int> b;
f(b); // will deduce T = const int
g(b); // will deduce X = int
演绎也包括非类型参数:
template < int I>
struct arg;
template <int I>
arg<I+1> f(arg<I>);
arg<3> a;
f(a); // will deduce I=3 and thus return arg<4>
但是,请记住,演绎是通过“模式匹配”完成的,编译器不需要执行任何类型的代数 20 :
// this template is formally valid, but deduction will never succeed...
template <int I>
arg<I> f(arg<I+1>)
{
// ...
}
arg<3> a;
f(a); // ...the compiler will not solve the equation I+1==3
arg<2+1> b;
f(b); // ...error again
No matching function for call to 'f'
Candidate template ignored: couldn't infer template argument 'I'
另一方面,如果一个类型包含在一个类模板中,那么它的上下文(外部类的参数)不能被推导出来:
template <typename T>
void f(typename std::vector<T>::iterator);
std::vector<double> v;
f(v.begin()); // error: cannot deduce T
注意,这个错误不依赖于特定的调用。这种推演在逻辑上是不可能的;t 可能不是唯一的。
template <typename T>
struct A
{ typedef double type; };
// if A<X>::type is double, X could be anything
可以添加伪参数来加强一致性:
template <typename T>
void f(std::vector<T>&, typename std::vector<T>::iterator);
编译器将从第一个参数中推导出 T,然后验证第二个参数的类型是否正确。
您也可以在调用函数时显式提供 T 的值:
template <typename T>
void f(typename std::vector<T>::iterator);
std::vector<double> w;
f<double>(w.begin());
经验表明,最好尽量少用没有推导出参数的函数模板。自动推导通常会给出更好的错误信息和更容易的函数查找;以下部分列出了一些常见情况。
首先,当使用模板语法调用函数时,编译器不一定要寻找模板。这可能会产生模糊的错误消息。
struct base
{
template <int I, typename X> // template, where I is non-deduced
void foo(X, X)
{
}
};
struct derived : public base
{
void foo(int i) // not a template
{
foo<314>(i, i); // line #13
}
};
1>error: 'derived::foo': function call missing argument list; use '&derived::foo' to create a pointer to member
1>error: '<' : no conversion from 'int' to 'void (__cdecl derived::* )(int)'
1> There are no conversions from integral values to pointer-to-member values
1>error: '<' : illegal, left operand has type 'void (__cdecl derived::* )(int)'
1>warning: '>' : unsafe use of type 'bool' in operation
1>warning: '>' : operator has no effect; expected operator with side-effect
当编译器遇到 foo <314>时,它会寻找任何 foo。derived 中的第一个匹配是 void foo(int ),查找停止。于是,foo <314>被曲解为(普通函数名) (少)(314)(大)。代码应该显式指定 base::foo。
第二,如果名称查找成功并有多个结果,则显式参数约束重载决策:
template <typename T>
void f();
template <int N>
void f();
f<double>(); // invokes the first f, as "double" does not match "int N"
f<7>(); // invokes the second f
然而,这可能会引起意想不到的麻烦,因为有些过载 21 可能会被默默忽略:
template <typename T>
void g(T x);
double pi = 3.14;
g<double>(pi); // ok, calls g<double>
template <typename T>
void h(T x);
void h(double x);
double pi = 3.14;
h<double>(pi); // unexpected: still calls the first h
这是另一个例子:
template <int I>
class X {};
template <int I, typename T>
void g(X<I>, T x);
template <typename T> // a special 'g' for X<0>
void g(X<0>, T x); // however, this is g<T>, not g<0,T>
double pi = 3.14;
X<0> x;
g<0>(x, pi); // calls the first g
g(x, pi); // calls the second g
最后但并非最不重要的一点是,旧编译器过去常常会引入微妙的链接器错误(比如调用错误的函数)。
1.2.2.特化作用
模板专门化只在名称空间级别有效 22 :
struct X
{
template <typename T>
class Y
{};
template <> // illegal, but usually tolerated by compilers
class Y<double>
{};
};
template <> // legal
class X::Y<double>
{
};
编译器只有在编译了专用版本之后才会开始使用它:
template <typename scalar_t>
scalar_t sq(const scalar_t& x)
{ ... }
struct A
{
A(int i = 3)
{
int j = sq(i); // the compiler will pick the generic template
}
};
template <>
int sq(const int& x) // this specialization comes too late, compiler gives error
{ ... }
但是,在这种情况下,编译器会给出一个错误(声明专门化在实例化之后)。顺便提一下,泛型类模板可能会明确地“提到”一个特例,作为某个成员函数中的一个参数。下面的代码实际上导致了前面提到的编译器错误。
template <typename T>
struct C
{
C(C<void>)
{
}
};
template <>
struct C<void>
{
};
正确的版本使用了正向声明 :
template <typename T>
struct C;
template <>
struct C<void>
{
};
template <typename T>
struct C
{
C(C<void>)
{
}
};
注意,您可以使用整数模板参数来部分专门化(并且您会经常这样做):
// general template
template <typename T, int N>
class MyClass
{ ... };
// partial specialization (1) for any T with N=0
template <typename T>
class MyClass<T, 0>
{ ... };
// partial specialization (2) for pointers, any N
template <typename T, int N>
class MyClass<T*, N>
{ ... };
然而,这种方法可能会引入歧义:
MyClass<void*, 0> m; // compiler error:
// should it use specialization (1) or (2)?
通常你必须明确列出所有的“组合”。如果你为所有 T1 ∈ A 和所有 T2 ∈ B 专门化 X
// partial specialization (3) for pointers with N=0
template <typename T>
class MyClass<T*, 0>
{ ... };
当通用模板中的模板参数之间存在依赖关系时,编写部分专门化是非法的。
// parameters (1) and (2) are dependent in the general template
template <typename int_t, int_t N>
class AnotherClass
{};
template <typename T>
class AnotherClass<T, 0>
{};
error: type 'int_t' of template argument '0' depends on template parameter(s)
只允许完全专门化:
template <>
class AnotherClass<int, 0>
{};
类模板特殊化可能与通用模板完全无关。它不需要有相同的成员,成员函数可以有不同的签名。
虽然不必要的界面改变是不良风格的征兆(因为它禁止对对象的任何一般操作),但这种自由通常可以被利用:
template <typename T, int N>
struct base_with_array
{
T data_[N];
void fill(const T& x)
{
std::fill_n(data_, N, x);
}
};
template <typename T>
struct base_with_array<T, 0>
{
void fill(const T& x)
{
}
};
template <typename T, size_t N>
class cached_vector : private base_with_array<T, N>
{
// ...
public:
cached_vector()
{
this->fill(T());
}
};
1.2.3.内部类模板
一个类模板可以是另一个模板的成员。其中一个重点是语法;内部类有自己的一组参数,但它知道外部类的所有参数。
template <typename T>
class outer
{
public:
template <typename X>
class inner
{
// use freely both X and T
};
};
如果 T 是明确定义的类型,则访问 inner 的语法是 outer
outer<int>::inner<double> a; // correct
template <typename Y>
void f()
{
outer<Y>::inner<double> x1; // error
outer<Y>::template inner<double> x1; // correct
}
通常很难或者不可能专门化内部类模板。专门化应该在 outer 之外列出,所以通常它们需要两个模板<...>子句,前者用于 T (outer),后者用于 X (inner)。
| 主模板:它定义了一个内部的
| outer 的完全专门化可能包含一个 inner
| inner_2 可以是专用的: | 模板<>类外部
| 固定 T (=double)和一般 x 的 inner_1 的特化。 | 模板<>模板
| inner_1 对于固定 T (=double)和固定 X (=char)的特化。 | 模板<>模板<>类外部
| 用任意 t 对固定 X 进行 inner_1 特殊化是非法的。 | 模板
注意,即使 X 相同,inner_1
template <typename T>
struct outer
{
template <typename X> struct inner {};
};
template <>
struct outer<int>
{
template <typename X> struct inner {};
};
int main()
{
outer<double>::inner<void> I1;
outer<int>::inner<void> I2;
I1 = I2;
}
error: binary '=' : no operator found which takes a right-hand operand of type 'outer<int>::inner<X>' (or there is no acceptable conversion)
比方说,不可能编写一个函数来测试任意两个“inner”是否相等,因为给定 inner
template <typename T, typename X>
bool f(outer<T>::inner<X>); // error: T cannot be deduced?
变量 I1 的实际类型不是简单的内
struct basic_inner
{
};
template <typename T>
struct outer
{
typedef basic_inner inner;
};
template <>
struct outer<int>
{
typedef basic_inner inner;
};
如果 inner 不依赖于 T,你可以写 23 :
template <typename X>
struct basic_inner
{
};
template <typename T>
struct outer
{
template <typename X>
struct inner : public basic_inner<X>
{
inner& operator=(const basic_inner<X>& that)
{
static_cast<basic_inner<X>&>(*this) = that;
return *this;
}
};
};
template <>
struct outer<int>
{
template <typename X>
struct inner : public basic_inner<X>
{
inner& operator=(const basic_inner<X>& that)
{
static_cast<basic_inner<X>&>(*this) = that;
return *this;
}
};
};
否则,您必须设计支持混合操作的 basic_inner 模板操作符:
template <typename X, typename T>
struct basic_inner
{
template <typename T2>
basic_inner& operator=(const basic_inner<X, T2>&)
{ /* ... */ }
};
template <typename T>
struct outer
{
template <typename X>
struct inner : public basic_inner<X, T>
{
template <typename ANOTHER_T>
inner& operator=(const basic_inner<X, ANOTHER_T>& that)
{
static_cast<basic_inner<X, T>&>(*this) = that;
return *this;
}
};
};
template <>
struct outer<int>
{
template <typename X>
struct inner : public basic_inner<X, int>
{
template <typename ANOTHER_T>
inner& operator=(const basic_inner<X, ANOTHER_T>& that)
{
static_cast<basic_inner<X, int>&>(*this) = that;
return *this;
}
};
};
int main()
{
outer<double>::inner<void> I1;
outer<int>::inner<void> I2;
I1 = I2; // ok: it ends up calling basic_inner::operator=
}
这在 C++ 社区中被称为可怕的初始化。 24
吓人代表“看似错误(受冲突的模板参数约束),但实际上与正确的实现一起工作”。简单地说,两个应该不同的内部类型(具体来说,外部< T1 >:“内部”和外部< T2 >:“内部”)实际上共享了实现,这意味着可以将它们统一视为“两个内部”。
正如您看到的函数模板,在编译器满足所有专门化之前,您永远不应该实例化主模板。如果只使用完全专门化,编译器会识别出问题并停止。太迟的部分专业化将被忽略:
struct A
{
template <typename X, typename Y>
struct B
{
void do_it() {} // line #1
};
void f()
{
B<int,int> b; // line #2: the compiler instantiates B<int,int>
b.do_it();
}
};
template <typename X>
struct A::B<X, X> // this should be a specialization of B<X,X>
// but it comes too late for B<int,int>
{
void do_it() {} // line #3
};
A a;
a.f(); // calls do_it on line #1
此外,添加 B 的完全专门化将触发编译器错误:
template <>
struct A::B<int, int>
{
void do_it() {}
};
error: explicit specialization; 'A::B<X,Y>' has already been instantiated
with
[
X=int,
Y=int
]
显而易见的解决方案是在 A::B 的专门化之后移动函数体。
1.3.风格惯例
风格是编写代码的方式;这个定义非常模糊,它包括了编程的许多不同方面,从语言技术到花括号的位置。
名称空间 std 中的所有 C++ 对象都展示了一种通用的风格,这使得库更加一致。
比如所有的名字都是小写 25 多字名字用下划线。容器有一个测试对象是否为空的成员函数 bool T::empty() const 和一个使容器为空的 void T::clear() 。这些都是风格的元素。
一个用纯 C 写的虚构的 STL 可能有一个全局函数 clear,为所有可能的容器重载。编写像 cont.clear()或 clear(&cont)这样的代码对 cont 有相同的净影响,甚至可能生成相同的二进制文件,但是,它有非常不同的风格。
所有这些方面在代码评审期间都很重要。如果风格与读者的格式一致,代码看起来自然清晰,维护也更容易。
风格的某些方面确实不太重要,因为它们很容易调整。例如,使用美化器——团队中的每个工作人员都可能在他的机器上有一个预配置的美化器,它与代码编辑器集成在一起,可以快速地重新格式化大括号、空格和换行符。
注意 JEdit(参见www.jedit.org
)是一个支持插件的免费多平台代码编辑器。
AStyle(艺术风格)是一个命令行开源代码美化器(见astyle.sourceforge.net
),其首选项包括最常见的格式化选项(见图 1-1 )。
图 1-1 。JEdit 的风格插件
大多数合理的风格约定是等价的;重要的是选择一个,并尝试在一段时间内保持一致。 26
理想情况下,如果代码是根据一些常见的行为惯例编写的,读者可能会根据风格推断出它是如何工作的,而不需要查看细节。
例如:
void unknown_f(multidimensional_vector<double, 3, 4>& M)
{
if (!M.empty())
throw std::runtime_error("failure");
}
大多数读者会把这个片段描述为“如果多维向量不为空,那么抛出一个异常”。然而,除了样式之外,代码中没有任何内容表明这是预期的行为。
事实上,多维向量::empty 原则上可以使容器为空,如果不成功,则返回一个非零错误代码。 27
命名惯例是风格的一个重要组成部分。
下面的例子列出了在构建对象名称时如何传达额外含义的一些想法。它不是一套公理,特别是没有一项比它的对立面更差/更好,但它是一个如何组合一种风格的详细示例,可以帮助您诊断和解决问题。
请记住,C++ 标准规定,有些标识符是“为任何用途的实现保留的”,有些是为全局或 std 名称空间中的名称保留的。这意味着用户名永远不应该:
- 以下划线开头(特别是后面跟着一个大写字母)
- 包含双下划线
- 包含一个美元符号(一些编译器可以接受,但是不可移植)
1.3.1.评论
许多好的编程实践都可以归结为为变化做准备或表达意图。新手强调前者,专家强调后者。”
约翰·库克
记得在你的代码中添加大量的注释。如果这对于任何编程语言都是有效的,那么对于 TMP 技术尤其如此,这很容易被误解。TMP 的正确行为基于奇怪的实体,比如空类、void 函数和看起来像错误的奇怪语言结构。对于代码的作者来说,要记住这些技术为什么以及如何工作真的很难,对于其他必须维护代码的人来说就更难了。
1.3.2.宏指令
宏在 TMP 中起着特殊的作用。一些程序员认为它们是必要的邪恶,事实上它们是必要的,但是它们也是邪恶的并不明显。
宏必须:
- 让读者认识到它们
- 防止名称冲突
满足这两个要求的最简单的方法是为所有宏选择一个唯一且足够难看的公共前缀,并使用小写/大写字母赋予名称额外的含义。
例如,您可能同意所有宏都以 MXT_ 开头。如果宏是持久的,即从不未定义,前缀将是 MXT 。如果宏的作用域是有限的(在同一个文件中它被定义和未定义),前缀将是 mXT_。
#ifndef MXT_filename_
#define MXT_filename_ // this is "exported" – let's name it MXT_*
#define mXT_MYVALUE 3 // this macro has limited "scope"
const int VALUE = mXT_MYVALUE; // let's name it mXT_*
#undef mXT_MYVALUE //
#endif //MXT_filename_
小写前缀 mxt 用于重新映射不同平台中的标准/系统函数名:
#ifdef _WIN32
#define mxt_native_dbl_isfinite _finite
#else
#define mxt_native_dbl_isfinite isfinite
#endif
为了获得更好的代码外观,您可以决定用宏替换一些关键字:
#define MXT_NAMESPACE_BEGIN(x) namespace x {
#define MXT_NAMESPACE_END(x) }
#define MXT_NAMESPACE_NULL_BEGIN() namespace {
#define MXT_NAMESPACE_NULL_END() }
和/或将名称空间指令包含在 ASCII-art 注释框中:
/////////////////////////////////////////////////////////////////
MXT_NAMESPACE_BEGIN(XT)
/////////////////////////////////////////////////////////////////
将一些(整数)函数作为一组宏是有用的:
#define MXT_M_MAX(a,b) ((a)<(b) ? (b) : (a))
#define MXT_M_MIN(a,b) ((a)<(b) ? (a) : (b))
#define MXT_M_ABS(a) ((a)<0 ? -(a) : (a))
#define MXT_M_SQ(a) ((a)*(a))
infix M 代表“宏”,在使用模板时会很有用:
template <int N>
struct SomeClass
{
static const int value = MXT_M_SQ(N)/MXT_M_MAX(N, 1);
};
注c++ 11 标准引入了一个新的关键字:constexpr 。 28
函数声明的 constexpr 没有副作用,它总是从相同的参数返回相同的结果。特别是,当使用编译时常量参数调用这样的函数时,它的结果也将是一个编译时常量:
constexpr int sq(int n) { return n*n; }
constexpr int max(int a, int b)
{ return a<b ? b : a; }
template <int N>
struct SomeClass
{
static const int value = sq(N)/max(N, 1);
最后,考虑一类特殊的宏。一个 宏指令 是一个宏,其用法逻辑上需要一整行代码。
换句话说,普通宏和指令的区别在于,后者不能与同一行上的任何东西共存(可能除了它的参数):
// directive
MXT_NULL_NAMESPACE_BEGIN()
#define MXT_PI 3.1415926535897932384626433832795029
// the use of MXT_PI does not take the whole line
// so it is not a directive.
const double x = std::cos(MXT_PI);
// directive
MXT_NULL_NAMESPACE_END()
一般来说,宏指令的定义不应该以分号结束,所以用户被迫手动关闭该行(在适当的时候),就像它是一个标准的函数调用一样。
// note: no trailing ';'
#define MXT_INT_I(k) int i = (k)
int main()
{
MXT_INT_I(0); // put ';' here
return 0;
}
这里有一个更复杂的例子。注意,结尾的分号是一个非常强的样式点,所以它甚至用在普通代码中分号不自然的地方。
#define mXT_C(NAME,VALUE) \
static scalar_t NAME() \
{ \
static const scalar_t NAME##_ = (VALUE); \
return NAME##_; \
}
template <typename scalar_t>
struct constant
{
// the final ';' at class level is legal, though uncommon
mXT_C(Pi, acos(scalar_t(-1)));
mXT_C(TwoPi, 2*acos(scalar_t(-1)));
mXT_C(PiHalf, acos(scalar_t(0)));
mXT_C(PiQrtr, atan(scalar_t(1)));
mXT_C(Log2, log(scalar_t(2)));
};
#undef mXT_C
double x = constant<double>::TwoPi();
但是,调用宏指令时需要特别小心,宏指令会扩展为一系列指令:
#define MXT_SORT2(a,b) if ((b)<(a)) swap((a),(b))
#define MXT_SORT3(a,b,c) \
MXT_SORT2((a),(b)); MXT_SORT2((a),(c)); MXT_SORT2((b),(c))
int a = 5, b = 2, c = 3;
MXT_SORT3(a,b,c); // apparently ok: now a=2, b=3, c=5
然而,这个代码被破坏了:
int a = 5, b = 2, c = 3;
if (a>10)
MXT_SORT3(a,b,c); // problem here!
因为它扩展到:
if (a>10)
MXT_SORT2(a,b);
MXT_SORT2(a,c);
MXT_SORT2(b,c);
更令人惊讶的是,下面的片段很清楚,但不正确:
if (a>10)
MXT_SORT2(a,b);
else
MXT_SORT2(c,d);
由于 if-then-else 在 C++ 中的关联方式,宏扩展为
if (a>10)
if (a<b)
swap(a,b);
else
if (c<d)
swap(c,d);
缩进不像代码执行的方式;该块实际上分为
if (a>10)
{
if (a<b)
swap(a,b);
else if (c<d)
swap(c,d);
}
要解决这个问题,您可以使用 do {...} while(假)成语:
#define MXT_SORT3(a,b,c) \
do { MXT_SORT2((a),(b)); MXT_SORT2((a),(c)); MXT_SORT2((b),(c)); } \
while (false)
这既允许将“本地代码”放入块中,也允许用分号结束指令。
请记住,这不会让您免于类似以下的错误:
MXT_SORT3(a, b++, c); // error: b will be incremented more than once
这就是为什么我们坚持认为宏可以通过一个“足够难看”的前缀立即识别出来。
要解决“如果”的宏问题,编写一个不做其他事情的分支:
#define MXT_SORT2(a,b) if ((b)<(a)) swap((a),(b)); else
现在 MXT_SORT2(a,b);扩展到 if(...)互换(...);否则;其中最后一个分号是空语句。更好的 29 :
#define MXT_SORT2(a,b) if (!((b)<(a))) {} else swap((a),(b))
最后一点,永远不要直接使用来自宏的类型。总是引入 typedef。如果没有仔细编写宏,*和 const 之间的关联可能会产生意外的结果。考虑一下:
T x = 0;
const T* p = &x; // looks correct
除非:
#define T char*
相反,考虑截取宏:
typedef T MyType; // ok, even if T is a macro.
// #undef T if you like
MyType x = 0; //
const MyType* p = &x; // now it works.
1.3.3.符号
大多数 C++ 项目包含几种符号(类、函数、常数等等)。可以在系统/框架实用程序(完全抽象和通用)和项目特定实体(包含特定逻辑并且不期望在其他地方重用)之间画一条粗略的分界线。
这个简单的分类可能对(人类)调试器很重要。如果任何一段代码被认为是“系统实用程序”,那么它是绝对可信的,在调试过程中通常会被“忽略”。另一方面,特定于项目的代码可能较少被测试,应该“介入”。
我们可以同意稳定符号应该遵循 STL 命名约定(小写,下划线,比如 stable_sort,hash_map 等等)。这通常是类模板的情况。
剩下的应该是 camel case(Java 约定就可以了)。
(framework header) sq.hpp
template <typename scalar_t>
scalar_t sq(const scalar_t& x) { return x*x; }; // 'system-level' function – lowercase
(project file) custom_scalar.h
struct MySpecialScalarType // 'project-level' class – mixed case
{
// ...
};
(project file) main.cpp
int main()
{
MySpecialScalarType x = 3.14;
MySpecialScalarType y = sq(x);
return 0;
}
一个仿函数 是一个对象的实例,它实现了至少一个运算符(),因此实例的名字就像一个函数。 30
如果函子通过非常数引用接受参数,则称其为修改。
一个谓词是一个非修改函子,它接受相同类型的所有参数并返回一个布尔值。例如,less 是一个二元谓词:
template <typename T>
struct less
{
bool operator()(const T&, const T&) const;
};
大多数仿函数包含运算符()的返回类型的 typedef,通常命名为 result_type 或 value_type。 31
函子通常是无状态的,或者它们携带很少的数据成员,所以它们是动态构建的。有时,您可能需要一个有意义的实例名,但这可能不太容易,因为如果仿函数有一个有限的“作用域”,那么唯一有意义的名称已经给了类。
calendar myCal;
std::find_if(year.begin(), year.end(), is_holiday(myCal));
// is_holiday is a class
// how do we name an instance?
您可以使用下列选项之一:
-
对于实例:
calendar myCal; is_holiday IS_HOLIDAY(myCal); std::find_if(year.begin(), year.end(), IS_HOLIDAY);
,使用小写的仿函数名并将其转换为大写
-
使用带有前缀/后缀的小写仿函数名,并在实例中删除它:
calendar myCal; is_holiday_t is_holiday(myCal); std::find_if(year.begin(), year.end(), is_holiday);
1.3.4.概括性
提高通用性的最好方法是重用标准类,比如 std::pair。
这带来了经过良好测试的代码,增加了互操作性;但是,它可能经常隐藏一些特定的逻辑,例如 pair::first 和 pair::second 的含义可能乍一看并不明显。请看下面的典型例子:
struct id_value
{
int id;
double value;
};
id_value FindIDAndValue(...);
这可以替换为:
std::pair<int, double> FindIDAndValue(...)
但是,第一个函数的调用者可以编写 p.id 和 p.value,这比 p.first 和 p.second 更容易阅读。您可能希望提供一种不太通用的方法来访问 pair 成员:
-
宏指令
#define id first // bad idea? #define value second // bad idea? #define id(P) P.first // slightly better #define value(P) P.second // slightly better
-
全局函数(这些被称为访问器;参见第 6.2.1 节
inline int& id(std::pair<int, double>& P) { return P.first; } inline int id(const std::pair<int, double>& P) { return P.first; }
-
成员的全局指针
typedef std::pair<int, double> id_value; int id_value::*ID = &id_value::first; double id_value::*VALUE = &id_value::second; // later std::pair<int, double> p; p.*ID = -5; p.*VALUE = 3.14;
要使 ID 和值成为常量,语法是:
int id_value::* const ID = &id_value::first;
1.3.5.模板参数
一个相当普遍接受的惯例是为非类型模板参数保留大写名称。这可能会导致与宏的某些名称冲突。并不总是需要给模板参数起一个名字(就像函数参数一样),所以如果可行的话,最好完全去掉这个名字:
// the following line is likely to give strange errors
// since some compilers define BIGENDIAN as a macro!
template <typename T, bool BIGENDIAN = false>
class SomeClass
{
};
template <typename T>
class SomeClass<T, true>
{
};
更安全的声明应该是 32 :
template <typename T, bool = false>
class SomeClass
类型参数通常由一个大写字母表示,通常是 T(或 T1,T2...)如果类型确实可以是任何东西。 33 A 和 R 传统上也用于匹配自变量和结果的参数:
int foo(double x) { return 5+x; }
template <typename R, typename A>
inline R apply(R (*F)(A), A arg)
{
return F(arg);
}
template <typename R, typename A1, typename A2>
inline R apply(R (*F)(A1, A2), A1 arg1, A2 arg2)
{
return F(arg1, arg2);
}
double x = apply(&foo 3.14);
否则,您可能希望使用以 _t 结尾的(有意义的)小写名称(例如,int_t、scalar_t、object_t、any_t 或 that_t)。
template <typename T, int N>
class do_nothing
{
};
template <typename int_t> // int_t should behave as an integer type<sup class="calibre7">34</sup>
struct is_unsigned
{
static const bool value = ...;
};
后缀 _t 在 C 语言中最初的意思是 typedef,它也广泛用于代表模板实例的(私有)typedef:
template <typename scalar_t>
class SomeContainer
{
// informally means:
// within this class, a pair always denotes a pair of scalars
private:
typedef std::pair<scalar_t, scalar_t> pair_t;
};
另一方面,公共 typedef 名称通常由小写的常规英语单词组成(例如 iterator_category)。在这种情况下,_type 是首选:
template <typename scalar_t>
class SomeContainer
{
public:
typedef scalar_t result_type;
};
1.3.6.元函数
我们经常会遇到无状态的类模板,它的成员只有枚举(通常是匿名的)、静态常量、类型(typedefs 或嵌套类)和静态成员函数。
概括第 1.1 节,我们认为这个模板是一个元函数,它将它的参数元组映射到一个类,这个类被看作是一个结果集(即它的成员)。
template <typename T, int N>
struct F
{
typedef T* pointer_type;
typedef T& reference_type;
static const size_t value = sizeof(T)*N;
};
元函数 F 将一对参数映射到三个结果:
| (T,N) | →中 | (指针类型,引用类型,值) |
| {type}×{int} | →中 | {type}×{type}×{size_t} |
大多数元函数要么返回单个类型,即通常命名的类型,要么返回单个数值常量(整数或枚举),即通常命名的值。35
template <typename T>
struct largest_precision_type;
template <>
struct largest_precision_type<float>
{
typedef double type;
};
template <>
struct largest_precision_type<double>
{
typedef double type;
};
template <>
struct largest_precision_type<int>
{
typedef long type;
};
类似地:
template <unsigned int N>
struct two_to
{
static const unsigned int value = (1<<N);
};
template <unsigned int N>
struct another_two_to
{
enum { value = (1<<N) };
};
unsigned int i = two_to<5>::value; // invocation
largest_precision<int>::type j = i + 100; // invocation
历史上,第一个元函数是使用枚举编写的:
template <size_t A>
struct is_prime
{
enum { value = 0 };
};
template <>
struct is_prime<2>
{
enum { value = 1 };
};
template <>
struct is_prime<3>
{
enum { value = 1 };
};
// ...
主要原因是编译器不能处理静态常量整数(包括 bool)。与静态常量相比,使用枚举的优势在于编译器不会为常量保留存储空间,因为计算要么是静态的,要么会失败。
相反,静态常量整数可能被“误用”为普通整数,例如,取其地址(编译器在枚举上不允许的操作)。
注意根据经典的 C++ 标准,将静态常数用作普通整数是非法的(除非该常数在。cpp 文件,作为类的任何其他静态数据成员)。但是,大多数编译器都允许这样做,只要代码不试图获取常量的地址或将它绑定到常量引用。在现代 C++ 中,这一要求被删除了。
此外,该语言允许声明一个静态整数常量(在函数范围内,而不是在类范围内),该常量由动态初始化,因此不是编译时常量:
static const int x = INT_MAX; // static
static const int y = std::numeric_limits<int>::max(); // dynamic
static const int z = rand(); // dynamic
double data[y]; // error
实际上,一个枚举通常相当于一个小整数。除非枚举的值太大,否则它们通常被实现为带符号的 int。最重要的区别是,如果没有显式强制转换,就不能将未命名的枚举绑定到模板参数:
double data[10];
std::fill_n(data, is_prime<3>::value, 3.14); // may give error!
前面的代码是不可移植的,因为可能定义了 std::fill_n。
template <..., typename integer_t, ...>
void fill_n(..., integer_t I, ...)
{
++I; // whatever...
--I; // whatever...
}
error C2675: unary '--' : "does not define this operator or a conversion to a type acceptable to the predefined operator
see reference to function template instantiation
'void std::_Fill_n<double*,_Diff,_Ty>(_OutIt,_Diff,const _Ty &,std::_Range_checked_iterator_tag)' being compiled
with
[
_Diff=,
_Ty=double *,
_OutIt=double **
]
实际上,枚举可以存储小整数(例如,以 2 为底的整数的对数)。因为它的类型不是显式的,所以在处理潜在的大型或无符号常量时应该避免使用它。作为 std::fill_n 调用的变通方法,只需将枚举转换为适当的整数:
std::fill_n(..., int(is_prime<3>::value), ...); // now ok!
通常,元函数会调用助手类(稍后您将会看到更多的例子):
template <int N>
struct ttnp1_helper
{
static const int value = (1<<N);
};
template <int N>
struct two_to_plus_one
{
static const int value = ttnp1_helper<N>::value + 1;
};
辅助变量的道德等价物是私有成员。从 TMP 的角度来看,数值常量和类型(def)是等效的编译时实体。
template <int N>
struct two_to_plus_one
{
private:
static const int aux = (1<<N);
public:
static const int value = aux + 1;
};
helper 类不私有不隐藏, 36 但是不应该使用,所以名字用 _helper 或者 _t(或者两者都用)来“丑化”。
1.3.7.名称空间和使用声明
通常,所有“公共”框架对象都被分组到一个公共的名称空间中,而“私有”对象则位于特殊的嵌套名称空间中。
namespace framework
{
namespace undocumented_private
{
void handle_with_care()
{
// ...
};
}
inline void public_documented_function()
{
undocumented_private::handle_with_care();
}
}
不必要地增加名称空间的数量不是一个好主意,因为依赖于参数的名称查找可能会引入微妙的问题,并且不同名称空间中的对象之间的友元声明是有问题的,甚至是不可能的。
通常,通用元编程框架的核心是一组头文件(扩展名为*。hpp 实际上用于纯 C++ 头文件)。在头文件中使用命名空间声明 通常被认为是不好的做法:
my_framework.hpp
using namespace std;
main.cpp
#include "my_framework.hpp"
// main.cpp doesn't know, but it's now using namespace std
然而,头文件中的 using-function 声明通常是可以的,甚至是可取的(参见本段后面的 do_something 示例)。
using-namespace 声明的一个特殊用途是头文件版本控制。 37
这是一个非常简短的例子:
namespace X
{
namespace version_1_0
{
void func1();
void func2();
}
namespace version_2_0
{
void func1();
void func2();
}
#ifdef USE_1_0
using namespace version_1_0;
#else
using namespace version_2_0;
#endif
}
因此,使用头部的客户端总是引用 X::func1。
现在我们将详细描述另一种情况,在这种情况下使用声明会有所不同。
函数模板通常用于提供“外部接口”,它是一组全局函数,允许算法执行对象的一般操作 38 :
一个虚构框架 1 的作者提供了一个函数 is_empty,它作用于一大类容器和 C 字符串:
// framework1.hpp
MXT_NAMESPACE_BEGIN(framework1)
template <typename T>
inline bool is_empty(T const& x)
{
return x.empty(); // line #1
}
template <>
inline bool is_empty(const char* const& x)
{
return x==0 || *x==0;
}
MXT_NAMESPACE_END(framework1)
这种方法的一个优点是易于扩展。对于任何新的 X 类型,您可以提供一个专用的 is_empty,它将优先于默认实现。但是,考虑一下如果函数被显式限定会发生什么:
// framework2.hpp
#include "framework1.hpp"
MXT_NAMESPACE_BEGIN(framework2)
template <typename string_t>
void do_something(string_t const& x)
{
if (!framework1::is_empty(x)) // line #2
{
// ...
}
}
MXT_NAMESPACE_END(framework2)
#include "framework2.hpp"
namespace framework3
{
class EmptyString
{
};
bool is_empty(const EmptyString& x)
{
return true;
}
}
int main()
{
framework3::EmptyString s;
framework2::do_something(s); // compiler error in line #1
}
第 2 行中用户提供的 is_empty 被忽略,因为 do_something 显式地从名称空间 framework1 中获取 is_empty。要解决这个问题,您可以重新打开名称空间 framework1 并在那里专门化 is_empty,或者修改 do _ 如下所示:
framework2.hpp
MXT_NAMESPACE_BEGIN(framework2)
using framework1::is_empty;
template <typename string_t>
void do_something(string_t const& x)
{
if (!is_empty(x))
{
//...
}
};
因此,您让参数相关的查找选择一个可用的 is_empty,但确保 framework1 可以始终提供一个默认的候选项(也参见 1.4.2 节中的讨论)。
1.4.经典图案
编写框架/库时,通常会使用和重用一小组名称。例如,容器应该有一个成员函数[[integer type]] size() const,它返回元素的数量。
采用统一的风格增加了对象的互操作性;更多详情,请参见第六章。以下所有段落将尝试描述与几个常见 C++ 名称相关的传统含义。
1.4.1.size_t 和 ptrdiff_t
在 C++ 中,没有唯一的标准和可移植的方法来命名大整数。现代编译器通常会为长整型和无符号长整型选择最大的整数。当你快速需要一个大而快的整数时,首选是 size_t (无符号)和 ptrdiff_t (有符号)。
size_t 是 sizeof 和运算符 new 的结果,它足够大,可以存储任意数量的内存;ptrdiff_t 表示两个指针的差。因为字符数组的长度是首尾相连的,所以根据经验,它们的大小是一样的。
此外,在平面 C++ 内存模型中,sizeof(size_t)也将是指针的大小,这些整数可能具有体系结构中的自然大小——比方说,在 32 位处理器上是 32 位,在 64 位处理器上是 64 位。它们也很快(处理器总线将执行从寄存器到存储器的原子传输)。
给定这个类:
template <int N>
struct A
{
char data[N];
};
sizeof(A
1.4.2.void T::swap(T&)
这个函数应该在常数时间内交换this 和参数,而不抛出异常。常数的实际定义是“仅依赖于 T 的时间量”。* 40
如果 T 有一个交换成员函数,用户期望它不会比传统的三份交换差(即 X = A;a = B;B=X)。事实上,这总是可能的,因为成员函数可以调用每个成员自己的交换:
class TheClass
{
std::vector<double> theVector_;
std::string theString_;
double theDouble_;
public:
void swap(TheClass& that);
{
theString_.swap(that.theString_);
theVector_.swap(that.theVector_);
std::swap(theDouble_, that.theDouble_);
}
};
唯一需要不固定时间的步骤是逐个元素地交换动态数组,但这可以通过整体交换数组来避免。
类 std::tr1::array
要解决的第一个问题是如何交换未指定类型的对象:
template <typename T>
class TheClass
{
T theObj_; // how do you swap two objects of type T?
void swap(TheClass<T>& that)
{
std::swap(theObj_, that.theObj_);
}
};
显式限定 std::是一个不必要的约束。你最好引入一个 using 声明,见 1.3.7 节:
using std::swap;
template <typename T>
class TheClass
{
T theObj_;
public:
void swap(TheClass<T>& that) // line #1
{
swap(theObj_, that.theObj_); // line #2
}
};
但是,这会导致编译器错误,因为根据通常的 C++ 名称解析规则,第 2 行中的 swap 是第 1 行中定义的 swap,它没有两个参数。
解决方案是引入一个名字不同的全局函数,这个习惯用法叫做与 ADL 互换:
using std::swap;
template <typename T>
inline void swap_with_ADL(T& a, T& b)
{
swap(a, b);
}
template <typename T>
class TheClass
{
T theObj_;
public:
void swap(TheClass<T>& that)
{
swap_with_ADL(theObj_, that.theObj_);
}
根据查找规则,swap_with_ADL 将调用转发到与 T 在同一个名称空间中定义的交换函数(希望是 T 自己的版本),或者转发到 std::swap(如果不存在其他函数)。由于没有同名的局部成员函数,所以 lookup 会跳过类级别。
互换的传统说法是 T & amp;但是,提供更多的重载可能是有意义的。如果一个对象在内部将其数据保存在 X 类型的标准容器中,那么提供 void swap(X&)可能是有用的,它具有宽松的时间复杂度预期:
template <typename T>
class sorted_vector
{
std::vector<T> data_;
public:
void swap(sorted_vector<T>& that)
{
data_.swap(that.data_);
}
void swap(std::vector<T>& that)
{
data_.swap(that);
std::sort(data_.begin(), data_.end());
}
};
更有 41 :
struct unchecked_type_t {};
inline unchecked_type_t unchecked() { return unchecked_type_t(); }
template <typename T>
class sorted_vector
{
// ...
void swap(std::vector<T>& that, unchecked_type_t (*)())
{
assert(is_sorted(that.begin(), that.end()));
data_.swap(that);
}
};
sorted_vector<double> x;
std::vector<double> t;
load_numbers_into(x);
x.swap(t);
// now x is empty and t is sorted
// later...
x.swap(t, unchecked); // very fast
总而言之:
- 用固定本机类型(整数、指针等)和标准容器(包括字符串)的参数显式限定 std::swap。
- 为 std::swap 编写 using 声明,当参数在全局函数中有未定义的类型 T 时调用非限定交换。
- 在具有交换成员函数的类中调用 swap_with_ADL。
std::swap 提供了交换本机类型和 std 类型的最佳实现。
交换用于具有移动语义的算法中:
void doSomething(X& result)
{
X temp;
// perform some operation on temp, then...
swap(temp, result);
}
以及根据复制构造函数实现异常安全赋值运算符:
class X
{
public:
X(const X&);
void swap(X&);
~X();
X& operator=(const X& that)
{
X temp(that); // if an exception occurs here, *this is unchanged
temp.swap(*this); // no exception can occur here
return *this; // now temp is destroyed and releases resources
}
};
如果执行无条件交换,最有效的解决方案是按值获取参数:
X& operator=(X that)
{
that.swap(*this);
return *this;
}
另一方面,您可能希望在手工调用复制构造函数之前执行额外的检查,即使这样效率更低 42 :
X& operator=(const X& that)
{
if (this != &that)
{
X temp(that);
temp.swap(*this);
}
return *this;
}
缺点是在某些时候,that 和 temp 都是活动的,所以您可能需要更多的空闲资源(例如,更多的内存)。
1.4.3.bool T::empty()const;void T::clear()
前一个函数测试一个对象是否为空;后者使之空虚。如果一个对象有一个成员函数 size(),那么调用 empty()应该不会比 size()==0 慢。
请注意,对象可能是空的,但仍然控制资源。例如,一个空的 vector 可能包含一个原始的内存块,实际上还没有构造任何元素。
特别是,它没有指明一个 clear 函数是否会释放对象资源;clear 是 reset 的同义词。
要对 auto 变量强制进行资源清理,通常的方法是用一个临时的:
T x;
// now x holds some resources...
T().swap(x);
1.4.4.x T::get()const;x T::base()constT3】
当类型 T 包装一个更简单的类型 x 时,使用 get 这个名称。因此,智能指针的 get 将返回内部普通指针。
相反,当包装器只是一个不同的接口时,函数库用于返回被包装对象的副本。因为智能指针通常会增加一些复杂性(例如,引用计数),所以命名基不像 get 那样合适。另一方面,std::reverse_iterator 是一个交换底层迭代器的++ 和-的接口,所以它有一个 base()。
1.4.5.x T::property()constT1】;void T::property(X)
在本节中,“属性”是一个符号名称。一个类可以公开两个名为“property”的重载成员函数,它们有两种不同的意图。
第一种形式返回当前实例的属性的当前值;第二个将属性设置为某个新值。属性集函数也可以具有以下形式:
X T::property(X newval)
{
const X oldval = property();
set_new_val(newval);
return oldval;
}
这种约定很优雅,但并不普遍使用;它存在于 std::iostream 中。
1.4.6.动作(值);动作(范围)
在本节中,“action”也是重载函数或成员函数的符号名。
如果对象自身的操作(例如 container.insert(value))可能会被顺序调用,则对象可能会提供一个或多个范围等效项。换句话说,它可以为成员函数提供两个或多个参数,一次标识一系列元素。一些常见的例子有:
- 一个元素和一个重复计数器
- 两个迭代器指向(begin...结束)
- 一个数组和两个索引
利用预先知道的范围取决于实现。像往常一样,值域等价函数永远不应该比琐碎的实现动作(range):= for(x in range){ action(x);}.
1.4.7.机械手
操纵器是 C++ 标准中最不为人知和最富表现力的部分之一。它们只是将流作为参数的函数。因为它们的签名是固定的,所以流有一个特殊的插入操作符来运行它们:
class ostream
{
public:
ostream& operator<<(ostream& (*F)(ostream&))
{
return F(*this);
}
inline ostream& endl(ostream& os)
{
os << '\n';
return os.flush();
}
};
int main()
{
// actually execute endl(cout << "Hello world")
std::cout << "Hello world" << std::endl;
}
一些操纵者有一个论点。实现可以使用模板代理对象将该参数传输到流:
struct precision_proxy_t
{
int prec;
};
inline ostream& operator<<(ostream& o, precision_proxy_t p)
{
o.precision(p.prec);
return o;
}
precision_proxy_t setprecision(int p)
{
precision_proxy_t result = { p };
return result;
}
cout << setprecision(12) << 3.14;
注意,更现实的实现可能想要在代理中嵌入函数指针,以便只有一个插入操作符:
class ostream;
template <typename T, ostream& (*FUNC)(ostream&, T)>
struct proxy
{
T arg;
proxy(const T& a)
: arg(a)
{
}
};
class ostream
{
public:
template <typename T, ostream& (*FUNC)(ostream&, T)>
ostream& operator<<(proxy<T, FUNC> p)
{
return FUNC(*this, p.arg);
}
};
ostream& global_setpr(ostream& o, int prec)
{
o.precision(prec);
return o;
}
proxy<int, global_setpr> setprecision(int p)
{
return p;
}
cout << setprecision(12) << 3.14;
注意注意,在经典 C++ 中,FUNC 只是一个成员:
template <typename T>
struct proxy
{
T arg;
ostream& (*FUNC)(ostream&, T);
};
class ostream
{
public:
template <typename T>
ostream& operator<<(proxy<T> p)
{
return p.FUNC(*this, p.arg);
}
};
原则上,函数模板可以用作操纵器,例如:
stream << manip1;
stream << manip2(argument);
stream << manip3<N>;
stream << manip4<N>(argument);
但实际上这是不鼓励的,因为许多编译器不接受 manip3。
1.4.8.操作员的位置
理解成员和非成员操作符之间的区别很重要。
当成员操作符被调用时,左边已经被静态地确定了,所以如果需要任何调整,只在右边执行。或者,非成员操作符将只精确匹配或给出错误。
假设您正在重写 std::pair:
template <typename T1, typename T2>
struct pair
{
T1 first;
T2 second;
template <typename S1, typename S2>
pair(const pair<S1, S2>& that)
: first(that.first), second(that.second)
{
}
};
现在加上运算符==。首先作为成员:
template <typename T1, typename T2>
struct pair
{
// ...
inline bool operator== (const pair<T1,T2>& that) const
{
return (first == that.first) && (second == that.second);
}
};
然后编译以下代码:
pair<int, std::string> P(1,"abcdefghijklmnop");
pair<const int, std::string> Q(1,"qrstuvwxyz");
if (P == Q)
{ ... }
这将起作用,并将调用 pair
最好将操作符放在类之外:
template <typename T1, typename T2>
bool operator== (const pair<T1,T2>& x, const pair<T1,T2>& y)
{
return (x.first == y.first) && (x.second == y.second);
}
至少,这段代码现在将无法编译,因为等式现在需要相同的对。明显的失败总是比微妙的问题更可取。
类似于经典的 C++ 规则,“如果你写一个定制的复制构造函数,那么你将需要一个定制的赋值操作符,”我们可以说,如果你写一个通用的复制构造函数,你将可能需要通用操作符,以避免临时转换的开销。在这种情况下,要么使用带有两个参数的模板成员函数,要么使用带有四个参数的全局运算符。有些程序员更喜欢全局操作符,如果可以只使用类的公共接口来实现它们的话(如前所示)。
template <typename T1, typename T2 >
struct pair
{
// ...
template <typename S1, typename S2>
inline bool operator== (const pair<S1, S2>& that) const
{
return (first == that.first) && (second == that.second);
}
};
如果 this->first 和 that.first 是可比较的(例如,int 和 const int),这将起作用。请注意,您可能仍然有临时转换,因为您正在委托给未指定的 T1::operator==。 43
1.4.9.秘密继承
从具体类的公共派生可以用作一种“强 typedef”:
class A
{
// concrete class
// ...
};
class B : public A
{
};
// now B works "almost" as A, but it's a different type
你可能需要在 b 中实现一个或多个“转发构造函数”。
这是模拟模板 typedefs 的策略之一(这在 C++ 中还不存在;参见第 12.6 节):
template <typename T1, typename T2>
class A
{
// ...
};
template <typename T>
class B : public A<T, T>
{
};
但是,只有当是一个存在未知或未记录的私有类时,这才是可接受的:
template <typename T>
class B : public std::map<T, T> // bad idea
namespace std
{
template <...>
class map : public _Tree<...> // ok: class _Tree is invisible to the user
秘密基类通常是不依赖于某些模板参数的操作符的良好容器。例如,测试两个对象之间的相等性可能是合理的,忽略所有纯粹装饰性的参数:
template <typename T, int INITIAL_CAPACITY = 16>
class C;
template <typename T>
class H
{
public:
H& operator==(const H&) const;
};
template <typename T, int INITIAL_CAPACITY>
class C : public H<T>
{
};
具有不同 INITIAL_CAPACITY 的两个容器 C 之间的比较将会成功,并调用它们的公共库 H::operator==。
1.4.10.文字零
有时候你需要写一个函数或者一个操作符,当一个文字零被传递时,它的行为会有所不同。智能指针经常出现这种情况:
template <typename T>
class shared_ptr
{
//...
};
shared_ptr<T> P;
T* Q;
P == 7; // should not compile
P == 0; // should compile
P == Q; // should compile
您可以通过编写一个重载来区分 0 和泛型 int,该重载接受指向没有成员的类的成员的指针:
class dummy {};
typedef int dummy::*literal_zero_t;
template <typename T>
class shared_ptr
{
// ...
bool operator==(literal_zero_t) const
{
用户无法创建 literal_zero_t,因为 dummy 没有 int 类型的成员,所以唯一有效的参数是 literal zero 的隐式强制转换(除非存在更专用的重载)。
1.4.11.布尔类型
有些类型,比如 std::stream,有一个 cast-to-boolean 操作符。如果天真地实施,这可能会导致不一致:
class stream
{
// ...
operator bool() const
{
// ...
}
};
stream s;
if (s) // ok, that's what we want
{
int i = s + 2; // unfortunately, this compiles
}
一个经典的解决方法是实现强制转换为 void*:
class stream
{
// ...
operator void*() const
{
// return 'this' when true or '0' when false
}
};
stream s;
if (s) // ok, that's what we want
{
int i = s + 2; // good, this does not compile...
free(s); // ...but this goes on
}
更好的解决方案还是指向成员的指针:
struct boolean_type_t
{
int true_;
};
typedef int boolean_type_t::*boolean_type;
#define mxt_boolean_true &boolean_type_t::true_
#define mxt_boolean_false 0
class stream
{
// ...
operator boolean_type() const
{
// return mxt_boolean_true or mxt_boolean_false
}
1.4.12.默认值和值初始化
如果 T 是一个类型,那么默认实例的构造并不意味着对象本身的初始化。的确切效果
T x;
严重依赖于 T。如果 T 是基本类型或 POD,则它的初始值是未定义的。如果 T 是一个类,它的一些成员可能还没有定义:
class A
{
std::string s_;
int i_;
public:
A() {} // this will default-construct s_ but leave i_ uninitialized
};
另一方面,这条线
T x = T();
会将 T 初始化为 0,比方说对于所有的基本类型,但是如果 T 是 A,它可能会崩溃,因为将未初始化的成员 i_ 从右边的 temporary 复制到 x 是非法的。
所以总结一下:
T a(); // error:
// a is a function taking no argument and returning T
// equivalent to T (*a)()
T b; // ok only if T is a class with default constructor
// otherwise T is uninitialized
T c(T()); // error: c is a function taking a function and returning T
// equivalent to T (*c)(T (*)())
T d = {}; // ok only if T is a simple aggregate<sup class="calibre7">44</sup> (e.g. a struct
// without user-defined constructors)
T e = T(); // requires a non-explicit copy constructor
// and may yield undefined behaviour at runtime
值初始化(见标准的第 8.5.1-7 段)是解决这个问题的一种方法。因为它只对类成员有效,所以您必须编写:
template <typename T>
struct initialized_value
{
T result;
initialized_value()
: result()
{
}
};
如果 T 是一个有默认构造函数的类,那么将使用它;否则,T 的存储将被设置为 0。如果 T 是一个数组,每个元素将被递归初始化:
initialized_value<double> x; // x.result is 0.0
initialized_value<double [5]> y; // y.result is {0.0, ..., 0.0}
initialized_value<std::string> z; // z.result is std::string()
1.5.代码安全
TMP 的精神是“优雅第一”。理论上,一些技术可以打开源代码中的漏洞,恶意程序员可以利用这些漏洞使程序崩溃。 45
考虑以下情况:
#include <functional>
class unary_F : public std::unary_function<int,float>
{
public:
// ...
};
int main()
{
unary_F u;
std::unary_function<int,float>* ptr = &u; // ok, legal!
delete ptr; // undefined behaviour!
return 0;
}
系统头文件
template<class _Arg, class _Result>
struct unary_function
{
typedef _Arg argument_type;
typedef _Result result_type;
protected:
~unary_function()
{
}
};
但是这种情况一般不会发生。 46
以下想法是由于萨特(【4】):
myclass.h
class MyClass
{
private:
double x_;
int z_;
public:
template <typename stream_t>
void write_x_to(stream_t& y)
{
y << x_;
}
};
是否可以合法读取/修改私有成员 MyClass:😒_?只需在包含 myclass.h 之后的某个地方添加一个专门化即可:
struct MyClassHACK
{
};
template <>
void MyClass::write_x_to(MyClassHACK&)
{
// as a member of MyClass, you can do anything...
z_ = 3;
}
最后,声明模板友谊时也有问题。首先,没有标准和可移植的方法来用模板参数声明友谊(更多细节请参考【5】)。
template <typename T, int N>
class test
{
friend class T; // uhm...
};
第二,没有办法让 test
template <typename T, int N>
class test
{
template <typename X, int J>
friend class test; // ok, but every test<X,J> has access
};
编写 MyClassHACK 的同一个恶意用户可以添加:
template <>
class test<MyClassHACK, 0>
{
public:
template <typename T, int N>
void manipulate(test<T,N>& x)
{
// a friend can do anything!
}
};
您将会看到,TMP 有时会利用在传统 C++ 中被正确标记为不良实践的技术,包括(但不限于):
- (空)基类中缺少非虚拟的受保护析构函数
- 实现强制转换运算符运算符 T() const
- 用单个参数声明非显式构造函数
1.6.编译器假设
大量使用模板意味着编译器的大量工作。并非所有符合标准的技术在每一个平台上都表现相同。 47
你用与语言无关的习语 来表示没有标准规定的行为,而只有合理的预期行为的所有语言特征。换句话说,当你使用与语言无关的习惯用法时,你可以预期大多数编译器会收敛于某些(最优)行为,即使标准没有要求它们这样做。
注意例如,C++ 标准规定,对于任何类型 T,sizeof(T) > 0,但不要求复合类型的大小最小。一个空结构的大小可以是 64,但是我们期望它的大小是 1(或者在最坏的情况下,大小不大于一个指针)。
符合标准的编译器可以合法地违反最优性条件,但是在实践中,这种情况很少发生。换句话说,语言中立的习惯用法是一种语言结构,它不会使程序变得更糟,但会给优秀的编译器提供一个很好的优化机会。
一个完全符合标准的代码片段可能会产生几个问题:
- 意外的编译器错误
- 运行时故障(访问违规、核心转储、蓝屏和恐慌反应)
- 巨大的编译/链接时间
- 次优运行速度
前两个问题是由于编译器错误造成的,并且涉及到寻找语言变通方法(但是第二个问题通常在为时已晚的时候遇到)。
第三个问题主要依赖于糟糕的模板代码。
第四个问题涉及到寻找优化器不能识别的语言无关的习惯用法,因此不必要地降低了程序的执行速度。
我们关心的预期行为的一个例子是向基类添加一个空析构函数。
class base
{
public:
void do_something() {}
protected:
~base() {}
};
class derived : public base
{
};
因为空析构函数不添加任何代码,所以我们希望有和没有它的可执行文件都是相同的。 48
我们假设编译器能够理解并以最佳方式处理以下段落中列出的情况。
1.6.1.内嵌
编译器必须能够自己管理函数内联,忽略内联指令和代码定位(成员函数体在此编写)。
全内联样式将定义和声明放在类体中;每个成员函数都是隐式内联的:
template <typename T>
class vector
{
public:
bool empty() const
{
// definition and declaration
}
};
合并的头样式将非内联成员函数的定义和声明分开,但是将它们保存在同一个文件中:
template <typename T>
class vector
{
public:
bool empty() const; // declaration, non inline
};
template <typename T>
bool vector <T>::empty() const
{
// definition
}
在任何情况下,无论您是否显式地编写它,内联指令都不仅仅是一个提示。一些流行的编译器确实可以根据编译器的判断选择内联任何函数。
具体来说,我们假设
-
如果函数足够简单,无论序列有多长,内联函数序列总是“最优的”:
template <typename T, int N> class recursive { recursive<T,N-1> r_; public: int size() const { return 1 + r_.size(); } }; template <typename T> class recursive<T, 0> { public: int size() const { return 0; } };
在前面的构造中,递归
- 编译器可以优化对无状态对象的(const)成员函数的调用,典型的例子是二元关系的运算符()。
让一个类持有一个仿函数的副本作为私有成员是一种常见的 STL 习惯用法:
template <typename T>
struct less
{
bool operator()(const T& x, const T& y) const
{
return x<y;
}
};
template < typename T, typename less_t = std::less<T> >
class set
{
less_t less_; // the less functor is a member
public:
set(const less_t& less = less_t())
: less_(less)
{
}
void insert(const T& x)
{
// ...
if (less_(x,y)) // invoking less_t::operator()
// ...
}
};
如果函子确实是无状态的,并且 operator()是 const,前面的代码应该相当于:
template <typename T>
struct less
{
static bool apply(const T& x, const T& y)
{
return x<y;
}
};
template < typename T, typename less_t = std::less<T> >
class set
{
public:
void insert(const T& x)
{
// ...
if (less_t::apply(x,y))
{}
}
};
然而,您为更大的通用性付出了代价,因为 less_ member 将消耗至少一个字节的空间。如果编译器实现了 EBO ( 空基优化),就可以解决这两个问题。
class stateless_base
{
};
class derived : public stateless_base
{
// ...
};
换句话说,从无状态基类的任何派生都不会使派生类更大。 50 如果 less 实际上是一个无状态结构,EBO 就不会给 set 的布局增加额外的字节。
template <typename T>
struct less
{
bool operator()(const T& x, const T& y) const
{
return x<y;
}
};
template < typename T, typename less_t = std::less<T> >
class set : private less_t
{
inline bool less(const T& x, const T& y) const
{
return static_cast<const less_t&>(*this)(x,y);
}
public:
set(const less_t& l = less_t())
: less_t(l)
{
}
void insert(const T& x)
{
// ...
if (less(x,y)) // invoking less_t::operator() through *this
{}
}
};
请注意辅助成员函数 less,它旨在防止与任何其他 set::operator()发生冲突。
1.6.2.错误消息
您希望编译器给出精确而有用的错误诊断,尤其是在处理模板时。不幸的是,“精确”和“有用”的含义对于人类和编译器来说可能不一样。
有时,TMP 技术特别诱导编译器在错误信息中输出提示。另一方面,用户应该准备好从编译器日志中包含的一些关键字中找出确切的错误,忽略所有的干扰。这里有一个噪音的例子:
\include\algorithm(21) : error 'void DivideBy10<T>::operator ()(T &) const' : cannot convert parameter 1 from 'const int' to 'int &'
with
[
T=int
]
Conversion loses qualifiers
iterator.cpp(41) : see reference to function template instantiation '_Fn1
std::for_each<XT::pair_iterator<iterator_t,N>,DivideBy10<T>>(_InIt,_InIt,_Fn1)'
being compiled
with
[
_Fn1= DivideBy10<int>,
iterator_t=std::_Tree<std::_Tmap_traits<int,double,std::less<int>,std::allocator
<std::pair<const int,double>>,false>>::iterator,
N=1,
T=int,
_InIt=XT::pair_iterator<std::_Tree<std::_Tmap_traits<int,double,std::less<int>,
std::allocator<std::pair<const int,double>>,false>>::iterator,1>
]
下面是用户应该看到的内容:
iterator.cpp(41) : error in 'std::for_each (iterator, iterator, DivideBy10<int>)'
with
iterator = XT::pair_iterator<std::map<int, double>::const_iterator, 1>
'void DivideBy10<T>::operator ()(T &) const' : cannot convert parameter 1 from 'const int' to 'int &'
这意味着 for_each 的调用者想要改变(也许除以 10?)std::map 的(常量)键,这是非法的。而最初的错误指向
错误消息中出现不友好的条目是因为编译器看到的“基本错误”可能与语义错误“相距甚远”。
长模板堆栈
如前所示,函数模板可以报告一个错误,这是由于从其调用方传递的参数。现代编译器会列出整个模板实例链。由于函数模板通常依赖于模板框架,这些错误通常在函数调用堆栈中的几个层次上。
实施细节
在前面的例子中,编译器显示的是 std::_Tree 而不是 std::map,因为 map::iterator 恰好是在单独的基类(named _Tree)中定义的。std::map 有一个公共 typedef,它从基类中借用了一个迭代器:
typedef typename _Tree<...>::iterator iterator;
这些通常对 std::map 用户隐藏的实现细节可能会在错误日志中泄漏。
扩展的 Typedefs
std::string 的错误可能显示为 std::basic_string <char ...="">,因为一些编译器会用它们的定义替换 typedefs。替换可能会引入用户未知的类型。
然而,编译器真的不可能决定执行这些替换是否方便。
假设有两个元函数叫做 F
typedef typename G<T>::type GT;
typedef typename F<GT>::type FGT;
可能会出现错误
-
当 T 不是 G 的有效参数时,在这种情况下你想读:
error "F<GT> [where GT=G<int>::type]...".
-
因为 G
:::type(已定义但用户未知)被 F 拒绝,所以可能更有用: error "F<GT> [where GT=double]...". ```</t>
然而,如果你不知道 G 的结果,一个日志条目比如 F
不完整的类型
如果使用得当,不完整的类型会导致特定的错误(参见 2.2 节)。然而,有些情况下,一个类型还没有完成,这可能会导致奇怪的错误。附录 a 中有一个很长的有启发性的例子。
通常,当编译器说“常量不是常量”或“类型不是类型”时,这通常意味着你要么递归地定义一个常量,要么使用一个尚未完成的类模板。
1.6.3.杂项提示
不管假设如何,真正的编译器可以做任何事情,所以本节概述了一些通用技巧。
不要责怪编译器
虫子会撒谎:
- 在代码中,以概率(100-ε)%
- 在优化器中,概率略大于(ε/2)%
- 在编译器中,概率小于(ε/2)%
即使只在发布版本中出现的问题也很少是由优化器错误引起的。调试版本和发布版本之间有一些自然的差异,这可能会隐藏程序中的一些错误。常见的因素有#ifdef 节、未初始化的变量、调试分配器返回的零填充堆内存等等。
编译器确实有 bug,但是一个常见的误解是它们只在发布版本中出现。由 MSVC7.1 编译的以下代码在发布时产生正确的值,而在调试时不产生正确的值:
#include <iostream>
int main()
{
unsigned __int64 x = 47;
int y = -1;
bool test1 = (x+y)<0;
x += y;
bool test2 = (x<0);
bool test3 = (x<0);
std::cout << test1 << test2 << test3; // it should print 000
return 0;
}
调试版本中 Mac OSX 的 GCC4 不会警告用户在一个控制台程序中有多个主函数,它会自动生成一个什么也不做的可执行文件。 51
将警告保持在默认级别
警告只是猜测。所有的编译器都能够识别“习惯用法”,这些习惯用法很可能是人为错误的征兆。概率越高,警告级别越低。显示顶级警告不太可能揭示错误,但是它会用无害的消息淹没编译器日志。 52
不要用“脏”代码修改来消除警告
如果某个特定的警告是令人讨厌的、合法的,并且可能不是错误,那么就不要修改代码。将编译器特定的#pragma disable-warning 指令放在该行的周围。这对未来的代码审查者很有用。
但是,应该小心使用这种解决方案(深度嵌套的函数模板中的警告可能会在编译器日志中生成许多冗长的虚假条目)。
应该而不是修复的最危险的警告之一是“有符号/无符号比较”。
混合操作数之间的许多二元运算都涉及到将两者提升为无符号运算,负数会变成正数,而且非常大。编译器会在某些情况下发出警告,而不是全部。
bool f(int a)
{
unsigned int c = 10;
return ((a+5)<c);
}
test01.cpp(4) : warning C4018: '<' : signed/unsigned mismatch
对于∈ {-5,-4,该函数返回 true,...,4}.如果将 c 改为 int,警告会消失,但是函数的行为会有所不同。
元函数中的相同代码不会产生任何警告:
template <int A>
class BizarreMF
{
static const int B = 5;
static const unsigned int C = 10;
public:
static const bool value = ((A+B)<C);
};
bool t = BizarreMF<-10>::value; // returns false
在实际代码中,有两种情况可能容易出现“符号性错误”:
-
将元函数返回类型从枚举更新为静态无符号常量:
static const bool value = (A+5) < OtherMF<B>::value; // unpredictable result: the type of OtherMF is unknown / may vary
-
Changing a container:
C++ 标准没有明确定义数组索引的整数类型。如果 p 有类型 T*,那么 p[i] == *(p+i),那么我应该有类型 ptrdiff_t,它是有符号的。然而,vector
::operator[]采用无符号索引。
总而言之,警告是:
- 编译器特定的
- 与代码正确性无关(既有产生警告的正确代码,也有编译干净的错误代码)
编写尽可能不产生警告的代码。
维护一个编译器错误目录
这在升级编译器时非常有用。
避免不规范的行为
这个建议在每一本关于 C++ 的书中都有,但是我们在这里重复一遍。程序员 54 倾向于用自己喜欢的编译器作为主要工具来判定一个程序是否正确,而不是 C++ 标准。一个合理的经验标准是使用两个或更多的编译器,如果他们不同意,检查标准。
不要害怕语言特性
只要有原生 C++ 关键字、函数或 std:: object,就可以认为不可能做得更好,除非牺牲一些特性。 55
C++ 程序中的严重瓶颈通常与语言特性的误用有关(有些特性比其他特性更容易被误用;候选项是虚函数和动态内存分配),但这并不意味着应该避免这些特性。
任何操作系统都可以足够快地分配堆内存,以至于合理数量的对 operator new 的调用都不会被注意到。 56
一些编译器允许你通过一个名为 alloca 的函数从堆栈中获取一点内存;原则上,alloca 后跟一个 placement new(和一个显式的析构函数调用)大致相当于 new,但是它会导致对齐问题。虽然标准允许堆内存适合任何类型,但这不适用于堆栈。更糟糕的是,在未对齐的内存上构建对象可能会在某些平台上偶然工作,而且在完全未被发现的情况下,可能会降低所有数据操作的速度。 57
相反的情况是交易特性。在强有力的额外假设下,有时可能做得比新的更好;例如,在分配/解除分配模式已知的单线程程序中:
// assume T1 and T2 are unspecified concrete types, not template parameters
std::multimap<T1, T2> m;
while (m.size()>1)
{
std::multimap<T1, T2>::iterator one = ...; // pick an element.
std::multimap<T1, T2>::iterator two = ...; // pick another one.
std::pair<T1, T2> new_element = merge_elements(*one, *two);
m.erase(one); // line #1
m.erase(two); // line #2
m.insert(new_element); // line #3
}
在这里,您可能希望胜过默认的基于 new 的分配器,因为两次删除之后总是跟着一次分配。粗略地说,当这由系统 new/delete 处理时,必须通知操作系统在第 2 行有更多的可用内存,但是第 3 行立即回收相同数量的内存。 58
想想你的代码的用户会怎么做
人类的记忆不如电脑记忆持久。一些在经典 C++ 中看起来显而易见或容易推导的东西在 TMP 中可能更难。
考虑一个简单的函数,例如:
size_t find_number_in_string(std::string s, int t);
您很容易猜到该函数会在第一个参数中查找第二个参数。现在考虑:
template <typename T, typename S>
size_t find_number_in_string(S s, T t);
虽然这对作者来说看起来很自然(毕竟 S 代表字符串),但我们应该考虑一些帮助记忆的技巧。
-
任何具有代码完成功能的 IDE 都将显示参数名 :
template <typename T, typename S> size_t find_number_in_string(S str, T number); template <typename NUMBER_T, typename STRING_T> size_t find_number_in_string(STRING_T str, NUMBER_T number);
-
在函数前的代码中插入一行注释;IDE 可以选择它并显示工具提示。
-
对参数的顺序或结果类型采用某种约定(比如 C 的 memcpy)。
1.7.预处理器
1.7.1.包括防护装置
如前所述,一个项目通常分布在许多源文件中。必须对每个文件进行组织,以便所有依赖项和先决条件都由包含的文件检查,而不是由调用者检查。特别是,头包含不应该依赖于#include 语句的顺序。
file "container.hpp"
#include <vector> // dependency is resolved here, not outside
#ifdef _WIN32 // preconditions are checked here
#error This file requires a 128-bit operating system. Please, upgrade.
#endif
template <typename T>
class very_large_container
{
// internally uses std::vector...
};
大多数框架最终都有一种根文件,负责准备环境:
- 当前平台的检测
- 编译器特定宏到框架宏的翻译
- 通用宏的定义(如 MXT_NAMESPACE_BEGIN)
- 包含 STL 标题
- 轻量级结构、类型定义和常量的定义
所有其他头文件都是从根文件开始的,根文件很少被修改。这通常会减少编译时间,因为可以指示编译器从根文件中提取预编译的头文件。
下面是一个例子:
///////////////////////////////////////////////////////////////////////
// platform detection
#if defined(_MSC_VER)
#define MXT_INT64 __int64
#elif defined(__GNUC__)
#define MXT_INT64 long long
#else
// ...
#endif
///////////////////////////////////////////////////////////////////////
// macro translation
// the framework will rely on MXT_DEBUG and MXT_RELEASE
#if defined(DEBUG) || defined(_DEBUG) || !defined(NDEBUG)
#define MXT_DEBUG
#else
#define MXT_RELEASE
#endif
///////////////////////////////////////////////////////////////////////
// general framework macros
#define MXT_NAMESPACE_BEGIN(x) namespace x {
#define MXT_NAMESPACE_END(x) }
///////////////////////////////////////////////////////////////////////
// STL
#include <complex>
#include <vector>
#include <map>
#include <utility>
///////////////////////////////////////////////////////////////////////
using std::swap;
using std::size_t;
typedef std::complex<double> dcmplx;
typedef unsigned int uint;
///////////////////////////////////////////////////////////////////////
struct empty
{
};
根据基本的 include guard 习惯用法,您应该将每个头包含在预处理程序指令中,这将防止在同一个翻译单元中出现多个包含:
#ifndef MXT_filename_
#define MXT_filename_
// put code here
#endif //MXT_filename_
作为这种技术的一个小变化,您可以给 MXT_filename_ 赋值。毕竟,这本书的全部意义在于将信息存储在不同寻常的地方:
#ifndef MXT_filename_
#define MXT_filename_ 0x1020 // version number
// put code here
#endif //MXT_filename_
#include "filename.hpp"
#if MXT_filename_ < 0x1010
#error You are including an old version!
#endif
无论如何,这样的保护对于包含循环是无效的。在 TMP 中循环发生得更频繁,这里只有头,没有*。cpp 文件,所以声明和定义要么一致,要么在同一个文件中。
假设 A.hpp 自成体系,B.hpp 包含 A.hpp,C.hpp 包含 B.hpp。
// file "A.hpp"
#ifndef MXT_A_
#define MXT_A_ 0x1010
template <typename T> class A {};
#endif
// file "B.hpp"
#ifndef MXT_B_
#define MXT_B _ 0x2020
#include "A.hpp"
template <typename T> class B {}; // B uses A
#endif
后来,开发人员修改了 A.hpp,使其包含 C.hpp。
// file "A.hpp"
#ifndef MXT_A_
#define MXT_A_ 0x1020
#include "C.hpp"
...
不幸的是,预处理器会生成一个文件,在 A 之前包含 B 的副本:
// MXT_A_ is not defined, enter the #ifdef
#define MXT_A_ 0x1020
// A.hpp requires including "C.hpp"
// MXT_C_ is not defined, enter the #ifdef
#define MXT_C_ 0x3030
// C.hpp requires including "B.hpp"
// MXT_B_ is not defined, enter the #ifdef
#define MXT_B _ 0x2020
// B.hpp requires including A.hpp
// however MXT_A_ is already defined, so do nothing!
template <typename T> class B {};
// end of include "B.hpp"
template <typename T> class C {};
// end of include "C.hpp"
template <typename T> class A {};
这通常会给出奇怪的错误信息。
总之,您应该检测到循环包含问题,即文件在完全编译之前(间接地)包含了自身的副本。
下面的框架头有所帮助(缩进仅用于说明目的)。
#ifndef MXT_filename_
#define MXT_filename_ 0x0000 // first, set version to "null"
#include "other_header.hpp"
/////////////////////////////////////////////////////////////
MXT_NAMESPACE_BEGIN(framework)
/////////////////////////////////////////////////////////////
// write code here
/////////////////////////////////////////////////////////////
MXT_NAMESPACE_END(framework)
/////////////////////////////////////////////////////////////
// finished! remove the null guard
#undef MXT_filename_
// define actual version number and quit
#define MXT_filename_ 0x1000
#else // if guard is defined...
#if MXT_filename_ == 0x0000 // ...but version is null
#error Circular Inclusion // ...then something is wrong!
#endif
#endif //MXT_filename_
这样的头文件不会解决循环包含(这是一个设计问题),但是编译器会尽快诊断它。无论如何,有时用一些前向声明替换#error 语句就足够了:
#ifndef MXT_my_vector_
#define MXT_my_vector_ 0x0000
template <typename T>
class my_vector
{
public:
// ...
};
#undef MXT_my_vector_
#define MXT_my_vector_ 0x1000
#else
#if MXT_my_vector_ == 0x0000
template <typename T>
class my_vector;
#endif
#endif //MXT_my_vector_
1.7.2.宏展开规则
巧妙使用宏可以简化元编程任务,比如成员函数生成的自动化。我们在这里简单提一下非显而易见的预处理器规则 59 :
-
标记串联运算符## 从两个字符串的串联中生成一个标记。它不仅仅是一个“空白消除”操作符。如果结果不是单个 C++ 标记,则是非法的:
#define M(a,b,c) a ## b ## c int I = M(3,+,2); // error, illegal: 3+2 is not a single token int J = M(0,x,2); // ok, gives 0x2
-
stringizer 前缀# 将文本 60 转换为有效的对应的 C++ 字符串,因此它将插入右反斜杠,依此类推。
-
通常宏展开是递归的。首先,参数被完全展开,然后它们在宏定义中被替换,然后最终结果被再次检查并可能再次展开:
#define A1 100 #define A2 200 #define Z(a,b) a ## b Z(A, 1); // expands to A1, which expands to 100 Z(A, 3); // expands to A3
-
然而,# 和## 这两个运算符,抑制了它们自变量上的,所以:
Z(B, A1); // expands to BA1, not to B100
-
为了确保所有内容都被展开,您可以添加一个额外的间接层,这个层显然什么也不做:
#define Y(a,b) a ## b #define Z(a,b) Y(a,b) Z(B,A1); // expands first to Y(B,A1). Since neither B nor A1 is an operand // of # or ##, they are expanded, so we get Y(B,100), // which in turn becomes B100
-
宏不能递归,所以在扩展 Z 时,不考虑对 Z 的任何直接或间接引用:
#define X Z #define Z X+Z Z; // expands first as X+Z. The second Z is ignored; then the first X // is replaced by Z, and the process stops, // so the final result is "Z+Z"
-
一个流行的技巧是定义一个宏本身。这实际上相当于一个#undef,除了宏仍然被定义(所以#ifdef 和类似的指令不会改变行为)。
#define A A
最后总结一下:
#define A 1
#define X2(a, b) const char* c##a = b
#define X(x) X2(x, #x)
#define Y(x) X(x)
X2(A, "A"); // const char* c##A = "A" const char* cA = "A";
X(A); // X2(A, #A) X2(1, "A") const char* c1 = "A";
Y(A); // X(A) X(1) X2(1, "1") const char* c1 = "1";
注意,在这段代码中,X 可能看起来只是 X2 的一个方便快捷的方式,但不是。通常你不能观察到区别,但是在 X 扩展到 X2 之前,论点扩展发生了,一些直接调用 X2 可以阻止的事情。
用常量(enum 或 static const int)替换定义整数的宏有多安全?答案在前面的代码片段中。更改后,预处理程序的技巧将被打破:
//#define A 1
static const int A = 1;
// ...
X(A); // const char* cA = "A";
Y(A); // const char* cA = "A";
但是如果 A 没有保证是宏,那么替换应该是透明的。 61
另一个值得一提的规则是,预处理程序尊重带参数和不带参数的宏之间的区别。特别是,它不会试图展开一个后跟一个左括号的 A,类似地,对于没有后跟一个括号的 X。这条规则在一个流行的习惯用法中被利用,它防止构造类型 C 62 的未命名实例:
template <typename T>
class C
{
public:
explicit C([[one argument here]]);
};
#define C(a) sizeof(sorry_anonymous_instance_not_allowed_from_ ## a)
C x("argument"); // ok: C not followed by bracket is not expanded
return C("temporary"); // error: the sizeof statement does not compile
最后,由于许多模板类型包含一个逗号,所以通常不可能通过宏安全地传递它们:
#define DECLARE_x_OF_TYPE(T) T x
DECLARE_x_OF_TYPE(std::map<int, double>); /* error:
^^^^^^^^^^^^ ^^^^^^^ two arguments */
对此有几种解决方法:
-
额外的括号(一般来说,这不太可能*起作用,因为在 C++ 中,括号中的类型没有多大用处):
DECLARE_x_OF_TYPE((std::map<int, double>)); // (std::map<int, double>) x; error ```*
** typedef 可以工作,除非该类型依赖于其他宏参数:
```cpp
typedef std::map<int, double> map_int_double;
DECLARE_x_OF_TYPE(map_int_double);
```
* 另一个宏:
```cpp
#define mxt_APPLY2(T, T1, T2) T< T1, T2 >
DECLARE_x_OF_TYPE(mxt_APPLY2(std::map,int,double));
```*
*__________________
1 不严格地说,这就是“元编程”中“元”前缀的原因。
2 在现代 C++ 中有更多,但你可以考虑它们的扩展;这里描述的是元编程一等公民。第十二章有更多细节。
3 通常任何整数类型都被接受,包括命名/匿名 enum、bool、typedefs(像 ptrdiff_t 和 size_t),甚至编译器特定类型(例如 MSVC 的 __int64)。指向成员/全局函数的指针是不受限制的;指向一个变量的指针(有外部链接)是合法的,但是它不能在编译时被解引用,所以这在实践中有非常有限的用途。参见第十一章。
链接器最终可能会将它们折叠起来,因为它们可能会产生相同的机器代码,但从语言的角度来看,它们是不同的。
5 一个例外是文字 0 可能不是有效的指针。
6 更完整的讨论见 1.3.6 节和 11.2.2 。
7 参见 1.3.2 节的注释。
8 你可以把一个浮点字面值强制转换成整数,所以严格来说,(int)(1.2)是允许的。并非所有的编译器都严格遵守这条规则。
9 使用 LINE 作为参数在实践中很少出现;它在自动类型枚举(见 7.6 节)和一些自定义断言的实现中很流行。
10 我们必须选择不同的名称,以避免遮蔽外部模板参数 scalar_t
11 另见www . open-STD . org/JT C1/sc22/wg21/docs/cwg _ defects . html # 666
。
12 即使不是正确的例子,开明的读者可能也要考虑一下 std::string,std::wstring,std::basic_string < T >之间的关系。
13 参见 1.4.9。
14 作为旁注,这再一次说明了在 TMP 中,你写的代码越少越好。
15 与 1.1.1 节中描述的 typename 的使用进行比较
16 参见 http://www.research.att.com/~bs/比雅尼·斯特劳斯特鲁普在他的《C++ 风格与技巧常见问题》中提到的“脆弱基类问题”。
17 确切的规则在【2】中有记载和解释。你可以参考这本书,对这里总结的几个段落进行详细的解释。
18 见下一节。
19 这个例子取自[2]。
20 特别是编译器不需要注意到 void f(arg < 2*N >)和 void f(arg < N+N >)是同一个模板函数,这样的双重定义会让程序格式不良。然而,在实践中,大多数编译器会识别出歧义并发出适当的错误。
21 模板函数不能部分专门化,只能重载。
22 不幸的是,一些流行的编译器容忍了这一点。
23
24 多出来的“Y”不过是诗意的许可。参考 Danny Kalev 在 http://www.informit.com/guides/content.aspx?g=cplusplus的精彩文章。
25 除 STD::numeric _ limits
甚至源代码也有生命周期,最终它会“死去”,也就是说,它会被从头重写。然而,设计越稳健,它的寿命就越长,而风格是设计的一部分。参见[5]。
27 如【5】所述,通常成员函数名应该是动作。因此,空应该是 make_empty 的同义词,而不是 is_empty 的同义词。但是,STL 约定是成立的,并且被普遍理解。如果有疑问,就像 std::vector 那样做。
28 具体要求和规格见 http://en.cppreference.com/w/cpp/language/constexpr。
最后两种实现的区别很大程度上在于它们对无效语法的反应。作为练习,考虑一些类似 MXT_SORT2(x,y)的恶意代码 if (true)抛出 _ exception。
读者可能想回顾一下本章开头的简单例子。
31 参见章节 6.2.1 。
32 有些编译器,比如 MSVC71,以前有未命名参数的问题;详细示例请参考第 11.3.3 段。
一些作者为此保留了关键字 typename。换句话说,他们声明 template < typename T >来表示 T 是“任何类型”,声明 template < class T >来表示 T 确实是一个类,而不是原生类型。然而,这种区分是相当人为的。
34 注意,这不是正式要求;只是一个名字而已!名字反映了我们对类型的看法;如有必要,稍后我们将强制执行此操作。
35 数学倾向的读者应该把后者看作前者的特例。常量 5 '可以用名为 five 或 static_value < int,5 >的类型来代替。这导致了更大的通用性。更多信息参见【3】。
36 它应该驻留在一个匿名的名称空间中,但这并不能使它不可访问。
37 优点在苹果技术说明 TN2185 中有广泛描述;参考下页:【http://developer.apple.com/technotes/tn2007/tn2185.html】。
38 这样的功能在【5】中表示为垫片。
39 如果 a 是长度为 2 的 T 的数组,那么(char *)(&a[1])-(char *)(&a[0])是一个 ptrdiff_t,至少与 sizeof(T)一样大。这意味着 ptrdiff_t 至少和 int 一样大。这个参数实际上表明 sizeof 的每个结果都可以存储在 ptrdiff_t 中。泛型 size_t 可能不会存储在 ptrdiff_t 中,因为 sizeof 不一定是满射的—可能有一个 size_t 值大于每个可能的 sizeof。
40 例如,创建 std::string 的副本所需的时间与字符串本身的长度成正比,所以这不仅取决于类型,还取决于实例;或者,复制一个 double 是一个常量时间操作。从数学上来说,“常数时间”的概念在 C++ 中并没有很好的定义;这个问题太复杂,无法用脚注来说明,但我们将概述一下这个想法。对于任何可能的输入,如果算法的执行时间受常数 K 的限制,则算法是 O(1)。如果可能的输入数量是有限的,即使数量很大,算法也自动为 O(1)。例如,在 C++ 中,两个 int 的和是 O(1)。一般来说,C++ 内存模型具有有限的可寻址空间(因为所有对象都有固定的大小,而“地址”是一个对象),这意味着某些算法的可能输入数量是有限的。快速排序的复杂度是 O(N*log(N)),但是 std::sort 在形式上可能被认为是 O(1),其中——不严格地说——常数 K 是对最大可能的数组进行排序所需的时间。
41 对比 2.3.1 章节。
42 有些对象可能想提前检查覆盖是否可行。例如,如果 T 是 std::string,其 size()= = the . size(),那么它可能能够执行安全的 memcpy。
43 注意,最好的选择是要求配对的对象提供合适的运算符,所以我们委托比较。例如,pair < const char,int >和 pair < std::string,int >不太可能触发临时字符串的构造,因为我们期望 STL 提供一个 operator==(const char,const std::string &)。
44c++ 11 中改变了“聚合”的定义,引入了统一初始化。由于这个问题相当复杂和详细,读者可能希望看到参考书目。
编写“完全防弹”的代码可能会增加复杂性的巨大成本。有时,这种复杂性也会抑制一些编译器优化。作为一条规则,程序员应该总是务实地推理,并接受这样一个事实,即代码不会处理每一种可能的极端情况。
46 参见 1.6 节。
47 平台,通常我们指的是集合{处理器、操作系统、编译器、链接器}。
从经验分析来看,有时受保护的空析构函数会抑制优化。一些测量结果已经发表在【3】。
49 注意递归< T,-1 >不会编译。
50 大多数编译器都实现了这种优化,至少在单继承的情况下是这样。
【51】MAC OS x 10 . 4 . 8,XCode 2.4.1,GCC 4.01。
52 将警告设置为最高级别只有一次,在最后的开发阶段或搜寻神秘的 bug 时。
53 见标准中 3.7.2。
54 包括这本书的作者。
55 当然,这个规则也有已知的例外:一些 C 运行时函数(sprintf,floor),甚至少数 STL 函数(string::operator+)。
56 无论如何,释放记忆可能是完全不同的一回事。
57 在 AMD 处理器上,double 应该对齐到一个 8 字节边界;否则,CPU 将执行多个不必要的加载操作。在不同的处理器上,访问未对齐的 double 可能会立即导致程序崩溃。
58 一个普遍的策略是把记忆分成几块,用某种程度的懒惰来释放它们。
59 要想获得完整的参考,可以考虑 GNU 手册【http://gcc.gnu.org/onlinedocs/cpp.pdf】的。
60 它只能应用于宏参数,不能应用于任意文本。
61 在这种情况下,它们应该用作枚举。特别是,如果它们碰巧是宏,那么取消对它们的定义,并用真实的枚举替换它们应该是安全的。
62 这个例子其实只有看完 2,2 节才会清楚。****
二、小对象工具包
前一章重点介绍了模板编程和风格之间的联系。简而言之,模板是优雅的,因为它们允许您编写看起来简单的高效代码,因为它们隐藏了潜在的复杂性。
如果你还记得第一章中 sq 的介绍性例子,很明显 TMP 的第一个问题是选择最好的 C++ 实体来建模一个概念,并使代码在实例化时看起来清晰。
大多数经典函数在内部使用临时变量并返回结果。临时变量很便宜,所以必须给中间结果一个名称,以增加算法的可读性:
int n_dogs = GetNumberOfDogs();
int n_cats = GetNumberOfCats();
int n_food_portions = n_dogs + n_cats;
BuyFood(n_food_portions);
在 TMP 中,临时变量的等价物是辅助类型。
为了给一个概念建模,我们会自由地使用许多不同的类型。他们中的大多数除了“在他们的名字中带有某种意义”之外什么也不做,就像前面例子中的 n_food_portions 一样。
这是 2.3 节的主题。
下面的段落列出了一些非常简单的对象,它们很自然地成为复杂模式的构建块。这些被称为“空心的”,因为它们不携带任何数据(它们可能根本没有成员)。本章介绍的代码可以在本书的其余部分自由重用。
2.1.空心类型
2.1.1.的实例
元编程中最通用的工具之一是 instance_of:
template <typename T>
struct instance_of
{
typedef T type;
instance_of(int = 0)
{
}
};
构造函数允许您声明全局常量并快速初始化它们。
const instance_of<int> I_INT = instance_of<int>(); // ok but cumbersome
const instance_of<double> I_DOUBLE = 0; // also fine.
注意记住,一个常量对象必须要么被显式初始化,要么有一个用户定义的默认构造函数。如果你只是写
struct empty
{
empty() {}
};
const empty EMPTY;
编译器可能会警告 EMPTY 是未使用的。事实上,抑制警告的一个很好的解决方法是:
struct empty
{
empty(int = 0) {}
};
const empty EMPTY = 0;
2.1.2.选择器
经典 C++ 中的传统代码将信息存储在变量中。例如,bool 可以存储两个不同的值。在元编程中,所有的信息都包含在类型本身中,所以 bool 的等价物是一个(模板)类型,可以用两种不同的方式实例化。这叫做选择器:
template <bool PARAMETER>
struct selector
{
};
typedef selector<true> true_type<sup class="calibre7">1</sup>;
typedef selector<false> false_type;
注意,选择器
template <bool B, typename T>
void f(const T& x)
{
}
int main()
{
double d = 3.14;
f<true>(d); // force B=true and deduce T=double
};
或者相当于:
template <typename T, bool B>
void f(const T& x, selector<B>)
{
}
int main()
{
double d = 3.14;
f(d, selector<true>()); // deduce B=true and T=double
};
后一种实现的优点之一是,您可以使用一个(廉价的)常量为第二个参数指定一个有意义的名称:
const selector<true> TURN_ON_DEBUG_LOGGING;
// ...
double d = 3.14;
f(d, TURN_ON_DEBUG_LOGGING); // deduce B=true and T=double
2.1.3.静态值
选择器的概括是一个静态值:
template <typename T, T VALUE>
struct static_parameter
{
};
template <typename T, T VALUE>
struct static_value : static_parameter<T, VALUE>
{
static const T value = VALUE;
};
请注意,您可以用 static_value
在 static_value 中,T 必须是整数类型;否则,静态常量初始化将变得非法。相反,在 static_parameter 中,T 可以是指针(而 VALUE 可以是文字零)。
可以添加一个成员强制转换运算符,以允许从静态常量切换到运行时整数 3 :
template <typename T, T VALUE>
struct static_value : static_parameter<T, VALUE>
{
static const T value = VALUE;
operator T () const
{
return VALUE;
}
static_value(int = 0)
{
}
};
所以你可以把 static_value
template <typename T, T VALUE>
inline T static_value_cast(static_value<T, VALUE>)
{
return VALUE;
};
2.1.4.约束的大小
C++ 标准对基本类型 4 的大小没有严格的要求,复合类型可以在成员之间的任何地方进行内部填充。
给定一个类型 T,假设你想获得另一个类型,T2,它的 sizeof 是不同的。
一个非常简单的解决方案是:
template <typename T>
class larger_than
{
T body_[2]; // private, not meant to be used
};
它必须保持 sizeof(T) <2*sizeof(T)≤sizeof(larger_than
该类最重要的用途是定义两种类型(参见第 4.2.1 节):
typedef char no_type;
typedef larger_than<no_type> yes_type;
警告这些定义与 C++0x std::false_type 和 std::true_type 不兼容,而是等效于 static_value < bool,false >和 static_value < bool,true >。
实际上,您可以安全地使用 char(根据定义,其大小为 1)和 ptrdiff_t(在大多数平台中,指针大于一个字节)。
可以声明大小正好为 N(N > 0)的类型:
template <size_t N>
struct fixed_size
{
typedef char type[N];
};
这样 sizeof(fixed _ size
注意 fixed_size
记住,声明一个返回数组的函数是非法的,但是一个数组的引用是可以的,并且具有相同的大小 5 :
fixed_size<3>::type f(); // error: illegal
int three = sizeof(f());
fixed_size<3>::type& f(); // ok
int three = sizeof(f()); // ok, three == 3
2.2.静态断言
静态断言是简单的语句,其目的是当模板参数不符合某个规范时引发(编译器)错误。
我在这里只说明主题的最基本的变化。
最简单的断言形式就是尝试使用你所需要的。如果您需要确保类型 T 确实包含一个常量命名值或一个类型命名类型,您可以简单地编写:
template <typename T>
void myfunc()
{
typedef typename T::type ERROR_T_DOES_NOT_CONTAIN_type;
const int ASSERT_T_MUST_HAVE_STATIC_CONSTANT_value(T::value);
};
如果 T 不一致,您将得到一个指向某种“描述性”行的错误。
对于更复杂的断言,您可以利用这样一个事实,即不完整的类型不能被构造,或者如果 T 不完整,sizeof(T)会导致编译器错误。
2.2.1.布尔断言
验证语句最简单的方法是使用类似选择器的类,如果条件为假,则该类的主体不存在:
template <bool STATEMENT>
struct static_assertion
{
};
template <>
struct static_assertion<false>;
int main()
{
static_assertion<sizeof(int)==314> ASSERT_LARGE_INT;
return 0;
}
error C2079: 'ASSERT_LARGE_INT' uses undefined struct 'static_assertion<false>'
这个习惯用法的所有变体都试图欺骗编译器发出更加用户友好的错误消息。安德烈·亚历山德雷斯库提出了一些改进措施。这里有一个例子。
template <bool STATEMENT>
struct static_assertion;
template <>
struct static_assertion<true>
{
static_assertion()
{}
template <typename T>
static_assertion(T)
{}
};
template <> struct static_assertion<false>;
struct error_CHAR_IS_UNSIGNED {};
int main()
{
const static_assertion<sizeof(double)!=8> ASSERT1("invalid double");
const static_assertion<(char(255)>0)> ASSERT2(error_CHAR_IS_UNSIGNED());
}
如果条件为假,编译器将报告类似“无法从 error_CHAR_IS_UNSIGNED 构建 static_assertion
每个断言都会在堆栈上浪费一些字节,但是可以使用 sizeof 将它包装在一个宏指令中:
#define MXT_ASSERT(statement) sizeof(static_assertion<(statement)>)
祈祷
MXT_ASSERT(sizeof(double)!=8);
如果成功,将转换为[[某个整数]],否则将转换为错误。因为像 1 这样的语句是无效的,所以优化器会忽略它。
宏断言的最大问题是逗号:
MXT_ASSERT(is_well_defined< std::map<int, double> >::value);
// ^
// comma here
//
// warning or error! MXT_ASSERT does not take 2 parameters
在这种情况下,宏的参数可能是第一个逗号之前的字符串(is_well_defined< std::map
有两种可能的解决方法—您可以键入逗号,或者在参数周围加上额外的括号:
typedef std::map<int, double> map_type;
MXT_ASSERT( is_well_defined<map_type>::value );
or:
MXT_ASSERT(( is_well_defined< std::map<int, double> >::value ));
C++ 预处理器只会被与宏的参数处于同一级别的逗号 6 混淆:
assert( f(x,y)==4 ); // comma at level 2: ok
assert( f(x),y==4 ); // comma at level 1: error
static_assertion 可用于在使用私有继承的类中进行断言:
template <typename T>
class small_object_allocator : static_assertion<(sizeof(T)<64)>
{
};
注意 static_assert 是现代 C++ 标准中的一个关键字。这里,为了便于说明,我对一个类使用了一个相似的名称。C++0x static_assert 的行为类似于一个采用常量布尔表达式和字符串文字(编译器将打印的错误消息)的函数:
static_assert(sizeof(T)<64, "T is too large");
与前面描述的私有继承类似,C++0x static_assert 也可以是类成员。
2.2.2.断言合法
做出断言的另一种方式是要求某个 C++ 表达式表示类型 T 的有效代码,返回 non-void(最常见的是,声明构造函数或赋值是可能的)。
#define MXT_ASSERT_LEGAL(statement) sizeof(statement)
如果允许使用 void,只需在 sizeof 中放置一个逗号操作符:
#define MXT_ASSERT_LEGAL(statement) sizeof((statement), 0)
例如:
template <typename T>
void do_something(T& x)
{
MXT_ASSERT_LEGAL(static_cast<bool>(x.empty()));
If (x.empty())
{
// ...
}
}
这个例子将编译,因此如果 x.empty()返回(任何可转换为)bool,它不会拒绝 T。t 可能有一个名为 empty 的成员函数返回 int,或者有一个名为 empty 的成员,其运算符()不带参数并返回 bool。
这是另一个应用:
#define MXT_CONST_REF_TO(T) (*static_cast<const T*>(0))
#define MXT_REF_TO(T) (*static_cast<T*>(0))
template <typename obj_t, typename iter_t>
class assert_iterator
{
enum
{
verify_construction =
MXT_ASSERT_LEGAL(obj_t(*MXT_CONST_REF_TO(iter_t))),
verify_assignment =
MXT_ASSERT_LEGAL(MXT_REF_TO(obj_t) = *MXT_CONST_REF_TO(iter_t)),
verify_preincr =
MXT_ASSERT_LEGAL(++MXT_REF_TO(iter_t)),
verify_postincr =
MXT_ASSERT_LEGAL(MXT_REF_TO(iter_t)++)
};
};
一个人类程序员应该读到,“我断言从 iter_t 的(const)实例的解引用结果中构造 obj_t 的实例是合法的”,对于其余的常数也是如此。
注意注意到一些标准迭代器可能第一次测试就失败了。例如,back_insert_iterator 可以在解引用时返回自身(一个特殊的赋值操作符将负责使*i = x 等价于 i = x)。
assert_iterator
请记住,由于宏中对逗号字符的通常限制,MXT_ASSERT_LEGAL 从不实例化对象。这是因为 sizeof 仅对其参数 7 执行维度检查。
另外,请注意宏指令的特殊用法。MXT_ASSERT_LEGAL 应该占用整行,但是由于它解析为一个编译时整数常量,所以可以使用枚举来“标记”关于一个类的所有不同断言(就像在 assert_iterator 中一样),并使代码更加友好。
编译器也可能发出指向这些断言的有用警告。如果 obj_t 是 int,iter_t 是 double*,编译器将引用 verify_assignment 枚举器并发出类似于以下内容的消息:
warning: '=' : conversion from 'double' to 'int', possible loss of data
: see reference to class template instantiation 'XT::assert_iterator<obj_t,iter_t>' being compiled
with
[
obj_t=int,
iter_t=double *
]
使用完全相同的技术,您可以混合不同种类的静态断言:
#define MXT_ASSERT(statement) sizeof(static_assertion<(statement)>)
template <typename obj_t, typename iter_t>
class assert_iterator
{ enum
{
//...
construction =
MXT_ASSERT_LEGAL(obj_t(*MXT_CONST_REF_TO(iter_t))),
size =
MXT_ASSERT(sizeof(int)==4)
};
};
作为练习,我列出了迭代器上一些更具启发性的断言。
事实上,assert_iterator 类验证前向 const _ iterators。我们可以去掉常数:
template <typename obj_t, typename iter_t>
class assert_nonconst_iterator : public assert_iterator<obj_t, iter_t>
{
enum
{
write =
MXT_ASSERT_LEGAL(*MXT_REF_TO(iter_t) = MXT_CONST_REF_TO(obj_t))
};
};
有时,在迭代器上工作的算法不需要知道底层对象的实际类型,这使得代码更加通用。例如,std::count 可能如下所示:
template <typename iter_t, typename object_t>
int count(iter_t begin, const iter_t end, const object_t& x)
{
int result = 0;
while (begin != end)
{
if (*begin == x)
++result;
}
return result;
}
不需要知道begin 和 x 的类型是否相同,不管begin 到底是什么,都可以假设它定义了一个运算符==适合与 object_t 进行比较。
假设您必须在比较之前存储*begin 的结果。
您可能需要迭代器类型遵循 STL 约定,这意味着 object_t 和 iterator::value_type 必须以某种方式兼容 8 :
template <typename obj_t, typename iter_t>
class assert_stl_iterator
{
typedef typename std::iterator_traits<iter_t>::value_type value_type;
enum
{
assign1 =
MXT_ASSERT_LEGAL(MXT_REF_TO(obj_t) = MXT_CONST_REF_TO(value_type)),
assign2 =
MXT_ASSERT_LEGAL(MXT_REF_TO(value_type) = MXT_CONST_REF_TO(obj_t))
};
};
最后,您可以对迭代器类型进行粗略的检查,使用 indicator_traits 获取它的标签,或者使用 MXT_ASSERT_LEGAL 进行写操作:
enum
{
random_access =
MXT_ASSERT_LEGAL(
MXT_CONST_REF_TO(iter_t) + int() == MXT_CONST_REF_TO(iter_t))
};
2.2.3.带有重载操作符的断言
sizeof 可以计算任意表达式的大小。因此,您可以创建 sizeof(f(x))形式的断言,其中 f 是一个重载函数,它可能返回一个不完整的类型。
在这里,我只是给出了一个例子,但是该技术将在 4.2.1 节中解释。
假设您想对数组的长度进行一些检查:
T arr[] = { ... };
// later, assert that length_of(arr) is some constant
因为静态断言需要一个编译时常量,所以不能将 length_of 定义为一个函数。
template <typename T, size_t N>
size_t length_of(T (&)[N])
{
return N;
}
MXT_ASSERT(length_of(arr) == 7); // error: not a compile-time constant
宏可以工作:
#define length_of(a) sizeof(a)/sizeof(a[0])
但是这是有风险的,因为它可以在支持 operator[](比如 std::vector 或指针)的不相关类型上被调用,这带来了令人讨厌的后果。
但是,你可以写:
class incomplete_type;
class complete_type {};
template <size_t N>
struct compile_time_const
{
complete_type& operator==(compile_time_const<N>) const;
template <size_t K>
incomplete_type& operator==(compile_time_const<K>) const;
};
template <typename T>
compile_time_const<0> length_of(T)
{
return compile_time_const<0>();
}
template <typename T, size_t N>
compile_time_const<N> length_of(T (&)[N])
{
return compile_time_const<N>();
}
这是可行的,但是不幸的是断言的语法并不完全自然:
MXT_ASSERT_LEGAL(length_of(arr) == compile_time_const<7>());
您可以将这些技术与第 2.1.4 节中的 fixed _ size
template <typename T, size_t N>
typename fixed_size<N>::type& not_an_array(T (&)[N]); // note: no body
#define length_of(X) sizeof(not_an_array(X))
现在,length_of 又是一个编译时常数,带有一些额外的类型安全检查。故意选择了 not_an_array 这个名称;它通常对用户隐藏,但当参数不正确时,它通常会被打印出来:
class AA {};
int a[5];
int b = length_of(a);
AA aa;
int c = length_of(aa);
error: no matching function for call to 'not_an_array(AA&)'
2.2.4.用函数指针建模概念
比雅尼·斯特劳斯特鲁普记录了以下观点。
一个概念是一个类型上的一组逻辑需求,可以被翻译成语法需求。
例如,“小于可比”类型必须以某种形式实现运算符
复杂的概念可能同时需要几个语法约束。要对模板参数元组施加复杂的约束,只需编写一个静态成员函数,其中所有代码行一起对概念进行建模(换句话说,如果所有代码行都编译成功,就满足了约束)。然后,只需在专用断言类的构造函数中初始化一个伪函数指针(概念函数从不运行),就可以让编译器发出相应的代码:
template <typename T1, typename T2>
struct static_assert_can_copy_T1_to_T2
{
static void concept_check(T1 x, T2 y)
{
T2 z(x); // T2 must be constructable from T1
y = x; // T2 must be assignable from T1
}
static_assert_can_copy_T1_to_T2()
{
void (*f)(T1, T2) = concept_check;
}
};
当您在堆栈上构建实例或从中派生实例时,可以触发概念检查:
template <typename T>
T sqrt(T x)
{
static_assert_can_copy_T1_to_T2<T, double> CHECK1;
}
template <typename T>
class math_operations : static_assert_can_copy_T1_to_T2<T, double>
{};
2.2.5.未实施
虽然 C++0x 允许您从类中“删除”成员函数,但在经典 C++ 中,您有时会希望表达这样一个事实,即不应该提供运算符:
template <typename T>
class X
{
// ...
X<T>& operator= (X<T>& that) { NOT_IMPLEMENTED; }
};
其中最后一条语句是一个失败的静态断言的宏。例如:
#define NOT_IMPLEMENTED MXT_ASSERT(false)
这种习惯用法的基本原理是,成员操作符在第一次使用时是编译器专用的,这正是您想要避免的。
然而,这种技术有风险且不可移植。编译器可以对未使用的模板成员函数发出的诊断数量各不相同。特别是,如果一个表达式不依赖于 T,编译器可以合法地尝试实例化它,所以 MXT_ASSERT(false)可以随时触发。
至少,返回类型应该是正确的:
X<T>& operator= (X<T>& that) { NOT_IMPLEMENTED; return *this; }
第二种选择是使断言依赖于 T:
#define NOT_IMPLEMENTED MXT_ASSERT(sizeof(T)==0)
最后,一个可移植的技术是用一个假注释导致一个链接器错误。这比编译器错误更不可取,因为链接器错误通常不会指向源代码中的某一行。这意味着他们不容易追溯。
#define NOT_IMPLEMENTED
X<T>& operator= (X<T>& that) NOT_IMPLEMENTED;
2.3.标记技术
假设您有一个包含名为 swap 的成员函数的类,您需要添加一个类似的名为 unsafe swap 的成员函数。换句话说,您正在添加一个现有函数的变体。您可以:
-
用相似的名字和相似的签名写一个不同的函数:
public: void swap(T& that); void unsafe_swap(T& that);
-
使用额外的运行时参数添加(一个或多个)原始函数的重载:
private: void unsafe_swap(T& that); public: void swap(T& that); enum swap_style { SWAP_SAFE, SWAP_UNSAFE }; void swap(T& that, swap_style s) { if (s == SWAP_SAFE) this->swap(that); else this->unsafe_swap(that); }
-
用一个额外的静态添加一个原函数的重载无用的自变量:
public: void swap(T& that); void swap(T& that, int); // unsafe swap: call as x.swap(y, 0)
这些选择没有一个是完全令人满意的。第一种是清晰的,但是不能很好地扩展,因为接口可能会增长太多。第二个可能会在运行时付出代价。最后一种不直观,应该记录下来。
相反,TMP 大量使用与语言无关的习惯用法,,它们是对代码生成没有影响的语言结构。
解决这个问题的一个基本技术是通过标签对象进行重载解析。*重载集合的每个成员都有一个不同静态类型的正式未命名参数。
struct unsafe {};
class X
{
public:
void swap(T& that);
void swap(T& that, unsafe);
};
这里有一个不同的例子:
struct naive_algorithm_tag {};
struct precise_algorithm_tag {};
template <typename T>
inline T log1p(T x, naive_algorithm_tag)
{
return log(x+1);
}
template <typename T>
inline T log1p(T x, precise_algorithm_tag)
{
const T xp1 = x+1;
return xp1==1 ? x : x*log(xp1)/(xp1-1);
}
// later...
double t1 = log1p(3.14, naive_algorithm_tag());
double t2 = log1p(0.00000000314, precise_algorithm_tag());
构建一个临时标签的开销并不大(大多数优化编译器什么都不做,就好像你有两个名为 log1p_naive 和 log1p_precise 的函数,每个函数有一个参数)。
因此,让我们更深入地研究一下重载选择的机制。
回想一下,您面临的问题是在编译时选择正确的函数,提供一个人类可读的额外参数。
额外的参数通常是一个空类的未命名实例:
template <typename T>
inline T log1p(T x, selector<true>);
template <typename T>
inline T log1p(T x, selector<false>);
// code #1
return log1p(x, selector<PRECISE_ALGORITHM>());
您可能想知道为什么需要一个类型,而用更简单的语法就可以达到同样的效果:
// code #2
if (USE_PRECISE_ALGORITHM)
return log1p_precise(x);
else
return log1p_standard(x);
标签分派的关键原则是程序只编译绝对必要的函数。在代码#1 中,编译器看到一个函数调用,但是在第二个片段中,有两个。if 决策是固定的,但是无关紧要(因为优化器可能会在以后简化冗余代码)。
事实上,标记调度允许代码在一个有效的函数和一个甚至不能编译的函数之间进行选择(参见下面关于迭代器的段落)。
这并不意味着每一个带有静态决策变量的if 都必须变成一个函数调用。通常,在复杂的算法中,显式语句更清晰:
do_it();
do_it_again();
if (my_options<T>::need_to_clean_up)
{
std::fill(begin, end, T());
}
2.3.1.类型标签
最简单的标签只是空的结构:
struct naive_algorithm_tag {};
struct precise_algorithm_tag {};
template <typename T>
inline T log1p(T x, naive_algorithm_tag);
template <typename T>
inline T log1p(T x, precise_algorithm_tag);
您可以使用模板标签将额外的参数传递给函数:
template <int N>
struct algorithm_precision_level {};
template <typename T, int N>
inline T log1p(T x, algorithm_precision_level<N>);
// ...
double x = log1p(3.14, algorithm_precision_level<4>());
您可以使用派生来构建标记层次结构。
这个例子描述了实际的 STL 实现是做什么的(注意到默认情况下继承是公共的):
struct input_iterator_tag {};
struct output_iterator_tag {};
struct forward_iterator_tag : input_iterator_tag {};
struct bidirectional_iterator_tag : forward_iterator_tag {};
struct random_access_iterator_tag : bidirectional_iterator_tag {};
template <typename iter_t>
void somefunc(iter_t begin, iter_t end)
{
return somefunc(begin, end,
typename std::iterator_traits<iter_t>::iterator_category());
}
template <typename iter_t>
void somefunc(iter_t begin, iter_t end, bidirectional_iterator_tag)
{
// do the work here
}
在这种情况下,双向和 random_access 迭代器将使用 somefunc 的最后一个重载。或者,如果在任何其他迭代器上调用 somefunc,编译器将产生一个错误。
通用实现将处理没有精确匹配的所有标签 9 :
template <typename iter_t, typename tag_t>
void somefunc(iter_t begin, iter_t end, tag_t)
{
// generic implementation:
// any tag for which there's no *exact* match, will fall here
}
可以使用指针使这种通用实现与标记层次结构兼容:
template <typename iter_t>
void somefunc(iter_t begin, iter_t end)
{
typedef
typename std::iterator_traits<iter_t>::iterator_category cat_t;
return somefunc(begin, end, static_cast<cat_t*>(0));
}
template <typename iter_t>
void somefunc(iter_t begin, iter_t end,
std::bidirectional_iterator_tag*)
{
// do the work here
}
template <typename iter_t>
void somefunc(iter_t begin, iter_t end,
void*)
{
// generic
}
重载决策规则将尝试选择丢失较少信息的匹配项。因此,从-到-base的转换比到 void*的转换更匹配。因此,只要有可能(只要迭代器类别至少是双向的),就会采用第二个函数。
另一个有价值的选择是:
template <typename iter_t>
void somefunc(iter_t begin, iter_t end, ...)
{
// generic
}
省略号操作符是最差的匹配,但是当标签是一个类时,它不能被使用(这就是为什么你必须切换到指针和标签)。
2.3.2.使用功能标记
一个稍微复杂一点的选择是使用函数指针作为标签:
enum algorithm_tag_t
{
NAIVE,
PRECISE
};
inline static_value<algorithm_tag_t, NAIVE> naive_algorithm_tag()
{
return 0; // dummy function body: calls static_value<...>(int)
}
inline static_value<algorithm_tag_t, PRECISE> precise_algorithm_tag()
{
return 0; // dummy function body: calls static_value<...>(int)
}
标签不是返回类型,而是函数本身。这个想法不知何故来自 STL 流操纵器(有一个共同的签名)。
typedef
static_value<algorithm_tag_t, NAIVE> (*naive_algorithm_tag_t)();
typedef
static_value<algorithm_tag_t, PRECISE> (*precise_algorithm_tag_t)();
template <typename T>
inline T log1p(T x, naive_algorithm_tag_t);
// later
// line 4: pass a function as a tag
double y = log1p(3.14, naive_algorithm_tag);
因为每个函数都有不同的唯一签名,所以可以使用函数名(相当于函数指针)作为全局常量。内联函数是唯一可以写入头文件而不会导致链接器错误的“常量”。
然后,您可以省略标记中的括号(将上面的第 4 行与前面示例中的对应行进行比较)。函数标记可以在命名空间中分组,也可以是结构的静态成员:
namespace algorithm_tag
{
inline static_value<algorithm_tag_t, NAIVE> naive()
{ return 0; }
inline static_value<algorithm_tag_t, PRECISE> precise()
{ return 0; }
}
或者:
struct algorithm_tag
{
static static_value<algorithm_tag_t, NAIVE> naive()
{ return 0; }
static static_value<algorithm_tag_t, PRECISE> precise()
{ return 0; }
};
double y = log1p(3.14, algorithm_tag::naive);
函数指针的另一个显著优点是,您可以对相同的运行时和编译时算法采用统一的语法:
enum binary_operation
{
sum, difference, product, division
};
#define mxt_SUM x+y
#define mxt_DIFF x-y
#define mxt_PROD x*y
#define mxt_DIV x/y
// define both the tag and the worker function with a single macro
#define mxt_DEFINE(OPCODE, FORMULA) \
\
inline static_value<binary_operation, OPCODE> static_tag_##OPCODE() \
{ \
return 0; \
} \
\
template <typename T> \
T binary(T x, T y, static_value<binary_operation, OPCODE>) \
{ \
return (FORMULA); \
}
mxt_DEFINE(sum, mxt_SUM);
mxt_DEFINE(difference, mxt_DIFF);
mxt_DEFINE(product, mxt_PROD);
mxt_DEFINE(division, mxt_DIV);
template <typename T, binary_operation OP>
inline T binary(T x, T y, static_value<binary_operation, OP> (*)())
{
return binary(x, y, static_value<binary_operation, OP>());
}
这是函数静态选择所需的常用机制。由于您定义重载的方式,下面的调用产生相同的结果(否则,用户会感到非常惊讶),即使它们不相同。第一种是首选:
double a1 = binary(8.0, 9.0, static_tag_product);
double a2 = binary(8.0, 9.0, static_tag_product());
但是,使用相同的工具,您可以进一步细化功能,并添加类似的运行时算法 10 :
template <typename T>
T binary(T x, T y, const binary_operation op)
{
switch (op)
{
case sum: return mxt_SUM;
case difference: return mxt_DIFF;
case product: return mxt_PROD;
case division: return mxt_DIV;
default:
throw std::runtime_error("invalid operation");
}
}
后者将被援引为:
double a3 = binary(8.0, 9.0, product);
这可能看起来相似,但它是一个完全不同的功能。它共享一些实现(在这种情况下,四个内核宏),但是它在运行时选择正确的一个。
- 操纵器(参见 1.4.7 节)类似于用作编译时常数的函数。然而,它们也有一些不同之处:
- 操纵器更加通用。所有操作都有相似的签名(必须得到流对象的支持),任何用户都可以提供更多的签名,但是它们涉及一些运行时调度。
- 函数常量是一个固定的集合,但是由于签名和重载操作符之间是一对一的匹配,所以没有运行时工作。
2.3.3.标签迭代
用静态值标记的函数的一个有用特性是,通过处理位和编译时计算,有可能编写自动展开一些“迭代调用”的函数。
例如,以下函数用零填充 C 数组:
template <typename T, int N>
void zeroize_helper(T* const data, static_value<int, N>)
{
zeroize_helper(data, static_value<int, N-1>());
data[N-1] = T();
}
template <typename T>
void zeroize_helper(T* const data, static_value<int, 1>)
{
data[0] = T();
}
template <typename T, int N>
void zeroize(T (&data)[N])
{
zeroize_helper(data, static_value<int, N>());
}
您可以交换两行并向后迭代:
template <typename T, int N>
void zeroize_helper(T* const data, static_value<int, N>)
{
data[N-1] = T();
zeroize_helper(data, static_value<int, N-1>());
}
这种展开被称为线性和两个指数,你可以有指数展开。为简单起见,假设 N 是 2 的幂:
template <int N, int M>
struct index
{
};
template <typename T, int N, int M>
void zeroize_helper(T* const data, index<N, M>)
{
zeroize_helper(data, index<N/2, M>());
zeroize_helper(data, index<N/2, M+N/2>());
}
template <typename T, int M>
void zeroize_helper(T* const data, index<1, M>)
{
data[M] = T();
}
template <typename T, int N>
void zeroize(T (&data)[N])
{
zeroize_helper(data, index<N, 0>());
}
double test[8];
zeroize(test);
图 2-1。N=8 时的指数展开
作为一种更复杂的情况,您可以迭代一组位。
假设枚举以复杂性递增的顺序描述了一些启发式算法:
enum
{
ALGORITHM_1,
ALGORITHM_2,
ALGORITHM_3,
ALGORITHM_4,
// ...
};
对于枚举中的每个值,都有一个执行检查的函数。当一切正常时,函数返回 true 如果检测到问题,函数返回 false:
bool heuristic([[args]], static_value<size_t, ALGORITHM_1>);
bool heuristic([[args]], static_value<size_t, ALGORITHM_2>);
// ...
如果您想用一个函数调用以递增的顺序运行部分或全部检查,该怎么办?
首先,使用 2 的幂修改枚举:
enum
{
ALGORITHM_1 = 1,
ALGORITHM_2 = 2,
ALGORITHM_3 = 4,
ALGORITHM_4 = 8,
// ...
};
用户将使用一个静态值作为标签,算法将与“按位或”(or +)结合。
typedef static_value<size_t, ALGORITHM_1 | ALGORITHM_4> mytag_t;
// this is the public function
template <size_t K>
bool run_heuristics([[args]], static_value<size_t, K>)
{
return heuristic([[args]],
static_value<size_t, K>(),
static_value<size_t, 0>());
}
以下是“私有”实现的详细信息:
#define VALUE(K) static_value<size_t, K>
template <size_t K, size_t J>
bool heuristic([[args]], VALUE(K), VALUE(J))
{
static const size_t JTH_BIT = K & (size_t(1) << J);
// JTH_BIT is either 0 or a power of 2.
// try running the corresponding algorithm, first.
// if it succeeds, the && will continue with new tags,
// with the J-th bit turned off in K and J incremented by 1
return
heuristic([[args]], VALUE(JTH_BIT)()) &&
heuristic([[args]], VALUE(K-JTH_BIT)(), VALUE(J+1)());
}
template <size_t J>
bool heuristic([[args]], VALUE(0), VALUE(J))
{
// finished: all bits have been removed from K
return true;
}
template <size_t K>
bool heuristic([[args]], VALUE(K))
{
// this is invoked for all bits in K that do not have
// a corresponding algorithm, and when K=0
// i.e. when a bit in K is off
return true;
}
2.3.4.标签和继承
一些类从它们的基类继承额外的重载。因此,分派标记调用的对象可能不知道哪个基将应答。
假设您有一个简单的分配器类,在给定固定大小的情况下,它将分配一个该长度的内存块。
template <size_t SIZE>
struct fixed_size_allocator
{
void* get_block();
};
现在,您将它包装在一个更大的分配器中。为简单起见,假设大多数内存请求的大小等于 2 的幂,您可以组装一个 compound_pool
这种分配的语法是 11 :
compound_pool<64> A;
double* p = A.allocate<double>();
这个想法的梗概是这样的。复合池
更准确地说,每个类都有一个返回分配器引用或指针的 pick 函数。
调用标签是 static_value <size_t n="">,其中 N 是请求内存块的大小。</size_t>
template <size_t SIZE>
class compound_pool;
template < >
class compound_pool<0>
{
protected:
template <size_t N>
void* pick(static_value<size_t, N>)
{
return ::operator new(N);
}
};
template <size_t SIZE>
class compound_pool : compound_pool<SIZE/2>
{
fixed_size_allocator<SIZE> p_;
protected:
using compound_pool<SIZE/2>::pick;
fixed_size_allocator<SIZE>& pick(static_value<SIZE>)
{
return p_;
}
public:
template <typename object_t>
object_t* allocate()
{
typedef static_value<size_t, sizeof(object_t)> selector_t;
return static_cast<object_t*>(get_pointer(this->pick(selector_t())));
}
private:
template <size_t N>
void* get_pointer(fixed_size_allocator<N>& p)
{
return p.get_block();
}
void* get_pointer(void* p)
{
return p;
}
};
注意 using 声明,它使每个类中所有重载的 pick 函数可见。在这里,compound _ pool<0>:::pick 的优先级较低,因为它是一个函数模板,但它总是成功。此外,由于它返回一个不同的对象,它最终选择了一个不同的 get_pointer。
1 熟悉现代 C++ 的读者会认识到,这样的 typedef 已经存在于命名空间 std 中。我将在第 12.1 节对这一论点进行更多的阐述。
2 你可以让选择器从另一个中派生出来,但是你不能明确地假设它们是可转换的。在 C++0x 下,你也可以用新的 using 符号写一个模板 typedef(见 12.6 节)。
3 参见 4.12 节。
4 只授予弱排序:1 = sizeof(char)≤sizeof(short)≤sizeof(int)≤sizeof(long)。
5 根据第 4.2.1 节中提供的材料,该备注将变得清晰。
6
7 但是,少数编译器无论如何都会在 MXT_INSTANCE_OF 上生成警告,报告不允许空引用。
8 实际上,解引用迭代器返回的是 STD::iterator _ traits
9 特别是,这也将处理随机访问迭代器。也就是说,它盲目地忽略了基本/派生标签层次结构。
10 这个例子预见了 7.3 节的观点。
11 有意省略了解除分配。*
三、静态编程
模板非常善于迫使编译器和优化器只在生成可执行程序时执行一些工作。根据定义,这叫做静态工作。这与动态工作相反,动态工作指的是程序运行时所做的事情。
有些活动必须在运行时之前完成(计算整数常量),有些活动对运行时有影响(为函数模板生成机器码,稍后执行)。
TMP 可以产生两种类型的代码——元函数,完全是静态的(例如,元函数 unsigned_integer
本节讨论编写高效元函数的技巧。
3.1.使用预处理器进行静态编程
编写自己做出决定的程序的经典方式是通过预处理指令。C++ 预处理器可以执行一些整数计算测试,并删除不合适的代码部分。
考虑下面的例子。您希望将固定长度的无符号整数类型(如 uint32_t)定义为正好 32 位宽,并对任何 2 的幂的位长做同样的事情。
规定
template <size_t S>
struct uint_n;
#define mXT_UINT_N(T,N) \
template <> struct uint_n<N> { typedef T type; }
并对当前平台上确实支持的所有大小的 uint_n 进行专门化。
如果用户尝试 uint _ n<16>:::type,但没有合适的类型,她将得到一个适当的、可理解的编译器错误(关于缺少模板专门化)。
所以你得让预处理器通过试错法 1 算出尺寸:
#include <climits>
#define MXT_I32BIT 0xffffffffU
#define MXT_I16BIT 0xffffU
#define MXT_I8BIT 0xffU
#if (UCHAR_MAX == MXT_I8BIT)
mXT_UINT_N(unsigned char,8);
#endif
#if (USHRT_MAX == MXT_I16BIT)
mXT_UINT_N(unsigned short,16);
#elif UINT_MAX == MXT_I16BIT
mXT_UINT_N(unsigned int,16);
#endif
#if (UINT_MAX == MXT_I32BIT)
mXT_UINT_N(unsigned int,32);
#elif (ULONG_MAX == MXT_I32BIT)
mXT_UINT_N(unsigned long,32);
#endif
这段代码可以工作,但是它相当脆弱,因为预处理器和编译器之间的交互是有限的。 2
请注意,这不仅仅是一个通用风格的争论(宏与模板),而是一个正确性的问题。如果预处理器删除了源文件的某些部分,那么在宏定义改变之前,编译器没有机会诊断所有的错误。另一方面,如果 TMP 决策依赖于编译器看到一整套模板,那么它只实例化其中的一部分。
注预处理器并不“邪恶”。
像前面的例子一样,基于预处理器的“元编程”通常会编译得更快,并且——如果简单的话——具有很高的可移植性。许多高端服务器仍然附带不支持基于语言(模板)的元编程的旧的或定制的编译器。另一方面,我应该提到的是,虽然编译器倾向于 100%符合标准,但对于预处理程序来说却不是这样。因此,晦涩的预处理器技巧可能无法产生预期的结果,并且由于误用预处理器而导致的错误很难被发现。 3
第 3.6.10 节展示并解释了不依赖于预处理器的 uint_n 实现。
3.2.编译复杂度
当类模板被实例化时,编译器生成:
- 班级级别的每个成员签名
- 所有静态常数和类型定义
- 只有严格必要的函数体
如果在同一个编译单元中再次需要同一个实例,可以通过查找找到它(这不需要特别高效,但仍然比实例化快)。
例如,给定以下代码:
template <size_t N>
struct sum_of_integers_up_to
{
static const size_t value = N + sum_of_integers_up_to<N-1>::value;
};
template <>
struct sum_of_integers_up_to<0>
{
static const size_t value = 0;
};
int n9 = sum_of_integers_up_to<9>::value; // mov dword ptr [n9],2Dh
int n8 = sum_of_integers_up_to<8>::value; // mov dword ptr [n8],24h
n9 的初始化有 10 个模板实例化的开销,但是 n8 的后续初始化有一个查找的开销(不是 9)。正如汇编代码所示,这两条指令对运行时没有任何影响。
通常,大多数元函数都是使用递归实现的。编译复杂度是元函数本身递归需要的模板实例的数量。
这个例子有线性复杂度,因为 X < N >的实例化需要 X < N-1 >...X < 0 >。虽然您通常希望寻找复杂度最低的实现(以减少编译时间,而不是执行时间),但是如果有大量的代码重用,您可以跳过这种优化。因为查找的原因,X < N >的第一次实例化会很昂贵,但是它允许 X < M >在同一个翻译单元中免费实例化,如果 M < N。
考虑这个优化的低复杂度实施的例子:
template <size_t N, size_t K>
struct static_raise
{
static const size_t value = /* N raised to K */;
};
平凡的实现具有线性复杂度:
template <size_t N, size_t K>
struct static_raise
{
static const size_t value = N * static_raise<N, K-1>::value;
};
template <size_t N>
struct static_raise<N, 0>
{
static const size_t value = 1;
};
为了获得 static_raise
最终 static_raise
然而,有一个公式只需要大约 log(K)个中间类型:
注意如果指数是 2 的幂,通过重复平方可以节省大量乘法运算。要计算 X 8 ,如果可以只存储中间结果,只需要三次乘法。由于 X8=((X2)2)2,需要执行
t = x*x; t = t*t; t = t*t; return t;
通常,您可以递归地使用标识:
#define MXT_M_SQ(a) ((a)*(a))
template <size_t N, size_t K>
struct static_raise;
template <size_t N>
struct static_raise<N, 0>
{
static const size_t value = 1;
};
template <size_t N, size_t K>
struct static_raise
{
private:
static const size_t v0 = static_raise<N, K/2>::value;
public:
static const size_t value = MXT_M_SQ(v0)*(K % 2 ? N : 1);
};
注意 MXT_M_SQ 的使用(参见第 1.3.2 节)。
最后一句话:仅仅因为元函数的自然实现包含递归,并不意味着任何递归实现都是最优的。 4
假设 N 是一个以 10 为基数的整数,你想提取第 I 个数字(姑且认为数字 0 是最右边的)作为数字:::
template <int I, int N>
struct digit;
很明显,你有两个选择。一个是主类本身的“完全”递归
template <int I, int N>
struct digit
{
static const int value = digit<i-1, N/10>::value;
};
template <int N>
struct digit<0, N>
{
static const int value = (N % 10);
};
或者可以引入一个辅助类主类:
template <int I>
struct power_of_10
{
static const int value = 10 * power_of_10<I-1>::value;
};
template <>
struct power_of_10<0>
{
static const int value = 1;
};
template <int I, int N>
struct digit
{
static const int value = (N / power_of_10<I>::value) % 10;
};
虽然第一种实现显然更简单,但第二种实现的伸缩性更好。如果你需要从 100 个不同的随机数中抽取第 8 个位,前者将产生 800 个不同的特化,因为重用的机会非常低。从数字<812345678>开始,编译器必须产生序列数字<71234567>,数字<6123456>...,并且这些类中的每一个很可能在整个程序中只出现一次。
另一方面,后一个版本产生 8 个不同的 10 的特殊幂,每次都可以重用,所以编译器的工作负载只是 100+10 个类型。
3.3.经典元编程习惯用法
元函数可以被看作是接受一个或多个类型并返回类型或常量的函数。在这一节中,您将看到如何实现一些基本操作。
二元运算符由两个变量的元函数代替。概念 T1==T2 成为 typeequal < T1,T2 > ::value:
template <typename T1, typename T2>
struct typeequal
{
static const bool value = false;
};
template <typename T>
struct typeequal<T, T>
{
static const bool value = true;
};
只要有可能,您应该从保存结果的基本类派生,而不是引入新的类型/常数。请记住,公共继承是由 struct 隐含的
template <typename T1, typename T2>
struct typeequal : public selector<false> // redundant
{
};
template <typename T>
struct typeequal<T, T> : selector<true> // public
{
};
三元运算符测试?T1 : T2 成为 type if
template <bool STATEMENT, typename T1, typename T2>
struct typeif
{
typedef T1 type;
};
template <typename T1, typename T2>
struct typeif<false, T1, T2>
{
typedef T2 type;
};
或者,根据前面的准则:
template <bool STATEMENT, typename T1, typename T2>
struct typeif : instance_of<T1>
{
};
template <typename T1, typename T2>
struct typeif<false, T1, T2> : instance_of<T2>
{
};
派生的强烈动机是更容易使用标记技术。因为您经常将元函数结果“嵌入”在选择器中,所以将元函数本身用作选择器会更容易。假设您有两个用随机元素填充一个范围的函数:
template <typename iterator_t>
void random_fill(iterator_t begin, iterator_t end, selector<false>)
{
for (; begin != end; ++begin)
*begin = rand();
}
template <typename iterator_t>
void random_fill(iterator_t begin, iterator_t end, selector<true>)
{
for (; begin != end; ++begin)
*begin = 'A' + (rand() % 26);
}
比较调用:
random_fill(begin, end, selector<typeequal<T, char*>::value>());
用更简单的 5 :
random_fill(begin, end, typeequal<T, char*>());
注意好奇的注意到,在它们的保护宏中存储版本号的头文件可以在类型 if 中使用。比较以下片段
#include "myheader.hpp"
typedef
typename typeif<MXT_MYHEADER_==0x1000, double, float>::type float_t;
#if MXT_MYHEADER_ == 0x1000
typedef double float_t;
#else
typedef float float_t;
#endif
如果 MXT_MYHEADER_ 未定义,第一个代码段将不会编译。预处理器会表现得好像变量为 0 一样。
3.3.1.静态短路
作为模板递归的案例研究,让我们比较静态和动态操作符的伪代码:
template <typename T>
struct F : typeif<[[CONDITION]], T, typename G<T>::type>
{
};
int F(int x)
{
return [[CONDITION]] ? x : G(x);
}
这些陈述不类似:
- 运行时语句被短路。除非必要,否则它不会执行代码,所以 G(x)可能永远不会运行。
- 静态操作符将总是编译所有提到的实体,只要提到它们的一个成员。所以第一个 F 会触发 G
:::type 的编译,不考虑结果被使用的事实(也就是说,即使条件为真)。
不存在自动静态短路。如果被低估了,这可能会增加构建时间而没有额外的好处,而且可能不会被注意到,因为结果无论如何都是正确的。
可以使用额外的“间接方式”重写该表达式:
template <typename T>
struct F
{
typedef
typename typeif<[[CONDITION]], instance_of<T>, G<T> >::type
aux_t;
typedef typename aux_t::type type;
};
这里只提 G
所以,如果条件为真,G
总结一下:
- 尽可能延迟访问成员
- 包装物品以利用界面
相同的优化适用于常数:
static const size_t value = [[CONDITION]] ? 4 : alignment_of<T>::value;
typedef typename
typeif<[[CONDITION]], static_value<size_t, 4>, alignment_of<T>>::type
aux_t;
static const size_t value = aux_t::value;
起初,看起来似乎不需要一些特殊的逻辑运算符,因为模板 6 中允许整数上的所有默认运算符:
template <typename T1, typename T2>
struct naive_OR
{
static const bool value = (T1::value || T2::value); // ok, valid
};
C++ 里经典的逻辑运算符都短路了;也就是说,如果第一个操作符足以返回一个结果,他们就不会对第二个操作符求值。类似地,你可以写一个静态 OR,它不需要编译它的第二个参数。如果 T1::value 为 true,则 T2::value 永远不会被访问,甚至可能不存在(以类似方式获得)。
// if (T1::value is true)
// return true;
// else
// return T2::value;
template <bool B, typename T2>
struct static_OR_helper;
template <typename T2>
struct static_OR_helper<false, T2> : selector<T2::value>
{
};
template <typename T2>
struct static_OR_helper<true, T2> : selector<true>
{
};
template <typename T1, typename T2>
struct static_OR : static_OR_helper<T1::value, T2>
{
};
3.4.隐藏模板参数
一些类模板可能有未记录的模板参数,通常是自动推导出来的,它们默默地选择正确的专门化。这是标签分发的配套技术,下面是一个例子:
template <typename T, bool IS_SMALL_OBJ = (sizeof(T)<sizeof(void*))>
class A;
template <typename T>
class A<T, true>
{
// implementation follows
};
template <typename T>
class A<T, false>
{
// implementation follows
};
通常,的用户将接受默认设置:
A<char> c1;
A<char, true> c2; // exceptional case. do at own risk
下面是出现在[3]中的一个例子的变体。
template <size_t N>
struct fibonacci
{
static const size_t value =
fibonacci<N-1>::value + fibonacci<N-2>::value;
};
template <>
struct fibonacci<0>
{
static const size_t value = 0;
};
template <>
struct fibonacci<1>
{
static const size_t value = 1;
};
可以使用隐藏的模板参数重写它:
template <size_t N, bool TINY_NUMBER = (N<2)>
struct fibonacci
{
static const size_t value =
fibonacci<N-1>::value + fibonacci<N-2>::value;
};
template <size_t N>
struct fibonacci<N, true>
{
static const size_t value = N;
};
为了防止默认值被更改,您可以通过添加后缀 _helper 来重命名原始类,从而在中间引入一个层:
template <size_t N, bool TINY_NUMBER>
struct fibonacci_helper
{
// all as above
};
template <size_t N>
class fibonacci : fibonacci_helper<N, (N<2)>
{
};
3.4.1.隐藏参数上的静态递归
让我们计算一个无符号整数 x 的最高位。假设 x 的类型为 size_t,如果 x==0,它通常会返回-1。
非递归算法将是:set N = size _ t 的比特数;测试第 N-1 位,然后第 N-2 位...等等,直到找到一个非零位。
首先,像往常一样,一个天真的实现:
template <size_t X, size_t K>
struct highest_bit_helper
{
static const int value =
((X >> K) % 2) ? K : highest_bit_helper<X, K-1>::value;
};
template <size_t X>
struct highest_bit_helper<X, 0>
{
static const int value = (X % 2) ? 0 : -1;
};
template <size_t X>
struct static_highest_bit
: highest_bit_helper<X, CHAR_BIT*sizeof(size_t)-1>
{
};
正如所写的那样,它是可行的,但是编译器可能需要为每个静态计算生成大量不同的类(也就是说,对于任何 X,您传递给 static_highest_bit)。
首先,您可以使用二分法重做算法。假设 X 有 N 位,将其分成上半部分和下半部分(U 和 L ),分别有(N-N/2)和(N/2)位。如果 U 是 0,用 L 代替 X;否则,将 X 替换为 U,并记住将结果递增(N/2) 7 :
在伪代码中:
size_t hibit(size_t x, size_t N = CHAR_BIT*sizeof(size_t))
{
size_t u = (x>>(N/2));
if (u>0)
return hibit(u, N-N/2) + (N/2);
else
return hibit(x, N/2);
}
这意味着:
template <size_t X, int N>
struct helper
{
static const size_t U = (X >> (N/2));
static const int value =
U ? (N/2)+helper<U, N-N/2>::value : helper<X, N/2>::value;
};
正如所写的,每个助手
静态短路可以减少编译时间,如果将所有算法都移到类型内部,效果会更好。 8
template <size_t X, int N>
struct helper
{
static const size_t U = (X >> (N/2));
static const int value = (U ? N/2 : 0) +
helper<(U ? U : X), (U ? N-N/2 : N/2)>::value;
};
这肯定不太清楚,但对编译器来说更方便。
因为 N 是 X 的位数,所以最初 N>0。
当 N==1 时,可以终止静态递归:
template <size_t X>
struct helper<X, 1>
{
static const int value = X ? 0 : -1;
};
最后,您可以使用 static_value 的派生来存储结果:
template <size_t X>
struct static_highest_bit
: static_value<int, helper<X, CHAR_BIT*sizeof(size_t)>::value>
{
};
递归深度是固定的和对数的。static_highest_bit
3.4.2.访问主模板
伪参数可以允许专门化回调主模板。
假设您有两个算法,一个用于计算 cos(x ),另一个用于计算 sin(x ),其中 x 是任意浮点类型。最初,代码组织如下:
template <typename float_t>
struct trigonometry
{
static float_t cos(const float_t x)
{
// ...
}
static float_t sin(const float_t x)
{
// ...
}
};
template <typename float_t>
inline float_t fast_cos(const float_t x)
{
return trigonometry<float_t>::cos(x);
}
template <typename float_t>
inline float_t fast_sin(const float_t x)
{
return trigonometry<float_t>::sin(x);
}
后来有人为 cos
您可以为 float 专门化/重载 fast_cos,或者使用隐藏模板参数,如下所示:
template <typename float_t, bool = false>
struct trigonometry
{
static float_t cos(const float_t x)
{
// ...
}
static float_t sin(const float_t x)
{
// ...
}
};
template <>
struct trigonometry<float, false>
{
static float_t cos(const float_t x)
{
// specialized algorithm here
}
static float_t sin(const float_t x)
{
// calls the general template
return trigonometry<float, true>::sin(x);
}
};
请注意,在专门化该类时,并不要求您编写
template <>
struct trigonometry<float>
{
因为第二个参数的默认值可以从声明中得知。
任何专门化都可以通过显式地将 Boolean 设置为 true 来访问相应的通用函数。
这项技术将在 7.1 节再次出现。
一个类似的技巧可以方便地使局部专门化变得明确。
C++ 不允许专门化一个模板两次,即使专门化是相同的。特别是,如果您混合使用标准类型定义和整数的大小写,代码会变得不可移植:
template <typename T>
struct is_integer
{
static const bool value = false;
};
template < > struct is_integer<short>
{ static const bool value = true; };
template < > struct is_integer<int>
{ static const bool value = true; };
template < > struct is_integer<long>
{ static const bool value = true; };
template < > struct is_integer<ptrdiff_t> // problem:
{ static const bool value = true; }; // may or may not compile
如果 ptrdiff_t 是第四种类型,比如 long long,那么所有的专门化都是不同的。或者,如果 ptrdiff_t 只是一个 long 类型定义,那么代码是不正确的。相反,这是可行的:
template <typename T, int = 0>
struct is_integer
{
static const bool value = false;
};
template <int N> struct is_integer<short, N>
{ static const bool value = true; };
template <int N> struct is_integer<int , N>
{ static const bool value = true; };
template <int N> struct is_integer<long , N>
{ static const bool value = true; };
template <>
struct is_integer<ptrdiff_t>
{
static const bool value = true;
};
因为 is_integer <ptrdiff_t>比 is_integer
这种技术的伸缩性不好,但是可以通过添加更多未命名的参数来扩展到少量的类型定义。这个例子使用了 int,但是任何东西都可以,比如 bool = false 或者 typename = void。
template <typename T, int = 0, int = 0>
struct is_integer
{
static const bool value = false;
};
template <int N1, int N2>
struct is_integer<long, N1, N2>
{ static const bool value = true; };
template <int N1>
struct is_integer<ptrdiff_t, N1>
{ static const bool value = true; };
template < >
struct is_integer<time_t>
{ static const bool value = true; };
3.4.3.歧义消除
在 TMP 中,生成从同一个基(间接)派生几次的类是很常见的。现在还不是列举完整例子的时候,所以这里有一个简单的例子:
template <int N>
struct A {};
template <int N>
struct B : A<N % 2>, B<N / 2> {};
template <>
struct B<0> {};
例如,B <9>的继承链如图 3-1 中的所示。
图 3-1 。B 的继承链< 9 >
注意,A <0>和 A <1>出现了几次。这是允许的,除了你不能显式或隐式地将 B <9>转换为 A <0>或<1>:
template <int N>
struct A
{
int getN() { return N; }
};
template <int N>
struct B : A<N % 2>, B<N / 2>
{
int doIt() { return A<N % 2>::getN(); } // error: ambiguous
};
您可以做的是添加一个隐藏的模板参数,以便不同级别的继承对应于物理上不同的类型。
最流行的歧义消除参数是计数器:
template <int N, int FAKE = 0>
struct A {};
template <int N, int FAKE = 0>
struct B : A<N % 2, FAKE<sup class="calibre7">11</sup>>, B<N / 2, FAKE+1> {};
template <int FAKE>
struct B<0, FAKE> {};
图 3-2。使用计数器修改 B <9>的继承链
另一个常用的消歧标记是 this 类型:
template <int N, typename T>
struct A {};
template <int N>
struct B : A<N % 2, B<N> >, B<N/2> {};
template <>
struct B<0> {};
图 3-3。使用标签类型修改 B <9>的继承链
这个观点在 5.2 节中被广泛使用
3.5.特质
Traits 类(或简称 traits)是一个静态函数、类型和常量的集合,抽象了一个类型 T 的公共接口,更准确地说,对于所有代表相同概念的 T,traits < T >是一个类模板,允许你统一操作 T。特别是,所有特征< T >都有相同的公共接口。 12
使用 traits,可以通过部分或完全忽略 T 类型的公共接口来处理它。这使得 traits 成为算法的最佳构建层。
为什么忽略 T 的公共接口?主要原因是因为它可能没有或可能不合适。
假设 T 表示一个“字符串”,你想得到 T 的一个实例的长度,T 可能是 const char*或者 std::string,但是你想让同一个调用对两者都有效。否则,将无法编写模板字符串函数。此外,0 作为“字符”可能对某些 T 有特殊的意义,但不是对所有 T。
第一个关于特质的严格定义是内森·迈尔斯在 1995 年写的一篇文章。
这种技术的动机是,当编写类模板或函数时,您会意识到一些类型、常量或原子操作是“main”模板参数的参数。
所以你可以放入额外的模板参数,但这通常是不切实际的。您还可以将参数分组到一个 traits 类中。下一个例子和下面的句子都引自迈尔斯的文章 14 :
因为用户从未提及,【traits class】名称可以很长,并且是描述性的。
template <typename char_t>
struct ios_char_traits
{
};
template <>
struct ios_char_traits<char>
{
typedef char char_type;
typedef int int_type;
static inline int_type eof() { return EOF; }
};
template <>
struct ios_char_traits<wchar_t>
{
typedef wchar_t char_type;
typedef wint_t int_type;
static inline int_type eof() { return WEOF; }
};
默认特征类模板为空。对于一个未知的字符类型,任何人都可以说些什么?然而,对于真实的字符类型,您可以专门化模板并提供有用的语义。
要在流中加入一个新的字符类型,你只需要为新的类型指定 ios_char_traits 。
注意 ios_char_traits 没有数据成员;它只提供公共定义。现在你可以定义 streambuf 模板 :
template <typename char_t>
class basic_streambuf
注意它只有一个模板参数,用户感兴趣的那个。
事实上,Myers 用一个正式的定义和一个有趣的观察总结了他的文章:
特质类:
一个用来代替模板参数的类。作为一个类,它集合了有用的类型和常量。作为一个模板,它为解决所有软件问题的“额外间接层”提供了一个途径。
这种技术在模板必须应用于本机类型,或者不能根据模板操作的需要为其添加成员的任何类型的情况下非常有用。
特征类可以是“全局的”或“局部的”。全局特征在系统中是简单可用的,它们可以在任何地方自由使用。特别是,一个全局 traits 类的所有专门化都有系统范围(所以专门化在任何地方都会自动使用)。事实上,当特征表达平台的属性时,这种方法是首选的。
template <typename char_t>
class basic_streambuf
{
typedef typename ios_char_traits<char_t>::int_type int_type;
...
};
注意例如,你可以访问浮点型的最大无符号整数。考虑以下伪代码:
template <typename T>
struct largest;
template <>
struct largest<int>
{
typedef long long type;
};
template <>
struct largest<float>
{
typedef long double type;
};
template <>
struct largest<unsigned>
{
typedef unsigned long long type;
};
显然,像 maximum
更灵活的方法是使用局部特征,将适当的类型作为附加参数传递给每个模板实例(默认为全局值)。
template <typename char_t, typename traits_t = ios_char_traits<char_t> >
class basic_streambuf
{
typedef typename traits_t::int_type int_type;
...
};
接下来的部分关注一种特殊的特征——纯静态特征,它不包含函数,只包含类型和常量。你会在 4.2 节回到这个论点。
3.5.1.类型特征
有些 traits 类只提供 typedefs,所以它们确实是多值元函数。作为一个例子,再次考虑 std::iterator_traits。
类型特征 15 是元函数的集合,提供关于给定类型的限定符的信息和/或改变这样的限定符。信息可以通过 traits 内部的静态机制推导出来,可以通过 traits 类的完全/部分专门化显式提供,也可以由编译器本身提供。 16
template <typename T>
struct is_const : selector<false>
{
};
template <typename T>
struct is_const<const T> : selector<true>
{
};
注意今天,类型特征被拆分以减少编译时间,但历史上它们是具有许多静态常数的大型整体类。
template <typename T>
struct all_info_together
{
static const bool is_class = true;
static const bool is_pointer = false;
static const bool is_integer = false;
static const bool is_floating = false;
static const bool is_unsigned = false;
static const bool is_const = false;
static const bool is_reference = false;
static const bool is_volatile = false;
};
通常,traits 有一个保守默认的通用实现,包括对类型类有意义的值的部分专门化和对单个类型定制的完全专门化。
template <typename T>
struct add_reference
{
typedef T& type;
};
template <typename T>
struct add_reference<T&>
{
typedef T& type;
};
template < >
struct add_reference<void>
{
// reference to void is illegal. don't put anything here<sup class="calibre7">17</sup>
};
特征通常是递归的:
template <typename T>
struct is_unsigned_integer : selector<false>
{
};
template <typename T>
struct is_unsigned_integer<const T> : is_unsigned_integer<T>
{
};
template <typename T>
struct is_unsigned_integer<volatile T> : is_unsigned_integer<T>
{
};
template < >
struct is_unsigned_integer<unsigned int> : selector<true>
{
};
template < >
struct is_unsigned_integer<unsigned long> : selector<true>
{
};
// add more specializations...
Traits 可以使用继承然后有选择地隐藏一些成员:
template <typename T>
struct integer_traits;
template <>
struct integer_traits<int>
{
typedef long long largest_type;
typedef unsigned int unsigned_type;
};
template <>
struct integer_traits<long> : integer_traits<int>
{
// keeps integer_traits<int>::largest_type
typedef unsigned long unsigned_type;
};
注意在 C++ 中,模板基类不在名称解析范围内:
template <typename T>
struct BASE
{
typedef T type;
};
template <typename T>
struct DER : public BASE<T>
{
type t; // error: 'type' is not in scope
};
然而,从静态的角度来看,DER是否包含类型成员:
template <typename T>
struct typeof
{
typedef typename T::type type;
};
typeof< DER<int> >::type i = 0; // ok: int i = 0
如果没有仔细设计,类型特征很容易受到困难的概念问题的影响,因为 C++ 类型系统比它看起来要复杂得多:
template <typename T>
struct is_const : selector<false>
{
};
template <typename T>
struct is_const<const T> : selector<true>
{
};
template <typename T>
struct add_const : instance_of<const T>
{
};
template <typename T>
struct add_const<const T> : instance_of<const T>
{
};
以下是一些奇怪之处:
-
如果 N 是编译时常数,T 是类型,那么可以形成两种不同的数组类型:T [N]和 T []。 18
-
像 const 这样应用于数组类型的限定符的行为有点奇怪。如果 T 是一个数组,例如 double [4],那么 const T 就是“四个 const double 的数组”,而不是“四个 double 的 const 数组”。具体来说,const T 是而不是 const:
typedef double T1; typedef add_const<T1>::type T2; T2 x = 3.14; // x has type const double bool b1 = is_const<T2>::value; // b1 is true typedef double T3[4]; typedef add_const<T3>::type T4; // T4 is "array of 4 const double"... T4 a = { 1,2,3,4 }; bool b2 = is_const<T4>::value; // ...which does not match "const T" // so b2 is false
因此,您应该添加更多的专门化:
template <typename T, size_t N>
struct is_const<const T [N]>
{
static const bool value = true;
};
template <typename T >
struct is_const<const T []>
{
static const bool value = true;
};
有两种可能的标准可以验证类型:
- 匹配就满足了;例如,const int 匹配 T==int 的 const T。
- 满足逻辑测试;例如,如果 const T 和 T 是同一类型,你可以说 T 是 const。
C++ 类型的系统非常复杂,在大多数情况下,标准看起来是相同的,但仍然不完全相同。通常,每当出现这样的逻辑问题时,解决方案将来自对您的需求的更精确的推理。对于任何 T,is _ const
是否应该添加返回 true 的 is_const
在 C++ 中,对象可以有不同程度的常量。更具体地说,它们可以是
- 可分配的
- 不变的
- 常数
被赋值 是一个句法属性。可赋值对象可以位于运算符=的左侧。常量引用是不可赋值的。然而实际上,T &在 T 为时是可赋值的。(顺便说一下,赋值会改变被引用的对象,而不是引用,但这无关紧要。)
被不可变 是一个逻辑属性。不可变对象在构造后不能被改变,因为它是不可赋值的,或者因为它的赋值不会改变实例的状态。由于不能将引用“指向”另一个对象,所以引用是不可变的。
是一个纯粹的语言属性。如果一个对象的类型与某个 T 的常量 T 匹配,那么这个对象就是常量。常量对象可能有一个简化的接口,operator=可能是受限制的成员函数之一。
引用不是唯一既不可变又可赋值的实体。这种情况可以用自定义操作符= 来重现。
template <typename T>
class fake_ref
{
T* const ptr_;
public:
// ...
const fake_ref& operator=(const T& x) const
{
*ptr_ = x; // ok, does not alter the state of this instance
return *this;
}
};
这也说明了 const 对象可能是可赋值的, 19 但并不意味着引用是 const,只是说可以用 const 对象模拟。
所以标准的方法是提供原子操作的类型特征,用最少的逻辑和一个匹配。is_const
然而,类型特征也很容易在用户代码中扩展。如果应用程序需要的话,您可以引入更多的概念,比如“侵入性常数”
template <typename T>
struct is_const_intrusive : selector<false>
{
};
template <typename T>
struct is_const_intrusive<const T> : selector<true>
{
};
template <typename T>
struct is_const_intrusive<const volatile T> : selector<true>
{
};
template <typename T>
struct is_const_intrusive<T&> : is_const_intrusive<T>
{
};
类型性状有无限的应用;这个例子使用了最简单的。假设 C
template <typename T>
class C
{
T member_;
public:
explicit C(argument_type x)
: member_(x)
{
}
};
你需要定义 argument_type 。如果 T 是一个值类型,最好通过对 const 的引用来传递它。但是如果 T 是引用,那么写 const T &就是非法的。所以你会写:
typedef typename add_reference<const T>::type argument_type;
这里,add_reference
如果 T 是对 const 的引用或引用,则 const T 是 T,add_reference 返回 T,这意味着参数类型也是 T。
3.5.2.拆除类型
通过添加限定符、考虑引用、指针和数组等等,C++ 中的一个类型可以生成无限多的“变体”。但是可能发生的情况是,您必须递归地删除所有附加属性,一次删除一个。这个递归过程通常被称为拆解。 20
本节展示了一个名为 copy_q 的元函数,它将所有“限定符”从类型 T1 转移到类型 T2,因此 copy _ q
类型演绎是完全递归的。一次分解一个属性,并将同一个属性移动到结果中。继续前面的例子,const double&匹配 T&其中 T 是 const double,所以结果是“对 copy_q
template <typename T1, typename T2>
struct copy_q
{
typedef T2 type;
};
template <typename T1, typename T2>
struct copy_q<T1&, T2>
{
typedef typename copy_q<T1, T2>::type& type;
};
template <typename T1, typename T2>
struct copy_q<const T1, T2>
{
typedef const typename copy_q<T1, T2>::type type;
};
template <typename T1, typename T2>
struct copy_q<volatile T1, T2>
{
typedef volatile typename copy_q<T1, T2>::type type;
};
template <typename T1, typename T2>
struct copy_q<T1*, T2>
{
typedef typename copy_q<T1, T2>::type* type;
};
template <typename T1, typename T2, int N>
struct copy_q<T1 [N], T2>
{
typedef typename copy_q<T1, T2>::type type[N];
};
更完整的实现可以解决由于 T2 作为参考而引起的问题:
copy_q<double&, int&>::type err1; // error: reference to reference
copy_q<double [3], int&>::type err2; // error: array of 'int&'
然而,这样的类是否应该安静地解决错误或停止编译是值得怀疑的。我们只需注意声明一个 std::vector
/usr/include/gcc/darwin/4.0/c++/ext/new_allocator.h: In instantiation of '__gnu_cxx::new_allocator<int&>':
/usr/include/gcc/darwin/4.0/c++/bits/allocator.h:83: instantiated from 'std::allocator<int&>'
/usr/include/gcc/darwin/4.0/c++/bits/stl_vector.h:80: instantiated from 'std::_Vector_base<int&, std::allocator<int&> >::_Vector_impl'
/usr/include/gcc/darwin/4.0/c++/bits/stl_vector.h:113: instantiated from 'std::_Vector_base<int&, std::allocator<int&> >'
/usr/include/gcc/darwin/4.0/c++/bits/stl_vector.h:149: instantiated from 'std::vector<int&, std::allocator<int&> >'
main.cpp:94: instantiated from here
/usr/include/gcc/darwin/4.0/c++/ext/new_allocator.h:55: error: forming pointer to reference type 'int&'
3.6.容器类型
那么什么是类型列表呢?肯定是那种奇怪的模板兽,对吧?
——安德烈·亚历山德雷斯库
模板参数的最大数量是由实现定义的,但是它通常大到足以使用一个类模板作为类型的容器。 21
这一节展示了一些基本的静态算法如何工作,因为将来你会多次重用相同的技术。实际上,在 TMP 中实现大多数 STL 概念是可能的,包括容器、算法、迭代器和函子,其中复杂性需求在编译时被翻译。 22
这一部分展示了基本技术的概念;稍后您将看到一些应用程序。
最简单的类型容器是对(链表的静态等价物)和数组 (类似于固定长度的 C 风格数组)。
template <typename T1, typename T2>
struct typepair
{
typedef T1 head_t;
typedef T2 tail_t;
};
struct empty
{
};
事实上,您可以使用 pairs 对轻松地存储任意(受合理限制)长度的列表。原则上,你可以形成一个完整的二叉树,但是为了简单起见,一个类型列表(T1,T2...Tn)被表示为类型对<t1 typepair="" ...="">>。换句话说,您将允许第二个组件是一对。实际上,它强制第二个组件成为类型对或空的,这是列表终止符。在伪代码中:
P0 = empty
P1 = typepair<T1, empty >
P2 = typepair<T2, typepair<T1, empty> >
// ...
Pn = typepair<Tn, Pn-1>
这顺便说明了使用类型对序列最简单的操作是 push_front。
按照 Alexandrescu 的符号(见[1]),我称这样的编码为类型列表 。你说第一个可访问的类型 Tn 是列表的头,Pn-1 是尾。
或者,如果您将最大长度固定为一个合理的数字,则可以将所有类型存储在一行中。由于缺省值(可以是空的或 void ),您可以在同一行上声明任意数量的参数:
#define MXT_GENERIC_TL_MAX 32
// the code "publishes" this value for the benefit of clients
template
<
typename T1 = empty,
typename T2 = empty,
// ...
typename T32 = empty
>
struct typearray
{
};
typedef typearray<int, double, std::string> array_1; // 3 items
typedef typearray<int, int, char, array_1> array_2; // 4 items
这些容器的属性是不同的。具有 J 个元素的类型列表要求编译器产生 J 个不同的类型。另一方面,数组是直接访问的,所以为类型数组编写算法需要编写许多(比如 32 个)专门化。类型列表更短,更递归,但是编译起来更费时间。
注在亚伯拉罕[3]所作的理论建立之前,有一些命名上的混乱。类型对的最初想法是由 Alexandrescu(在[1]和随后在 CUJ)充分发展的,他引入了名称 typelist 。
显然,Alexandrescu 也是第一个使用类型数组作为包装器以简单的方式声明长类型列表的人:
template <typename T1, typename T2, ..., typename Tn>
struct cons
{
typedef typepair<T1, typepair<T2, ...> > type;
};
然而,名称 typelist 仍然被广泛用作更通用类型容器的同义词。
3.6.1 .type at
typeat 是一个元函数,从容器中提取第 n 个类型。
struct Error_UNDEFINED_TYPE; // no definition!
template <size_t N, typename CONTAINER, typename ERR = Error_UNDEFINED_TYPE>
struct typeat;
如果第 n 个类型不存在,结果是 ERR。
同一个元函数可以处理类型数组和类型列表。正如预期的那样,数组需要所有可能的专门化。泛型模板只是返回一个错误,然后元函数首先在类型数组上专门化,然后在类型列表上专门化。
template <size_t N, typename CONTAINER, typename ERR = Error_UNDEFINED_TYPE>
struct typeat
{
typedef ERR type;
};
template <typename T1, ... typename T32, typename ERR>
struct typeat<0, typearray<T1, ..., T32>, ERR>
{
typedef T1 type;
};
template <typename T1, ... typename T32, typename ERR>
struct typeat<1, typearray<T1, ..., T32>, ERR>
{
typedef T2 type;
};
// write all 32 specializations
用于类型列表的相同代码更加简洁。列表的第 N 个类型被声明为等于列表尾部的第(N-1)个类型。如果 N 为 0,则结果为头部类型。但是,如果遇到空列表,结果是 ERR。
template <size_t N, typename T1, typename T2, typename ERR>
struct typeat<N, typepair<T1, T2>, ERR>
{
typedef typename typeat<N-1, T2, ERR>::type type;
};
template <typename T1, typename T2, typename ERR>
struct typeat<0, typepair<T1, T2>, ERR>
{
typedef T1 type;
};
template <size_t N, typename ERR>
struct typeat<N, empty, ERR>
{
typedef ERR type;
};
注意,无论使用什么索引,typeat
还要注意较短的实现:
template <size_t N, typename T1, typename T2, typename ERR>
struct typeat<N, typepair<T1, T2>, ERR> : typeat<N-1, T2, ERR>
{
};
3.6.2.返回一个错误
当元函数 F
- 完全移除 F
的主体。 - 给 F
一个空体,没有结果(类型或值)。 - 定义 F
:::type,如果使用的话会导致编译错误(void 或者没有定义的类)。 - 使用用户提供的错误类型定义 F
:::type(如前所示)。
请记住,强制编译器错误是相当激烈的;这类似于抛出异常。很难忽略这一点,但是伪类型更像是返回 false。false 可以很容易地转换为 throw,而伪类型可以转换为编译器错误(静态断言就足够了)。
3.6.3.深度
借助一些简单的宏 23 ,处理类型数组会更容易:
#define MXT_LIST_0(T)
#define MXT_LIST_1(T) T##1
#define MXT_LIST_2(T) MXT_LIST_1(T), T##2
#define MXT_LIST_3(T) MXT_LIST_2(T), T##3
// ...
#define MXT_LIST_32(T) MXT_LIST_31(T), T##32
令人惊讶的是,您可以编写看起来极其简单明了的类声明。下面是一个例子(预处理前后)。
template <MXT_LIST_32(typename T)>
struct depth< typelist<MXT_LIST_32(T)> >
template <typename T1, ... , typename T32>
struct depth< typelist<T1, ... T32> >
名为 depth 的元函数返回类型列表的长度:
template <typename CONTAINER>
struct depth;
template <>
struct depth< empty > : static_value<size_t, 0>
{
};
template <typename T1, typename T2>
struct depth< typepair<T1, T2> > : static_value<size_t, depth<T2>::value+1>
{
};
- 主模板未定义,因此深度
不可用。 - 如果类型列表的深度是 K,编译器必须生成 K 个不同的中间类型(即深度
...深度 其中 Pj 是列表的第 j 个尾部)。
对于类型数组,再次使用宏。typearray <>的深度为 0;typearray
template <MXT_LIST_0(typename T)>
struct depth< typearray<MXT_LIST_0(T)> >
: static_value<size_t, 0> {};
template <MXT_LIST_1(typename T)>
struct depth< typearray<MXT_LIST_1(T)> >
: static_value<size_t, 1> {};
// ...
template <MXT_LIST_32(typename T)>
struct depth< typearray<MXT_LIST_32(T)> >
: static_value<size_t, 32> {};
请注意,即使恶意用户在中间插入一个假的空分隔符,depth 也会返回最后一个非空类型的位置:
typedef typearray<int, double, empty, char> t4;
depth<t4>::value; // returns 4
事实上,这个调用将匹配深度
在任何情况下,empty 都应该被限制在一个不可访问的名称空间中。
3.6.4.前后
本节向您展示了如何从两个类型容器中提取第一个和最后一个类型。
template <typename CONTAINER>
struct front;
template <typename CONTAINER>
struct back;
首先,当容器为空时,您会导致一个错误:
template <>
struct back<empty>;
template <>
struct front<empty>
{
};
虽然 front 很简单,但 back 会遍历整个列表:
template <typename T1, typename T2>
struct front< typepair<T1, T2> >
{
typedef T1 type;
};
template <typename T1>
struct back< typepair<T1, empty> >
{
typedef T1 type;
};
template <typename T1, typename T2>
struct back< typepair<T1, T2> >
{
typedef typename back<T2>::type type;
};
或者简单地说:
template <typename T1, typename T2>
struct back< typepair<T1, T2> > : back<T2>
{
};
对于类型数组,你利用了深度和类型的速度非常快的事实,你简单地做了一些自然的事情,比如说,一个向量。后面的元素是大小为-1 的元素。原则上,这也适用于类型列表,但是它会在整个列表中“迭代”几次(每次“迭代”都会导致一个新类型的实例化)。
template <MXT_LIST_32(typename T)>
struct back< typearray<MXT_LIST_32(T)> >
{
typedef typelist<MXT_LIST_32(T)> aux_t;
typedef typename typeat<depth<aux_t>::value – 1, aux_t>::type type;
};
template <>
struct back< typearray<> >
{
};
template <MXT_LIST_32(typename T)>
struct front< typearray<MXT_LIST_32(T)> >
{
typedef T1 type;
};
template <>
struct front< typearray<> >
{
};
3.6.5.找到
您可以执行顺序搜索,并返回与给定 T 匹配的(第一个)类型的索引。如果 T 没有出现在 CONTAINER 中,您将返回一个常规数字(比如-1),而不是导致编译器错误。
递归版本的代码基本如下:
- 没有任何东西属于空容器。
- 一对元素中的第一个元素的索引为 0。
- 索引是 1 加上尾部 T 的索引,除非后一个索引是未定义的。
template <typename T, typename CONTAINER>
struct typeindex;
template <typename T>
struct typeindex<T, empty>
{
static const int value = (-1);
};
template <typename T1, typename T2>
struct typeindex< T1, typepair<T1, T2> >
{
static const int value = 0;
};
template <typename T, typename T1, typename T2>
struct typeindex< T, typepair<T1, T2> >
{
static const int aux_v = typeindex<T, T2>::value;
static const int value = (aux_v==-1 ? -1 : aux_v+1);
};
类型数组的第一个实现是:
/* tentative version */
template <MXT_LIST_32(typename T)>
struct typeindex< T1, typearray<MXT_LIST_32(T)> >
{
static const int value = 0;
};
template <MXT_LIST_32(typename T)>
struct typeindex< T2, typearray<MXT_LIST_32(T)> >
{
static const int value = 1;
};
// ...
如果要查找的类型与数组中的第一个类型相同,则值为 0;如果它等于数组中的第二个类型,则值为 1,依此类推。不幸的是,下面的是不正确的:
typedef typearray<int, int, double> t3;
int i = typeindex<int, t3>::value;
有不止一个匹配项(即前两个),这会产生一个编译错误。我把这个问题的解决推迟到下一节之后。
3.6.6.推动和弹出
前面已经提到,类型对最简单的操作是 push_front。只需将新的头部类型与旧的容器包装在一起:
template <typename CONTAINER, typename T>
struct push_front;
template <typename T>
struct push_front<empty, T>
{
typedef typepair<T, empty> type;
};
template <typename T1, typename T2, typename T>
struct push_front<typepair<T1, T2>, T>
{
typedef typepair< T, typepair<T1, T2> > type;
};
很自然,pop_front 也很简单:
template <typename CONTAINER>
struct pop_front;
template <>
struct pop_front<empty>;
template <typename T1, typename T2>
struct pop_front< typepair<T1, T2> >
{
typedef T2 type;
};
要对类型数组实现相同的算法,必须采用一个非常重要的技术,名为模板旋转 。 该旋转将所有模板参数向左(或向右)移动一个位置。
template <P1, P2 = some_default, ..., PN = some_default>
struct container
{
typedef container<P2, P3, ..., PN, some_default> tail_t;<sup class="calibre7">24</sup>
};
由 pop_front 产生的类型称为容器的 tail (这就是为什么源代码反复引用 tail_t 的原因)。
参数不必是类型。下面的类计算正整数列表中的最大值。
#define MXT_M_MAX(a,b) ((a)<(b) ? (b) : (a))
template <size_t S1, size_t S2=0, ... , size_t S32=0>
struct typemax : typemax<MXT_M_MAX(S1, S2), S3, ..., S32>
{
};
template <size_t S1>
struct typemax<S1,0,0,...,0> : static_value<size_t, S1>
{
};
顺便提一下,只要可行,加速旋转是很方便的。在前面的示例中,您应该编写
template <size_t S1, size_t S2=0, ... , size_t S32=0>
struct typemax
: typemax<MXT_M_MAX(S1, S2), MXT_M_MAX(S3, S4), ..., MXT_M_MAX(S31, S32)>
{
};
要计算 N 个常数的最大值,只需要 typemax 的 log2(N)个实例,而不是 N 个。
很容易将旋转和宏与优雅结合 25 :
template <typename T0, MXT_LIST_31(typename T)>
struct pop_front< typearray<T0, MXT_LIST_31(T)> >
{
typedef typearray<MXT_LIST_31(T)> type;
};
template <MXT_LIST_32(typename T), typename T>
struct push_front<typearray<MXT_LIST_32(T)>, T>
{
typedef typearray<T, MXT_LIST_31(T)> type;
};
使用 pop_front,您可以实现通用的顺序查找。注意,为了清楚起见,您想要添加一些中间的 typedefs。与元编程一样,类型相当于经典 C++ 中的变量。您可以将 typedefs 视为等同于(命名的)临时变量。此外,私有和公共部分有助于将“临时”变量从结果中分离出来:
您在这里要遵循的程序是:
-
空容器中 T 的索引是-1。
-
数组<t1 ...="">中 T1 的索引为 0(这一点毫无疑问地成立,即使 T1 出现不止一次)。
-
要获得 T 在数组<t1 t2="" t3="" ...="">中的索引,需要计算它在一个旋转数组中的索引,并将结果加 1。
template <typename T> struct typeindex<T, typearray<> > { static const int value = (-1); }; template <MXT_LIST_32(typename T)> struct typeindex< T1, typearray<MXT_LIST_32(T)> > { static const int value = 0; }; template <typename T, MXT_LIST_32(typename T)> struct typeindex< T, typearray<MXT_LIST_32(T)> > { private: typedef typearray<MXT_LIST_32(T)> argument_t; typedef typename pop_front<argument_t>::type tail_t; static const int aux_v = typeindex<T, tail_t>::value; public: static const int value = (aux_v<0) ? aux_v : aux_v+1; }; ```</t1>
3.6.7.关于模板旋转的更多信息
模板参数可以很容易地旋转;然而,从左到右消费通常更简单。假设您想通过输入以 10 为基数的所有数字来合成一个整数。这里有一些伪代码。
template <int D1, int D2 = 0, ... , int DN = 0>
struct join_digits
{
static const int value = join_digits<D2, ..., DN>::value * 10 + D1;
};
template <int D1>
struct join_digits<D1>
{
static const int value = D1;
};
join_digits<3,2,1>::value; // compiles, but yields 123, not 321
相反,观察到在旋转中消耗 DN 并不容易。这将不会编译,因为每当 DN 等于它的缺省值(零)时,值就根据它本身来定义:
template <int D1, int D2 = 0, ..., int DN-1 = 0, int DN = 0>
struct join_digits
{
static const int value = join_digits<D1,D2, ...,D<sub class="calibre19">N-1</sub>>::value * 10 + DN;
};
向右旋转不会产生正确的结果:
template <int D1, int D2 = 0, ..., int DN-1 = 0, int DN = 0>
struct join_digits
{
static const int value = join_digits<0,D1,D2, ...,D<sub class="calibre19">N-1</sub>>::value * 10 + DN;
};
解决方案是简单地存储辅助常数,并从尾部借用它们:
template <int D1 = 0, int D2 = 0, ..., int DN = 0>
struct join_digits
{
typedef join_digits<D2, ..., DN> next_t;
static const int pow10 = 10 * next_t::pow10;
static const int value = next_t::value + D1*pow10;
};
template <int D1>
struct join_digits<D1>
{
static const int value = D1;
static const int pow10 = 1;
};
join_digits<3,2,1>::value; // now really gives 321
模板旋转有两种方式:
-
直接旋转主模板(如上所示):
template <int D1 = 0, int D2 = 0, ..., int DN = 0> struct join_digits { ... }; template <int D1> struct join_digits<D1> { ... };
-
参数的旋转。这增加了额外的“间接性”:
template <int D1 = 0, int D2 = 0, ..., int DN = 0> struct digit_group { // empty }; template <typename T> struct join_digits; // primary template not defined template <int D1, int D2, ..., int DN> struct join_digits< digit_group<D1, ..., DN> > { // as above }; template <> struct join_digits< digit_group<> > { // as above };
第一种解决方案通常更容易编码。然而,第二种有两个重要的优点:
- “携带”模板参数元组的 T 类型可以重用。t 通常是某种类型的容器。
- 暂时假设 join_digits <...>是一个真类(不是元函数),它实际上被实例化了。编写接受任何 join_digits 实例的通用模板将会很容易。他们只需要取 join_digits
。但是,如果 join_digits 有一个很长且不确定数量的参数,客户端将不得不像 X. 26 那样处理它
3.6.8.结块
pop_front 中封装的旋转技术可以用来创建元组作为aggregate 对象。
在合成中,聚集 A 是一个在其模板参数中包含类型容器 C 的类。该类使用 front
template <typename C>
class A : public A<typename pop_front<C>::type>
{
typename front<C>::type member_;
public:
// ...
};
template < >
class A<empty>
{
};
template < >
class A< typearray<> >
{
};
- 继承可以是公开的、私有的,甚至是受保护的。
- 有两种可能的递归停止器:A <empty_typelist>和 A <empty_typearray>。</empty_typearray></empty_typelist>
因此,一个凝聚体是一个对象包,其类型在容器中列出。如果 C 是 typearray
图 3-4 。团聚体 A 的布局
请注意,在所审查的实现中,对象的内存布局相对于类型容器是相反的。
要访问包的元素,您需要再次使用 rotation。假设所有成员都是公共的。通过一个全局函数和一个合适的 traits 类的协作,您将获得对第 n 个聚集成员的引用。
有两种同样好的发展策略:侵入性特质和非侵入性特质。
侵入特征要求团块暴露一些辅助信息:
template <typename C>
struct A : public A<typename pop_front<C>::type>
{
typedef typename front<C>::type value_type;
value_type member;
typedef typename pop_front<C>::type tail_t;
};
template <typename agglom_t, size_t N>
struct reference_traits
{
typedef reference_traits<typename agglom_t::tail_t, N-1> next_t;
typedef typename next_t::value_type value_type;
static value_type& ref(agglom_t& a)
{
return next_t::ref(a);
}
};
template <typename agglom_t>
struct reference_traits<agglom_t, 0>
{
typedef typename agglom_t::value_type value_type;
static value_type& ref(agglom_t& a)
{
return a.member;
}
};
template <size_t N, typename agglom_t>
inline typename reference_traits<agglom_t,N>::value_type& ref(agglom_t& a)
{
return reference_traits<agglom_t, N>::ref(a);
}
一个简单的例子:
typedef typearray<int, double, std::string> C;
A<C> a;
ref<0>(a) = 3;
ref<1>(a) = 3.14;
ref<2>(a) = "3.14";
非侵入性特征相反,用部分专门化来决定信息:
template <typename agglom_t, size_t N>
struct reference_traits;
template <typename C, size_t N>
struct reference_traits< A<C>, N >
{
typedef reference_traits<typename pop_front<C>::type, N-1> next_t;
typedef typename front<C>::type value_type;
};
在可行的情况下,非侵入性特征是首选。reference_traits 的作者能否修改 a 的定义并不明显。然而,traits 通常需要对象的合理“合作”。此外,自动推导代码是 A 类内部代码的复制,自动推导出的值往往是“刚性的”,因此侵扰性不是一个明显的输家。
一种特殊情况是根据类型列表建模的不含重复的团聚体。实现要简单得多,因为用伪造型代替旋转就足够了:
template <typename T, typename tail_t> // cast-like syntax
T& ref(A< typepair<T, tail_t> >& a) // T is non-deduced
{
return a.member;
}
typedef typepair<int, typepair<double, typepair<std::string, empty> > > C;
A<C> a;
ref<double>(a) = 3.14;
ref<std::string>(a) = "greek pi";
ref<int>(a) = 3;
这种转换之所以有效,是因为语法 ref
事实上,C++ 标准允许在参数推导之前进行一次从派生到基的转换,如果这是精确匹配的充分必要条件的话。
这里,将类型 A
为了存储从一个聚集中提取的值,声明一个 reference _ traits<agglom_t>:::value _ type 类型的对象。</agglom_t>
最后,稍微多一点侵入性,您只需将一个成员函数添加到:
template <typename C>
struct A : public A< typename pop_front<C>::type >
{
typedef typename front<C>::type value_type;
value_type member;
typedef typename pop_front<C>::type tail_t;
tail_t& tail() { return *this; }
};
template <typename agglom_t, size_t N>
struct reference_traits
{
// ...
static value_type& get_ref(agglom_t& a)
{
return next_t::get_ref(a.tail());
}
};
调用成员函数而不是隐式转换允许您切换到私有继承,甚至切换到 has-a 关系:
template <typename C>
class A
{
public:
typedef typename pop_front<C>::type tail_t;
typedef typename front<C>::type value_type;
private:
A<tail_t> tail_;
value_type member;
public:
tail_t& tail() { return tail_; }
// ...
};
对象的内存布局现在与类型容器的顺序相同。
3.6.9.转换
许多算法实际上需要线性数量的递归步骤,对于类型列表和类型数组都是如此。实际上,类型对表示满足了大多数实际目的,除了一个:类型列表的声明确实是不可行的。
正如预期的那样,从类型数组转换到类型列表非常容易,反之亦然。
提供一个统一的实现 27 是一个有趣的练习:
template <typename T>
struct convert
{
typedef typename pop_front<T>::type tail_t;
typedef typename front<T>::type head_t;
typedef
typename push_front<typename convert<tail_t>::type, head_t>::type
type;
};
template <>
struct convert< typearray<> >
{
typedef empty type;
};
template <>
struct convert< empty >
{
typedef typearray<> type;
};
请注意,此代码中的 T 是泛型类型容器,而不是泛型类型。
之前,您使用部分模板专门化来防止错误的静态参数类型。例如,如果你尝试 front
然而,这种保护在这里是不必要的。convert 构建在 front 和 pop_front 之上,它们将执行所需的参数验证。在这种情况下,编译器将诊断出在 convert
问题只是一个不太清楚的调试信息。在您必须纠正问题的选项中,您可以编写类型特征来标识类型容器,然后放置断言:
template <typename T>
struct type_container
{
static const bool value = false;
};
template <typename T1, typename T2>
struct type_container< typepair<T1, T2> >
{
static const bool value = true;
};
template <>
struct type_container<empty>
{
static const bool value = true;
};
template <MXT_LIST_32(typename T)>
struct type_container< typearray<MXT_LIST_32(T)> >
{
static const bool value = true;
};
template <typename T>
struct convert
: static_assert< type_container<T>::value >
{
//...
很有可能,编译器会发出指向断言行的第一个错误。
注5.2 节完全致力于坏的静态参数类型。您将会遇到这样的函数模板,它们静态地将其模板参数限制为那些具有特定接口的模板参数。
通过插入表示该类型的空容器的类型来扩展类型容器特征是有用的(主模板不变)。
template <typename T1, typename T2>
struct type_container< typepair<T1, T2> >
{
static const bool value = true;
typedef empty type;
};
template <>
struct type_container<empty>
{
static const bool value = true;
typedef empty type;
};
template <MXT_LIST_32(typename T)>
struct type_container< typearray<MXT_LIST_32(T)> >
{
static const bool value = true;
typedef typearray<> type;
};
当足够多的“低级”元函数——如 front、back、push_front 等——可用时,大多数元算法将在数组和列表上工作。您只需要两个不同的递归终止,以及一个对 typearray <>的专门化和一个对 empty 的专门化。
另一个选择是空-空成语:让一个 helper 类把原始类型容器作为 T 和第二个类型,第二个类型是同类的空容器(从 traits 获得)。当这些相等时,你停下来。
template <typename T>
struct some_metafunction
: static_assert<type_container<T>::value>
, helper<T, typename type_container<T>::type>
{
};
template <typename T, typename E>
struct helper
{
// general case:
// T is a non-empty type container of any kind
// E is the empty container of the same kind
};
template <typename E>
struct helper<E, E>
{
// recursion terminator
};
3.6.10.元函数
用户函子、谓词和二元运算可以用模板-模板参数替换。下面是一个简单的元函数:
template <typename T>
struct size_of
{
static const size_t value = CHAR_BIT*sizeof(T);
};
template <>
struct size_of<void>
{
static const size_t value = 0;
};
下面是一个简单的二元元关系:
template <typename X1, typename X2>
struct less_by_size : selector<(sizeof(X1) < sizeof(X2))>
{
};
template <typename X>
struct less_by_size<void, X> : selector<true>
{
};
template <typename X>
struct less_by_size<X, void> : selector<false>
{
};
template <>
struct less_by_size<void, void> : selector<false>
{
};
这是一个可能用到它的元函数的框架:
template <typename T, template <typename X1, typename X2> class LESS>
struct static_stable_sort
: static_assert< type_container<T>::value >
{
// write LESS<T1, T2>::value instead of "T1<T2"
typedef [[RESULT]] type;
};
本节不是描述一个实现,而是描述 static_stable 排序的一个可能的应用。假设我们的源代码包含一组返回无符号整数的随机生成器:
class linear_generator
{
typedef unsigned short random_type;
...
};
class mersenne_twister
{
typedef unsigned int random_type;
...
};
class mersenne_twister_64bit
{
typedef /* ... */ random_type;
...
};
用户将在一个类型容器中列出所有的生成器,按照从最好(首选算法)到最差的顺序。这个容器可以按 sizeof(typename T::random_type)排序。最后,当用户要求 X 类型的随机数时,您扫描排序后的容器并停留在 random_type 至少与 X 大小相同的第一个元素上,然后使用生成器返回值。由于排序是稳定的,第一个合适的类型在用户偏好中也是最好的。
如前所述,我现在转向根据大小(以位为单位)选择无符号整数的问题。
首先,将所有候选对象放入一个类型容器中:
typedef typearray<unsigned char, unsigned short, unsigned int,
unsigned long, unsigned long long> all_unsigned;
您必须从左到右扫描列表,并使用具有指定大小的第一个类型(也可以将特定于编译器的类型追加到列表中)。
注意这里需要一点代数。根据符号函数的定义,对于任何整数,都有等式δ sign(δ)=|δ|。另一方面,如果 S 是{-1,0,1}中的规定常数,等式δ S=|δ|分别意味着δ≤0,δ=0,δ≥0。这个基本关系允许您用一个整数参数表示三个谓词(小于或等于零、等于零和大于或等于零)。
在下面的代码中,T 是任意类型的容器:
#define MXT_M_ABS(a) ((a)<0 ? –(a) : (a))
enum
{
LESS_OR_EQUAL = -1,
EQUAL = 0,
GREATER_OR_EQUAL = +1
};
template
<
typename T,
template <typename X> class SIZE_OF,
int SIGN,
size_t SIZE_BIT_N
>
struct static_find_if
: static_assertion< type_container<T>::value >
{
typedef typename front<T>::type head_t;
static const int delta = (int)SIZE_OF<head_t>::value – (int)SIZE_BIT_N;
typedef typename typeif
<
SIGN*delta == MXT_M_ABS(delta),
front<T>,
static_find_if<typename pop_front<T>::type,
SIZE_OF, SIGN, SIZE_BIT_N>
>::type aux_t;
typedef typename aux_t::type type;
};
// define an unsigned integer type which has exactly 'size' bits
template <size_t N>
struct uint_n
: static_find_if<all_unsigned, size_of, EQUAL, N>
{
};
// defines an unsigned integer type which has at least 'size' bits
template <size_t N>
struct uint_nx
: static_find_if<all_unsigned, size_of, GREATER_OR_EQUAL, N>
{
};
typedef uint_n<8>::type uint8;
typedef uint_n<16>::type uint16;
typedef uint_n<32>::type uint32;
typedef uint_n<64>::type uint64;
typedef uint_nx<32>::type uint32x;
注意,选择模板参数的顺序是为了清楚地说明使用的是 static_find_if,而不是 static_find_if 本身。 28
如果找不到合适的类型会怎么样?任何无效的使用都将展开长时间的错误级联(代码已被编辑以抑制大部分干扰):
uint_n<25>::type i0 = 8;
uint_nx<128>::type i1 = 8;
error C2039: 'type' : is not a member of 'front<typearray<>>'
: see declaration of 'front<typearray<>>'
: see reference to class template instantiation
'static_find_if<T,SIZE_OF,SIZE_BIT_N,SIGN>' being compiled
with
[
T=pop_front<pop_front<pop_front<pop_front<pop_front<all_unsigned>::type>::type>::type>::type>::type,
]
: see reference to class template instantiation 'static_find_if<T,SIZE_OF,SIZE_BIT_N,SIGN>' being compiled
with
[
T=pop_front<pop_front<pop_front<pop_front<all_unsigned>::type>::type>::type>::type,
]
: see reference to class template instantiation
'static_find_if<T,SIZE_OF,SIZE_BIT_N,SIGN>' being compiled
with
[
T=pop_front<pop_front<pop_front<all_unsigned>::type>::type>::type,
]
[...]
: see reference to class template instantiation
'static_find_if<T,SIZE_OF,SIZE_BIT_N,SIGN>' being compiled
with
[
T=all_unsigned,
]
: see reference to class template instantiation
'uint_n<SIZE_BIT_N>' being compiled
with
[
SIZE_BIT_N=25
]
基本上,编译器是在说,在 uint _ n<25>:::type 的演绎过程中,在将 pop_front 应用于类型数组五次之后,它以一个空容器结束,该容器没有 front 类型。
然而,很容易得到一个更易于管理的报告。作为递归终止符的结果,您只是添加了一个未定义的类型:
template
<
template <typename X> class SIZE_OF,
int SIGN,
size_t SIZE_BIT_N
>
struct static_find_if<typearray<>, SIZE_OF, SIGN, SIZE_BIT_N>
{
typedef error_UNDEFINED_TYPE type;
};
现在,错误消息更加简洁:
error C2079: 'i0' uses undefined class 'error_UNDEFINED_TYPE'
error C2079: 'i1' uses undefined class 'error_UNDEFINED_TYPE'
3.7.风格概述
编程元函数时,识别:
- 暗示性的名称和语法。
- 表达概念需要哪些模板参数。
- 该算法依赖于哪些原子动作。
- 递归的高效实现。
- 必须隔离的特殊情况。
如果元函数的名称类似于一个经典算法(比如 find_if),那么您可以采用一个类似的名称(static_find_if),或者甚至是一个相同的名称(比如 typelist::find_if)。
一些作者在纯静态算法后面添加了下划线,因为这允许模仿真实的关键字(typeif 将被称为 if_)。
如果需要几个模板参数,编写代码以便用户能够记住它们的含义和顺序。通过名称给出语法提示是个好主意:
: static_find_if<all_unsigned, size_of, GREATER_OR_EQUAL, N>
许多不相关的参数应该被分组到一个 traits 类中,该类应该有一个易于复制的默认实现。
最后,下表可以帮助您将经典算法转换为静态算法。
| |
经典 C++ 函数
|
静态元编程
|
| --- | --- | --- |
| 他们所操纵的 | 对象的实例 | 类型 |
| 参数处理 | 通过参数公共接口 | 通过元功能 |
| 处理不同的争论 | 功能霸主 | 部分模板专门化 |
| 回送结果 | 零个或一个返回语句 | 零个或多个静态数据(类型或常量),通常是继承的 |
| 错误捕捉 | 尝试...捕捉块 | 额外模板参数错误 |
| 用户提供的回调 | 函子 | 模板-模板参数 |
| 临时对象 | 局部变量 | 私有 typedef/静态常量 |
| 函数调用 | 是的,作为子程序 | 是的,也是通过推导 |
| 算法结构 | 迭代或递归 | 静态递归,用合适的完整/部分模板专门化停止 |
| 有条件的决定 | 语言结构(if,switch) | 部分专业化 |
| 错误处理 | 抛出异常返回 false | 中止编译不返回结果将结果设置为不完整类型 |
1 记住预处理器在编译器之前运行,所以它不能依赖 sizeof。
2 再读一遍前面的笔记。
3 另请参见www . boost . org/doc/libs/1 _ 46 _ 0/libs/wave/doc/序言. html
。
4 这个例子摘自与马可·马塞罗的一次私人谈话。
5 我在书中并不总是使用推导记号,主要是为了清晰。然而,我强烈鼓励在产品代码中采用它,因为它提高了代码重用。
6 除了强制转换为非整数类型。比如 N*1.2 是非法的,但是 N+N/5 是可以的。
7 实际中,N 总是偶数,所以 N-N/2 == N/2。
8 参见第 7.2 节中的双重检查停止。
9 我坚持认为问题是可解的,因为 is_integer < long >和 is_integer < ptrdiff_t >的实现是完全相同的;否则,它就是病态的。举个反例,考虑一个 time_t 和 long 转换成字符串的问题;即使 time_t 很长,字符串也需要不同。因此,这个问题不能通过 TMP 技术来解决。
10 这是好事,因为一个构建良好的模板类不该需要它。
11 在这里,假和假+1 都起作用。
12 相同并不意味着所有功能必须完全相同,因为一些差异可能对“统一使用”产生有限的影响。举个简单的例子,参数可以通过值或常量引用传递。
13 可得:cantrip.org/trails.html.文章引用的参考文献有[10]、[11]和[12]。
14 句子略有重排。
15 由 John Maddock 和 Steve Cleary 引入的术语类型特征在这里作为一个普通名称使用,但它也作为一个专有名称而流行,表示一个特定的库实现。参见cppreference.com/header/type_traits
或www . boost . org/doc/libs/1 _ 57 _ 0/libs/type _ traits/doc/html/index . html
。
16 在现代 C++ 中,有一个专用的< type_traits >头,它包含了这里描述的大部分元函数,以及许多经典 C++ 中无法复制的功能。比如 has_trivial_destructor < T >没有编译器的配合是无法还原的,当前的实现总是返回 false,除了内置类型。
17 可以定义 add _ reference
18 这是实际使用的。一些智能指针,包括 std::unique_ptr,在类型匹配 T[]时使用操作符 delete [],在任何其他情况下使用单次删除。
19 或者,std::pair < const int,double >既不是 const 也不能赋值。
20 “拆型”这个说法是斯蒂芬·c·杜赫斯特提出来的。
21c++ 标准包含一个信息部分,称为“实现数量”,其中建议了模板参数(1024)和嵌套模板实例化(1024)的最小数量,但编译器不需要考虑这些数量。
22 论据上的引用是【3】。
无论如何,boost 预处理器库会更合适,但是它的描述将需要另一个章节。这里,重点是简单的:一个战略性的手写宏可以显著提高代码的美观性。
*24 原则上,有些 _default 不应该被明确指定。所有形式的代码重复都会导致维护错误。在这里,我展示它是为了强调旋转。
25 参见 3.6.3 节。
这不是问题,如果 join_digits 是一个函子,客户很可能会把它当作 X。
27 又是一次拆式练习;还要注意,使用 push_back 而不是 push_front 会反转容器。
28 我采用了 find_if 这个名字,带有一些滥用的符号;一个真正的 static_find_if 应该是 static_find_if < typename T,template
四、重载决议
本章介绍基于过载解决的 TMP 技术。
常见的基础架构如下:
- 你想测试 T 型是否满足一个条件。
- 您编写了几个同名的静态函数,比如 test,并向它们传递一个“携带”T 类型的伪参数(换句话说,一个允许推导 T 的参数,比如 T*)。
- 编译器根据 C++ 语言规则选择最佳候选项。
- 您可以使用返回类型或间接从该类型的属性推断出使用了哪个函数,并最终做出决定。
第一部分介绍了一些定义。
4.1.组
一个组 是一个提供单个例程的优化变量的类。从外部来看,一个组就像一个整体功能,可以自动为每个调用选择最佳实现。
一个组由两个实体组成:
- 包含(单个)静态成员函数变量的模板结构。
- 一个配套的全局函数模板,它只是将执行转发给组中正确的成员,根据自动推导的模板参数和一些框架提供的信息执行静态决策。
组本身通常是一个模板,即使形式上没有必要(可以用模板成员函数将组写成普通的类)。
最后,观察群体和特性在某种程度上是正交的。特征包含特定类型的所有动作,而组包含许多类型的单个动作。
4.1.1.从超载到群体
组是一组重载函数的演化。
步骤 1:您意识到默认的模板实现可以处理大多数情况,所以您只需添加重载的变体:
template <typename T>
bool is_product_negative(T x, T y)
{
return x<0 ^ y<0;
}
bool is_product_negative(short x, short y)
{
return int(x)*int(y) < 0;
}
bool is_product_negative(unsigned int x, unsigned int y)
{
return false;
}
bool is_product_negative(unsigned long x, unsigned long y)
{
return false;
}
步骤 2:实现被聚集在几个使用标签挑选的模板中。
template <typename T>
bool is_product_negative(T x, T y, selector<false>)
{
return x<0 ^ y<0;
}
template <typename T>
bool is_product_negative(T x, T y, selector<true>)
{
return int(x)*int(y) < 0;
}
template <typename T>
bool is_product_negative(T x, T y)
{
typedef selector<(sizeof(T)<sizeof(int))> small_int_t;
return is_product_negative(x, y, small_int_t());
}
步骤 3:将所有的辅助函数归入一个类中,并在外面留下一个单独的函数来分派工作:
// companion function
template <typename T>
bool is_product_negative(T x, T y)
{
return is_product_negative_t<T>::doIt(x, y);
}
template <typename T>
struct is_product_negative_t
{
static bool doIt(T x, T y)
{ ... }
static bool doIt(unsigned, unsigned)
{ return false; }
};
这是另一个非常简单的组:
struct maths
{
template <typename T>
inline static T abs(const T x)
{
return x<0 ? -x : x;
}
inline static unsigned int abs(unsigned int x)
{
return x;
}
};
template <typename T>
inline T absolute_value(const T x)
{
return maths::abs(x);
}
注意记住,组类,作为一个非模板,总是完全实例化的。此外,头文件中的非模板函数必须内联声明。
进一步假设你有一个名为 has_abs_method 的元函数,这样 has_abs_method < T >:::如果 T 类型的对象 x 的绝对值由 x.abs()给定,则值为真。 1
这允许你的团队变得更复杂。在下一个例子中,您将为 double 专门化整个组,专门化将忽略 has_abs_method
template <typename scalar_t>
struct maths
{
static scalar_t abs(const scalar_t& x, selector<false>)
{
return x<0 ? –x : x;
}
static scalar_t abs(const scalar_t& x, selector<true>)
{
return x.abs();
}
};
template <>
struct maths<double>
{
template <bool UNUSED>
static double abs(const double x, selector< UNUSED >)
{
return std::fabs(x);
}
};
template <typename scalar_t>
inline scalar_t absolute_value(const scalar_t& x)
{
typedef selector< has_abs_method<scalar_t>::value > select_t;
return maths<scalar_t>::abs(x, select_t());
}
过多的重载可能会发生冲突。记住,非模板函数比匹配的模板更好,但这不适用于使用类的模板参数的成员函数:
template <typename scalar_t>
struct maths
{
static scalar_t abs(const scalar_t& x, selector<false>)
{
return x<0 ? –x : x;
}
static int abs(const int x, selector<false>)
{
return std::abs(x);
}
}
error: ambiguous call to overloaded function, during instantiation of absolute_value<int>
这正是“双层”模板选择的优势。“第一层”是伴随函数中 scalar_t 的自动推导,“第二层”是重载选择,在参数已经固定的类模板(组)内执行:
template <typename scalar_t>
inline scalar_t absolute_value(const scalar_t& x)
{
// collect auxiliary information, if needed
return math<scalar_t>::abs(x, ...);
}
将它们结合起来,你就拥有了更少的全局函数模板(过多的重载很可能导致“模棱两可的调用”)。此外,该组可以有子例程(私有静态成员函数)。
用户有几种扩展选择:
- 专门化整个群体(如果是模板的话)
- 专门化全局伴随函数
- 利用现有框架的模型类型(例如,specialize has_abs_method)
选择部分可以更微妙,在中间有额外的层。如下例所示,通过隐式参数提升选择组的正确成员:
#include <cmath>
struct tag_floating
{
tag_floating() {}
tag_floating(instance_of<float>) {}
tag_floating(instance_of<double>) {}
tag_floating(instance_of<long double>) {}
};
struct tag_signed_int
{
tag_signed_int() {}
tag_signed_int(instance_of<short>) {}
tag_signed_int(instance_of<int>) {}
tag_signed_int(instance_of<long>) {}
};
struct tag_unsigned_int
{
tag_unsigned_int() {}
tag_unsigned_int(instance_of<unsigned short>) {}
tag_unsigned_int(instance_of<unsigned int>) {}
tag_unsigned_int(instance_of<unsigned long>) {}
};
template <typename scalar_t>
struct maths
{
inline static scalar_t abs(const scalar_t x, tag_signed_int)
{
return x<0 ? -x : x;
}
inline static scalar_t abs(const scalar_t x, tag_unsigned_int)
{
return x;
}
inline static scalar_t abs(const scalar_t x, tag_floating)
{
return fabs(x);
}
};
template <typename scalar_t>
inline scalar_t absv(const scalar_t& x)
{
return maths<scalar_t>::abs(x, instance_of<scalar_t>());
}
使用反转的选择器层次结构可以获得相同的效果(例如,让 instance_of
您还可以引入中间选择器(不幸的是,您必须手工编写构造函数):
struct tag_int
{
tag_int() {}
tag_int(instance_of<short>) {}
tag_int(instance_of<int>) {}
tag_int(instance_of<long>) {}
tag_int(instance_of<unsigned short>) {}
tag_int(instance_of<unsigned int>) {}
tag_int(instance_of<unsigned long>) {}
};
template <typename scalar_t>
struct maths
{
static scalar_t mod(const scalar_t x, const scalar_t y, tag_int)
{
return x % y;
}
static scalar_t mod(const scalar_t& x, const scalar_t& y, tag_floating)
{
return fmod(x, y);
}
};
template <typename scalar_t>
inline scalar_t mod(const scalar_t& x, const scalar_t& y)
{
return maths<scalar_t>::mod(x, y, instance_of<scalar_t>());
}
注意,在这段代码中,maths
然而,当解析依赖于模板参数的表达式时,由于不知道实际涉及的类型,编译器将接受任何形式上合法的 C++ 语句。 3 因此,如果两个参数 x 和 y 中至少有一个具有泛型类型 T,则 x % y 在实例化之前都被认为是有效的。
前一个例子可以明确地工作,因为伴随函数限制了对名为 mod 的 maths
有时,组与一个特殊的头文件相关联,该头文件使用宏块检测平台信息,并使用 typedefs 在 C++ 中翻译它:
// file "root.hpp"
// note: this code is fictitious
struct msvc {};
struct gcc {};
#if defined(__MSVC) // preprocessor compiler detection...
typedef msvc compiler_type; // ...translated in c++
#elif defined(__GCC__)
typedef gcc compiler_type;
#endif
// from here on, there's a global type tag named "compiler_type"
在不同的平台上,同一个函数可能有不同的“最佳”实现,因此您可以使用 compiler_type 作为标签来选择最合适的一个(但是所有的函数必须是合法的 C++ 代码):
template <typename scalar_t, typename compiler_t>
struct maths
{
static scalar_t multiply_by_two(const scalar_t x)
{ return 2*x; }
};
template < >
struct maths<unsigned int, msvc>
{
static unsigned int multiply_by_two(const unsigned int x)
{ return x << 1; }
};
template <typename scalar_t>
inline scalar_t multiply_by_two(const scalar_t& x)
{
return maths<scalar_t, compiler_type>::multiply_by_two(x);
}
请注意,您可以根据需要对成员函数进行分支选择——可以同时在多个标记上进行,也可以分层进行。
作为一个规则,每当您需要操作一个标准函数的结果时,您可能想要使用“编译器标签”,该标准函数在某种程度上被定义为特定于编译器的,例如,美化由 typeid(...).名称()。
考虑一个真实世界的例子。根据标准,如果 A 和 B 都是有符号整数,不都是正的,A % B 的符号是未定义的(如果改为 A > 0 和 B > 0,标准保证 A % B > 0)。
例如,-10 % 3 可以产生-1 或+2,因为-10 可以写成 3(-3)+(-1)或 3(-4)+(+2),并且|-1|<3 和|2|<3。在任何情况下,这两种解决方案将相差 3。
然而,运算符% 通常被实现为 A 和(A % B)具有相同的符号(实际上,这与用于 fmod 的规则相同)。因此,编写一个允许这个条件的提醒函数是有意义的。
由于(-A) % B == -(A % B)和 A % (-B) == A % B,您可以推断出,当 A % B 的本机实现产生不同的结果时,您可以返回 sign(A)*(|A| % |B|)。
一个简单的实现可以依赖于(-3) % 2 等于+1 或-1。(注意,下面的代码不是 100%防弹的,但这是一个很好的妥协。)
template <typename T, int X = (-3)%2, int Y = (-3)%(-2), int Z = 3%(-2)>
struct modgroup;
// if X=+1, Y=-1, Z=+1 then operator% already does what we want
// (strictly speaking, we tested only int)
template <typename T>
struct modgroup<T, 1, -1, 1>
{
static scalar_t mod(const T x, const T y)
{
return x % y;
}
};
// in any other case, fall back to the safe formula
template <typename T, int X, int Y, int Z>
struct modgroup
{
static scalar_t mod(const T x, const T y)
{
const T result = abs(x) % abs(y);
return x<0 ? –result : result;
}
};
template <typename scalar_t>
struct maths
{
static scalar_t mod(const scalar_t x, const scalar_t y,
tag_int)
{
return modgroup<scalar_t>::mod(x, y);
}
static scalar_t mod(const scalar_t& x, const scalar_t& y,
tag_floating)
{
return fmod(x, y);
}
};
template <typename scalar_t>
inline scalar_t mod(const scalar_t& x, const scalar_t& y)
{
return maths<scalar_t>::mod(x, y, instance_of<scalar_t>());
}
4.1.2.运行时间衰减
类型标记可以实现一个特殊的强制转换运算符,这样,如果组中没有重载与标记完全匹配,执行将在默认函数中继续,该函数通常在运行时执行一些工作。原型是一个静态整数,如果没有更好的匹配,它会衰减为正常整数。
假设您想用零填充 C 数组:
template <typename T, T VALUE>
struct static_value
{
// ...
operator T() const
{
return VALUE;
}
};
template <typename T>
struct zeroize_helper
{
static void apply(T* const data, static_value<int, 1>)
{
*data = T();
}
static void apply(T (&data)[2], static_value<int, 2>)
{
data[0] = data[1] = T();
}
static void apply(T* const data, const int N)
{
std::fill_n(data, N, T());
}
};
template <typename T, int N>
void zeroize(T (&data)[N])
{
zeroize_helper<T>::apply(data, static_value<int, N>());
}
- 你不用 0,而是写 T(),它适用于更广泛的类型。
- 如果 N 大于 2,则最佳匹配是第三个成员。
- 组中的每个函数都可以自由决定转换,甚至忽略 static_value。
- 默认情况下,可以接受每个 static_value,但不一定在运行时执行所有工作,而是使用另一个模板函数:
template <>
struct zeroize_helper<char>
{
template <int N>
struct chunk
{
char data[N];
};
template <int N>
static void apply(char* const data, static_value<int, N>,
selector<true>)
{
*reinterpret_cast<chunk<N>*>(data) = chunk<N>();
}
template <int N>
static void apply(char* const data, static_value<int, N>,
selector<false>)
{
memset(data, N, 0);
}
template <int N>
static void apply(char* const data, static_value<int, N> S)
{
apply(data, S, selector<sizeof(chunk<N>) == N>());
}
};
4.2.更多特征
这部分完成了对特征的回顾。
这一次,您将使用静态编程限制的特征,但也作为函数组。先说一个具体的案例。
4.2.1.字符串的函数集
假设你要为字符串写一些通用算法。当然你可以使用迭代器,特别是随机访问迭代器,对吗?大多数 STL 实现都有 char 优化的算法,比如 std::find、std::copy 等等。
用户的唯一负担是大量调用 strlen 来查找范围的结尾。strlen
const char* c_string = "this is an example";
// can we avoid this?
std::copy(c_string, c_string+strlen(c_string), destination);
您可以使用 traits 进行更多的优化:
template <typename string_t>
struct string_traits
{
typedef /* dependent on string_t */ const_iterator;
typedef const string_t& argument_type;
const_iterator begin(argument_type s);
const_iterator end (argument_type s);
static bool is_end_of_string(const_iterator i, argument_type s);
};
假设对于每一个有意义的字符串,string_traits 都有相同的接口,你可以写一个算法如下:
template <typename string_t>
void loop_on_all_chars(const string_t& s)
{
typedef string_traits<string_t> traits_t;
typename traits_t::const_iterator i = traits_t::begin(s);
while (!traits_t::is_end_of_string(i, s))
{
std::cout << *(i++);
}
}
代码冗长但清晰。然而在这一点上,你的成就可能并不明显。string_traits 的半透明接口 在进行比较时提供了更多的自由度:
template <typename char_t>
struct string_traits< std::basic_string<char_t> >
{
typedef char_t char_type;
typedef
typename std::basic_string<char_type>::const_iterator
const_iterator;
typedef const std::basic_string<char_type>& argument_type;
static const_iterator begin(argument_type text)
{
return text.begin();
}
static const_iterator end(argument_type text)
{
return text.end();
}
static bool is_end_of_string(const_iterator i, argument_type s);
{
return i == s.end();
}
};
template <>
struct string_traits<const char*>
{
typedef char char_type;
typedef const char* const_iterator;
typedef const char* argument_type;
static const_iterator begin(argument_type text)
{
return text;
}
static const_iterator end(argument_type text)
{
return 0; // constant-time
}
static bool is_end_of_string(const_iterator i, argument_type s);
{
// a constant-time "C" test for end of string
return (i==0) || (*i==0);
}
};
由于 end 现在是常数时间 ,您保存了一个线性时间通道(您将再次遇到这个完全相同的问题,并在 6.2.2 节中用不同的技术解决它。
您可以很容易地将 string_traits 扩展为一个完整的接口(为了便于阅读,一些单词已被重命名):
template <typename string_t>
struct string_traits
{
typedef /* ... */ char_type;
typedef /* ... */ const_iterator;
typedef /* ... */ argument_type; // either string_t or const string_t&
static size_t npos();
static size_t find1st(arg_t txt, const char_t c, size_t offset=0);
static size_t find1st(arg_t txt, const arg_t s, size_t offset=0);
static size_t findlast(arg_t txt, const char_t s, size_t offset);
static size_t findlast(arg_t txt, const arg_t s, size_t offset);
static size_t find1st_in(arg_t txt, const char_t* charset, size_t offs=0);
static size_t find1st_out(arg_t txt, const char_t* charset, size_t offs=0);
static size_t size(arg_t txt);
static const_iterator begin(arg_t txt);
static const_iterator end(arg_t txt);
static const char_t* c_str(arg_t txt);
static bool empty(const_iterator begin, const_iterator end);
static bool less(const_iterator begin, const_iterator end);
static size_t distance(const_iterator begin, const_iterator end);
};
要利用接口并利用 std::string 成员函数,请考虑以下约定:
- 所有迭代器都是随机访问的。
- find 函数返回字符的索引(在所有类型的字符串中都是可移植的)或 npos(),这意味着“没有找到”。
static size_t find1st(arg_t text, const char_type c, size_t offset=0)
{
const char_t* pos = strchr(text+offset, c);
return pos ? (pos-text) : npos();
}
在 const char* 的特殊化中,你对 end 迭代器进行了二义性处理,它可以是一个空指针,表示“直到找到 char 0”。因此,您可以按如下方式实现距离:
static size_t distance(const_iterator begin, const_iterator end)
{
return end ? end-begin : (begin ? strlen(begin) : 0);
}
最后,您可以通过公共派生来继承函数集,和 traits 一样,因为它们是无状态的(所以可以省略受保护的空析构函数):
template <>
struct string_traits<char*> : string_traits<const char*>
{
};
4.2.2.概念特征
正如您在第一章中反复看到的,traits 类规定了语法,而不是精确的实体。代码可以借用 traits 的方式来实现几种不同的实现。
假设您有某种智能指针类,它的 traits 类也负责释放内存:
template <typename T, typename traits_t = smart_ptr_traits<T> >
class smart_ptr
{
typedef typename traits_t::pointer pointer;
pointer p_;
public:
~smart_ptr()
{
traits_t::release(p_);
}
// ...
};
特质::发布可以是:
- 公共静态函数(或仿函数);相关代码在函数体中。
template <typename T>
struct smart_ptr_traits
{
typedef T* pointer;
static void release(pointer p)
{
delete p;
}
- 触发转换操作符的公共静态函数,该操作符实际上运行代码。
template <typename T>
struct smart_ptr_traits
{
static void release(bool)
{
};
class pointer
{
// ...
public:
operator bool()
{ ... }
};
// ....
使用稍微不同的语法,您可以将其重写如下:
template <typename T, typename traits_t = smart_ptr_traits<T> >
class smart_ptr
{
typedef typename traits_t::pointer pointer;
pointer p_;
static void traits_release(typename traits_t::release)
{
// note: empty body
};
public:
~smart_ptr()
{
traits_release(p_);
}
Release 现在可以是一个类型,相关代码在(非显式)构造函数体中。
template <typename T>
struct smart_ptr_traits
{
typedef T* pointer;
struct release
{
release(pointer p)
{
delete p;
}
};
代码可以再次触发一个转换运算符:
template <typename T>
struct smart_ptr_traits
{
struct release
{
};
class pointer
{
// ...
public:
operator release()
{
delete p_;
return release();
}
};
};
所有这些实现都是有效的,您可以选择实际执行的代码的最佳位置。 4
如果 traits::release 是作为一种类型提供的,那么它可能具有易于与程序的其余部分共享的静态数据(例如,您可以记录所有释放的指针)。
4.2.3.特定平台特征
回想一下,traits 类可以是“全局的”或“局部的”。全局特征类随处可见,局部特征应该作为参数传递。
全局特征是首选的,以使客户端可以轻松访问某些平台属性:
template <typename char_t>
struct textfile_traits
{
static char_t get_eol() { return '\n'; }
// ...
};
下面的完整示例用一个类模板表示一个 timer 对象,并从一个“timer traits”类借用附加信息:
- 如何获得当前时间(未指定单位)
- 如何将时间转换成秒(使用频率)
template <typename traits_t>
class basic_timer
{
typedef typename traits_t::time_type tm_t;
typedef typename traits_t::difference_type diff_t;
tm_t start_;
tm_t stop_;
inline static tm_t now()
{
return traits_t::get_time();
}
double elapsed(const tm_t end) const
{
static const tm_t frequency = traits_t::get_freq();
return double(diff_t(end-start_))/frequency;
}
public:
typedef tm_t time_type;
typedef diff_t difference_type;
basic_timer()
: start_()
{}
difference_type lap() const
{ return now()-start_; }
time_type start()
{ return start_ = now(); }
difference_type stop()
{ return (stop_ = now())-start_; }
difference_type interval() const
{ return stop_-start_; }
double as_seconds() const
{ return elapsed(stop_); }
double elapsed() const
{ return elapsed(now()); }
};
下面是一个测量时钟时间(以秒为单位)的 traits 类示例:
#include <ctime>
struct clock_time_traits
{
typedef size_t time_type;
typedef ptrdiff_t difference_type;
static time_type get_time()
{
time_t t;
return std::time(&t);
}
static time_type get_freq()
{
return 1;
}
};
这里有一个不同的 traits 类来计算 CPU 时间:
struct cpu_time_traits
{
typedef size_t time_type;
typedef ptrdiff_t difference_type;
static time_type get_time()
{
return std::clock();
}
static time_type get_freq()
{
return CLOCKS_PER_SEC;
}
};
和一个简短的用例:
basic_timer<clock_time_traits> t;
t.start();
// ...
t.stop();
std::cout << "I ran for " << t.as_seconds() << " seconds.";
traits 的基本限制是所有成员函数必须包含有效的 C++ 代码,即使没有使用。不能在其中一个函数中使用编译器特定的代码。
由于不同的操作系统可以为时间测量提供更精确的 API,您可能会想写一些专门的特征:
#include <windows.h>
struct windows_clock_time_traits
{
typedef ULONGLONG time_type;
typedef LONGLONG difference_type;
static time_type get_time()
{
LARGE_INTEGER i;
QueryPerformanceCounter(&i);
return i.QuadPart;
}
static time_type get_freq()
{
LARGE_INTEGER value;
QueryPerformanceFrequency(&value);
return value.QuadPart;
}
};
#include <sys/time.h>
struct macosx_clock_time_traits
{
typedef uint64_t time_type;
typedef int64_t difference_type;
static time_type get_time()
{
timeval now;
gettimeofday(&now, 0);
return time_type(now.tv_sec) * get_freq() + now.tv_usec;
}
static time_type get_freq()
{
return 1000000;
}
};
除了大整数的 typedefs 之外,这个 traits 接口是标准的 C++,所以您可能想把预处理器隔离在“工厂头”中,以后完全依赖模板属性:
// platform_detect.hpp
struct windows {};
struct macosx {};
struct other_os {};
#if defined(WIN32)
typedef windows platform_type;
#elif defined(__APPLE__)
typedef macosx platform_type;
#else
typedef other_os platform_type;
#endif
// timer_traits.hpp
template <typename platform_t>
struct clock_time_traits;
template < >
struct clock_time_traits<windows>
{
// implementation with QPC/QPF
};
template < >
struct clock_time_traits<macosx>
{
// implementation with gettimeofday
};
template < >
struct clock_time_traits<other_os>
{
// implementation with std::time
};
typedef basic_timer< clock_time_traits<platform_type> > native_timer_type;
不幸的是,代码是不可移植的(然而,如果它能编译,它就能正确运行)。
根据该标准,编译器不需要诊断未使用的模板成员函数中的错误,但如果需要,它要求所有提到的实体都是定义良好的。特别是,GCC 将在 clock _ time _ traits
由于这种方法很有吸引力,一些变通方法是可行的:
- 定义一个宏,其名称和参数与函数相同:
// define as nothing because the return type is void
// otherwise define as an appropriate constant, e.g. 0
#define QueryPerformanceCounter(X)
#if defined(WIN32)
#undef QueryPerformanceCounter // remove the fake...
#include <windows.h> // ...and include the true function
#endif
- 声明—但不定义—函数。这是首选的解决方案,因为 Windows traits 不应该链接到其他操作系统中。
#if !defined(WIN32)
void QueryPerformanceCounter(void*);
#endif
注意一个常见的窍门,如果函数返回 void,就是将函数本身的名字定义为< nothing >。逗号分隔的参数列表将被解析为逗号运算符。
这也允许使用省略号函数:
#define printf
printf("Hello world, %f", cos(3.14));
然而,有几个潜在的问题。首先,宏将表达式的返回类型更改为 double(最后一个参数)。此外,程序还在评估 cos(3.14)。另一种也能最大限度地减少运行时工作量的方法是:
inline bool discard_everything(...) { return false };
#define printf false && discard_everything
4.2.4.合并特征
特别是当您处理大型特征时,让用户定制特征类的较小部分是一个很好的实践。通常,这个问题的解决方法是将 traits 类分成几个部分,然后使用公共继承将它们重新组合,形成一个 traits 默认值。
假设您在 traits 中对一些比较运算符进行分组:
template <typename T>
struct binary_relation_traits
{
static bool gt(const T& x, const T& y) { return x>y; }
static bool lt(const T& x, const T& y) { return x<y; }
static bool gteq(const T& x, const T& y) { return x>=y; }
static bool lteq(const T& x, const T& y) { return x<=y; }
static bool eq(const T& x, const T& y) { return x==y; }
static bool ineq(const T& x, const T& y) { return x!=y; }
};
binary_relation_traits 的一般实现假设 T 定义了所有六个比较运算符,但是这个例子支持两个重要的特例,即:
- t 仅定义运算符<
- t 仅定义运算符
没有您的支持,用户将不得不从头开始实现所有的 traits 结构。所以必须重新排列代码 如下:
template <typename T>
struct b_r_ordering_traits
{
static bool gt(const T& x, const T& y) { return x>y; }
static bool lt(const T& x, const T& y) { return x<y; }
static bool gteq(const T& x, const T& y) { return x>=y; }
static bool lteq(const T& x, const T& y) { return x<=y; }
};
template <typename T>
struct b_r_equivalence_traits
{
static bool eq(const T& x, const T& y) { return x==y; }
static bool ineq(const T& x, const T& y) { return x!=y; }
};
template <typename T>
struct binary_relation_traits
: public b_r_ordering_traits<T>
, public b_r_equivalence_traits<T>
{
};
然后你要写备选块 ,可以组合:
template <typename T>
struct b_r_ordering_less_traits
{
static bool gt(const T& x, const T& y) { return y<x; }
static bool lt(const T& x, const T& y) { return x<y; }
static bool gteq(const T& x, const T& y) { return !(x<y); }
static bool lteq(const T& x, const T& y) { return !(y<x); }
};
template <typename T>
struct b_r_equivalence_equal_traits
{
static bool eq(const T& x, const T& y) { return x==y; }
static bool ineq(const T& x, const T& y) { return !(x==y); }
};
template <typename T>
struct b_r_equivalence_less_traits
{
static bool eq(const T& x, const T& y) { return !(x<y) && !(y<x); }
static bool ineq(const T& x, const T& y) { return x<y || y<x; }
};
最后,通过派生和一个隐藏的模板参数 来组合这些片段。
enum
{
HAS_JUST_OPERATOR_LESS,
HAS_OPERATOR_LESS_AND_EQ,
HAS_ALL_6_OPERATORS
};
template <typename T, int = HAS_ALL_6_OPERATORS>
struct binary_relation_traits
: b_r_ordering_traits<T>
, b_r_equivalence_traits<T>
{
};
template <typename T>
struct binary_relation_traits<T, HAS_JUST_OPERATOR_LESS>
: b_r_ordering_less_traits<T>
, b_r_equivalence_less_traits<T>
{
};
template <typename T>
struct binary_relation_traits<T, OPERATOR_LESS_AND_EQ>
: b_r_ordering_less_traits<T>
, b_r_equivalence_equal_traits<T>
{
};
此外,可以使用适当的枚举和“按位或”语法来链接特征。 5
如果您想提供一个枚举集,包含 2 的幂,将使用标准的标志成语 ,进行组合,但是在编译时:
fstream fs("main.txt", ios::in | ios:out);
typedef binary_relation_traits<MyType, native::less | native::eq> MyTraits;
首先,您让标志从 1 开始,因为您需要 2 的幂。
namespace native
{
enum
{
lt = 1,
lt_eq = 2,
gt = 4,
gt_eq = 8,
eq = 16,
ineq = 32
};
}
第二,使用部分专门化将 traits 类分成原子:
template <typename T, int FLAG>
struct binary_relation_traits; // no body!
template <typename T>
struct binary_relation_traits<T, native::lt>
{
static bool lt(const T& x, const T& y) { return x<y; }
};
template <typename T>
struct binary_relation_traits<T, native::lt_eq>
{
static bool lteq(const T& x, const T& y) { return x<=y; }
};
// and so on...
如果用户提供的位掩码标志设置为(native::ineq |...),性状
你需要一个辅助元函数叫做 static _ highest _ bit
有了这个工具,您就有了一个实现:
template <typename T, unsigned FLAG>
struct binary_relation_traits;
template <typename T>
struct binary_relation_traits<T, 0>
{
// empty!
};
template <typename T>
struct binary_relation_traits<T, native::lt>
{
static bool lt(const T& x, const T& y) { return x<y; }
};
template <typename T>
struct binary_relation_traits<T, native::gt>
{
static bool gt(const T& x, const T& y) { return x>y; }
};
// write all remaining specializations
// then finally...
template <typename T, unsigned FLAG>
struct binary_relation_traits
: binary_relation_traits<T, FLAG & (1 << static_highest_bit<FLAG>::value)>
, binary_relation_traits<T, FLAG – (1 << static_highest_bit<FLAG>::value)>
{
// empty!
};
现在,用户可以在编译时选择 binary_relation_traits 成员:
typedef binary_relation_traits<MyType, native::less | native::eq> MyTraits;
MyType a, b;
MyTraits::lt(a,b); // ok.
MyTraits::lteq(a,b); // error: undefined
这种技术本身很有趣,但是它不满足最初的要求,因为您只能选择“本地”操作符 。但是您可以添加更多的标志:
namespace native
{
enum
{
lt = 1,
lt_eq = 2,
gt = 4,
gt_eq = 8,
eq = 16,
ineq = 32
};
}
namespace deduce
{
enum
{
ordering = 64,
equivalence = 128,
ineq = 256
};
}
template <typename T>
struct binary_relation_traits<T, deduce::ordering>
{
static bool gt(const T& x, const T& y) { return y<x; }
static bool gteq(const T& x, const T& y) { return !(x<y); }
static bool lteq(const T& x, const T& y) { return !(y<x); }
};
template <typename T>
struct binary_relation_traits<T, deduce ::ineq>
{
static bool ineq(const T& x, const T& y) { return !(x==y); }
};
template <typename T>
struct binary_relation_traits<T, deduce::equivalence>
{
static bool eq(const T& x, const T& y) { return !(x<y) && !(y<x); }
static bool ineq(const T& x, const T& y) { return x<y || y<x; }
};
typedef
binary_relation_traits
<
MyType,
native::less | deduce::ordering | deduce::equivalence
>
MyTraits;
注意,任何不必要的重复(比如 native::ineq | deduct::ineq)都会在第一次使用时触发编译器错误。如果特质< T,N >和特质< T,M >都有一个成员 x,特质< T,N+M > ::x 就是一个模棱两可的称呼。
4.3 .sfinae
“替换失败不是错误”(或 SFINAE) 原则是 C++ 标准提供的一种保证。您将确切地看到它的含义,以及当函数模板不满足编译时条件时,如何从重载集中移除函数模板。
记住,当一个类模板被实例化时,编译器生成:
- 班级级别的每个成员签名
- 只有严格必要的函数体
因此,这段代码 无法编译:
template <typename T>
struct A
{
typename T::pointer f() const
{
return 0;
}
};
A<int> x;
一旦遇到一个
template <typename T>
struct A
{
int f() const
{
typename T::type a = 0;
return a;
}
};
A<int> x;
只要一个
此外,当编译器遇到 f(x)并且 X 具有类型 X 时,它应该决定调用哪个特定的 f,因此它将所有可能的候选对象从最好的到最差的排序,并尝试在任何模板参数中替换 X。如果这种替换产生了具有无效签名的函数(签名,而不是主体!),候选人默默丢弃。这就是 SFINAE 原理。
template <typename T>
typename T::pointer f(T*);
int f(void*);
int* x = 0;
f(x);
首选第一个 f,因为 T比 void更匹配;但是 int 没有名为指针的成员类型, 所以使用第二个 f。SFINAE 仅在替换产生形式上无效的表达式时适用(如 int::pointer)。相反,当结果是无法编译的类型时,它不适用:
template <typename T, int N>
struct B
{
static const int value = 100/N;
};
template <typename T>
B<T, 0> f(T*);
int f(void*);
B
为了利用 SFINAE 的 ,当您想要“启用”或“禁用”一个函数模板的特定重载时,您可以在它的签名中人为地插入一个依赖名称,该名称可能会解析为一个无效的表达式(一个不存在的类型,如 int::pointer)。
如果所有的候选项都被丢弃了,就会出现一个编译器错误(SFINAE 的简单使用实际上看起来像静态断言)。
SFINAE 的应用主要有两种:f 被选中后运行时和 f 根本不执行时。
4.3.1 .sfinae 元函数
使用 SFINAE 和 sizeof ,您可以编写基于 t 类型的接口做出决定的元函数。这非常接近于不同编程语言中所谓的反射。
基本成分是:
- 尺寸不同的两种(或多种)类型;让我们称之为是和不是。
- 一组重载函数 f,其中至少有一个必须是模板,返回 YES 或 NO。
- 用 sizeof(f(something))定义的静态常数。
以下范例有助于澄清这一点:
template <typename T>
class YES { char dummy[2]; }; // has size > 1
typedef char NO; // has size == 1
template <typename T>
class MF
{
template <typename X>
static YES<[[condition on X]]> test(X);
static NO test(...);
static T this_type();
public:
static const bool value = sizeof(test(this_type())) != sizeof(NO);
};
当参数类型为 t 时,编译器必须决定调用哪个测试。它将首先尝试评估 YES (因为 void*和省略号...具有非常低的优先级)。如果这会生成无效的类型,test 的第一个重载将被丢弃,它将选择另一个重载。
请注意一些重要事实:
- 静态函数不需要有主体;sizeof 中只使用他们的签名。
- 是的
不需要 2 号。如果写 sizeof(test(this _ type())= = 2,那就错了。然而,char 必须的大小为 1,因此您可以验证 sizeof(test(this _ type())>是否为 1。 - 至少一个测试函数应该是依赖于新的参数 x 的模板。根据 T(MF 的参数)定义测试是错误的,因为 SFINAE 不适用。
- 您使用一个返回 T 的伪函数,而不是调用 test(T()),因为 T 可能没有默认的构造函数。
有些编译器会发出警告,因为将对象传递给省略号函数是非法的。实际上,代码不会运行,因为 sizeof 包装了整个表达式,但是警告可能会很长,很烦人。一个好的解决方法是传递指向函数的指针:
template <typename X>
static YES<[[condition on X]]> test(X*);
static NO test(...);
static T* this_type();
如果切换到指针:
- void 成为可接受的类型(因为 T*存在)。
- 引用变得非法(指向引用的指针是错误的)。
所以无论哪种方式,你都必须写一些明确的 MF 专门化来处理极限情况。
如果模板参数的任何替换产生了无效类型,不一定在返回类型中,则 SFINAE 适用。事实上,有时使用参数更方便:
template <typename T>
class MF
{
template <typename X>
static YES<void> test([[type that depends on X]]*);
template <typename X>
static NO test(...);
public:
static const bool value = sizeof(test<T>(0)) != sizeof(NO);
};
如果第一个表达式中 X 的替换产生了一个有效的类型,从而产生了一个有效的指针,test
显式调用测试
举个简单的例子,您可以测试 T 类型是否有一个名为 pointer 的成员类型:
template <typename T>
class has_pointer_type
{
template <typename X>
static YES<typename X::pointer> test(X*);
static NO test(...);
static T* this_type();
public:
static const bool value = sizeof(test(this_type())) != sizeof(NO);
};
或者(几乎)等价地: 8
template <typename T>
class has_pointer_type
{
template <typename X>
static YES<void> test(typename X::pointer*);
template <typename X>
static NO test(...);
public:
static const bool value = sizeof(test<T>(0)) == sizeof(YES);
};
通过将模板参数修改为 YES,可以检查 T 是否有一个名为 value 的静态常量。再一次,从一个常见的“是”类型中推导出来是很方便的:
// copied from Section 2.1.4
typedef char no_type;
typedef larger_than<no_type> yes_type;
template <int VALUE>
struct YES2 : yes_type
{
};
template <typename T>
class has_value
{
template <typename X>
static YES2<X::value> test(X*);
// ...
};
或者您可以检查是否存在具有固定名称和签名的成员函数 9 :
template <typename T, void (T::*F)(T&)>
struct YES3 : yes_type
{
};
template <typename T>
class has_swap_member
{
template <typename X>
static YES3<X, &X::swap> test(X*);
// ...
};
最后,一个流行的习惯用法是使用一个假的指向成员的指针来检查 T 是一个类还是一个基本类型。(文字零可以强制转换为 int T::*如果 T 是一个类,即使它没有 int 类型的成员。)
template <typename T>
class is_class
{
template <typename X>
static yes_type test(int X::*);
template <typename X>
static no_type test(...);
public:
static const bool value = (sizeof(test<T>(0))!=sizeof(no_type));
};
4.3.2.多重决策
到目前为止显示的示例采用了单一的是/否决策路径,但是一些标准可能更复杂。让我们写一个元函数来标识所有带符号的整数 10 :
if (T is a class)
return false
if (T is a pointer)
return false
if (T is a reference)
return false
if (we can have a non-type template parameter of type T)
{
if (the expression "T(0) > T(-1)" is well-formed and true)
return true
else
return false
}
else
{
return false
}
template <typename X, bool IS_CLASS = is_class<X>::value>
class is_signed_integer;
template <typename X>
class is_signed_integer<X*, false> : public selector<false>
{
};
template <typename X>
class is_signed_integer<X&, false> : public selector<false>
{
};
template <typename X>
class is_signed_integer<X, true> : public selector<false>
{
};
template <typename X>
class is_signed_integer<X, false>
{
template <typename T>
static static_parameter<T, 0>* decide_int(T*);
static void* decide_int(...);
template <typename T>
static selector<(T(0) > T(-1))> decide_signed(static_parameter<T, 0>*);
static selector<false> decide_signed(...);
static yes_type cast(selector<true>);
static no_type cast(selector<false>);
static X* getX();
public:
static const bool value =
sizeof(cast(decide_signed(decide_int(getX()))))==sizeof(yes_type);
};
对于最终的 sizeof 测试,cast 将所有可能的中间返回类型映射到 yes_type 或 no_type。
一般来说,可以扩展这个想法,返回一个枚举(更准确地说,一个 size_t),而不是 bool。假设你有更多的中间决策案例:
static T1 decide(int*);
static T2 decide(double*);
...
static Tn decide(void*);
然后你可以绘制 T1,T2,...使用 fixed_size 将 Tn 转换为枚举:
static fixed_size<1>::type& cast(T1);
static fixed_size<2>::type& cast(T2);
// ...
public:
static const size_t value = sizeof(cast(decide(...)));
};
4.3.3.仅 _ 如果
SFINAE 的另一个有趣的用途是从一组重载(成员)函数中排除不符合某些条件的元素:
template <bool CONDITION>
struct static_assert_SFINAE
{
typedef void type;
};
template <>
struct static_assert_SFINAE<false>
{
};
如果函数具有指向 X 的指针类型的参数,其中 X 被定义为 static _ assert _ SFINAE<...>:::type,则替换任何评估为 false 的条件都会生成无效的表达式。以便从重载集中移除特定的函数。
伪指针参数的默认值为 0,这意味着用户可以安全地忽略它的存在。 11
#define ONLY_IF(COND) typename static_assert_SFINAE<COND>::type* = 0
template <typename T>
void f(T x, ONLY_IF(is_integer<T>::value))
{
}
void f(float x)
{
}
// later...
double x = 3.14;
f(x); // calls f(float)
这种技术通常在类模板的通用复制构造函数中很有用:
template <typename T1>
class MyVector
{
public:
// not used if T2 is T1
template <typename T2>
MyVector(const MyVector<T2>& that)
{
}
};
使用 ONLY_IF (has_conversion 可以很容易地引入对 T2 的限制,这在 4.4 节中有完整的说明。
template <typename T2>
MyVector(const MyVector<T2>& that,
ONLY_IF((has_conversion<T2,T1>::L2R)))
{
}
另一个应用是 static_value 的“静态造型”。你可能需要把静态值
template <typename T, T VALUE>
struct static_value
{
static const T value = VALUE;
static_value(const int = 0)
{
}
template <typename S, S OTHER>
static_value(const static_value<S, OTHER>,
typename only_if<VALUE==OTHER, int>::type = 0)
{
}
};
有时,不是对参数,而是对返回值应用这个习惯用法会很有用:
template <bool CONDITION, typename T = void>
struct only_if
{
typedef T type;
};
template <typename T>
struct only_if<false, T>
{
};
template <typename T>
typename only_if<is_integer<T>::value,T>::type multiply_by_2(const T x)
{
return x << 1;
}
这个函数要么格式错误,要么接受一个常量 T 并返回 T。
4.3.4.SFINAE 和返回的函子
到目前为止,您看到的各种测试函数对它们的返回类型没有用处,返回类型的大小才是最重要的。有时它们会返回一个被立即调用的仿函数。考虑一个简单的例子,其中如果 x 有一个名为 size_type 的类型成员,函数 number_of_elem 返回 x.size(),否则返回 1。
template <typename T, typename S>
struct get_size
{
S operator()(const T& x) const { return x.size(); }
get_size(int) {}
};
struct get_one
{
template <typename T>
size_t operator()(const T&) const { return 1; }
get_one(int) {}
};
template <typename T>
get_size<T, typename T::size_type> test(const T* x) // SFINAE
{
return 0;
}
get_one test(const void*)
{
return 0;
}
template <typename T>
size_t number_of_elem(const T& x)
{
return test(&x)(x);
}
std::vector<int> v;
std::map<int, double> m;
double x;
number_of_elem(v); // returns v.size()
number_of_elem(m); // returns m.size()
number_of_elem(x); // returns 1
您可以使用上一段中的一些技术来描述一个日志回调的实现,它具有基于元编程的可变日志级别。
在科学计算中,你可以遇到长时间运行的函数。因此,即使在函数运行时,也有必要保持与函数的交互,例如,获取进度反馈或发送中止信号。由于没有对环境的假设(计算例程通常是可移植的),你不能传递一个进度条的指针,你必须设计一个同样可移植的界面。
一个可能的解决方案如下。该函数在内部用关于程序状态的所有有意义的信息更新一个结构(其类型为其调用方所知),并且它在该结构上定期调用一个用户仿函数:
struct algorithm_info
{
int iteration_current;
int iteration_max;
double best_tentative_solution;
size_t time_elapsed;
size_t memory_used;
};
template <..., typename logger_t>
void algorithm(..., logger_t LOG)
{
algorithm_info I;
for (...)
{
// do the work...
I.iteration_current = ...;
I.best_tentative_solution = ...;
LOG(I);
}
}
您可以尝试在记录器和算法之间设计一些静态交互,以便只更新信息的一些相关部分。如果 LOG 什么都不做,就不会浪费时间更新 I。
首先,所有可记录的信息都被分成不同的级别。logger_t 将声明一个名为 log_level 的静态常量,算法循环将不会更新与被忽略级别中的信息相对应的对象。
按照惯例,没有成员 log_level 或 log_level=0 相当于跳过日志。
template <int LEVEL = 3>
struct algorithm_info;
template <>
struct algorithm_info<0>
{
};
template <>
struct algorithm_info<1> : algorithm_info<0>
{
int iteration_current;
int iteration_max;
};
template <>
struct algorithm_info<2> : algorithm_info<1>
{
double best_value;
};
template <>
struct algorithm_info<3> : algorithm_info<2>
{
size_t time_elapsed;
size_t memory_used;
};
第二,使用 SFINAE 查询 logger_t 中名为 log_level 的常量:
template <int N>
struct log_level_t
{
operator int () const
{
return N;
}
};
template <typename T>
log_level_t<T::log_level> log_level(const T*)
{
return log_level_t<T::log_level>();
}
inline int log_level(...)
{
return 0;
}
最后,一个简单的开关就可以了。如果 logger_t 包含 log_level,SFINAE 将选择 log_level 的第一个重载,返回一个立即转换为 integer 的对象。否则,较弱的重载将立即返回 0。
switch (log_level(&LOG))
{
case 3:
I.time_elapsed = ...;
I.memory_used = ...;
case 2: // fall through
I.best_value = ...;
case 1: // fall through
I.iteration_current = ...;
I.iteration_max = ...;
case 0: // fall through
default:
break;
}
LOG(I);
这个实现是最简单的代码,但是 LOG 仍然可以访问整个对象 I,甚至是没有初始化的部分。
关于级别的静态信息已经包含在 log_level_t 中,因此将该对象转换为执行转换的仿函数是合适的。
template <int N>
struct log_level_t
{
operator int () const
{
return N;
}
typedef const algorithm_info<N>& ref_n;
typedef const algorithm_info< >& ref;
ref_n operator()(ref i) const
{
return i;
}
};
template <typename T>
log_level_t<T::log_level> log_level(const T*)
{
return log_level_t<T::log_level>();
}
inline log_level_t<0> log_level(...)
{
return log_level_t<0>();
}
switch (log_level(&LOG))
{
// as above...
}
LOG(log_level(&LOG)(I));
这迫使 LOG 实现一个操作符(),该操作符只接受正确的信息“片段”。
4.3.5.SFINAE 和软件更新
基于 SFINAE 的元函数的许多用途之一是条件需求检测。
TMP 库经常与用户类型和用户函子交互,这通常必须满足一些(最小的)接口约束。原则上,这些库的新版本会对额外的优化提出额外的要求,但这通常会与向后兼容性相冲突。
假设您通过将一个自定义二进制关系传递给一个外部库函数(称为 nonstd::sort)来对一个范围进行排序:
struct MyLess
{
bool operator()(const Person& x, const Person & y) const
{
// ...
}
};
std::vector<Person> v;
nonstd::sort(v.begin(), v.end(), MyLess());
排序库 2.0 版本要求 MyLess 包含一个名为 static void CompareAndSwap(Person&a,Person & b)的附加函数,所以这段代码不会编译。
相反,该库可以很容易地检测到是否提供了这样一个函数,如果提供了,就自动调用一个更快的基于 CAS 的并行算法。
这种特性的“自我检测”允许独立升级底层库。
这也适用于特征:
struct MyTraits
{
static const bool ENABLE_FAST_ALLOCATOR = true;
static const bool ENABLE_UTF8 = true;
static const bool ENABLE_SERIALIZATION = false;
};
typedef nonstd::basic_string<char, MyTraits> MyString;
字符串 库的 2.0 版本使用了一个额外的成员:
struct MyTraits
{
static const bool ENABLE_FAST_ALLOCATOR = true;
static const bool ENABLE_UTF8 = true;
static const bool ENABLE_SERIALIZATION = false;
static const size_t NUMBER_OF_THREADS = 4;
};
但是库的作者不应该假设这个新的常量存在于他接收的 traits 类中。但是,他可以使用 SFINAE 间接提取该值(如果存在),或者使用默认值:
template <typename T, size_t DEFAULT>
class read_NUMBER_OF_THREADS
{
template <typename X>
static static_value<size_t, X::NUMBER_OF_THREADS> test(X*);
static static_value<size_t, DEFAULT> test(void*);
template <size_t N>
static typename fixed_size<N+1>::type& cast(static_value<size_t,N>);
static T* getT();
public:
static const size_t value = sizeof(cast(test(getT()))) - 1;
};
+1/-1 技巧是避免长度为零的数组所必需的。
nonstd::basic_string 的作者会写:
template <typename char_t, typename traits_t>
class basic_string
{
// ...
int n = read_NUMBER_OF_THREADS<traits_t, 4>::value;
所以这个类甚至可以编译旧的特征。
作为一个规则,你不需要检查 NUMBER_OF_THREADS 确实有 type (static const) size_t,任何整数都可以。有可能更严谨,但一般不值得机械。我将展示所有的细节,但是您应该将这一部分的剩余部分视为一个练习。你需要三个额外的元函数:
- 用通常的技术检测 T 是否有任何名为 NUMBER_OF_THREADS 的常量。
- 如果这是假的,结果立即为假(第 2 行)。
- 否则,使用不同的专门化,在这里编写 T::NUMBER_OF_THREADS 是合法的。您将这个“项目”传递给一个测试函数(第 1 行)。最佳选择是带有 REQUIRED_T 类型参数的非模板函数;另一个选项是匹配所有其他内容的模板,因此不会发生强制转换。
template <typename T>
struct has_any_NUMBER_OF_THREADS
{
template <typename X>
static static_value<size_t, X::NUMBER_OF_THREADS> test(X*);
static no_type test(void*);
template <size_t N>
static yes_type cast(static_value<size_t, N>);
static no_type cast(no_type);
static T* getT();
static const bool value = (sizeof(cast(test(getT()))) > 1);
};
template <typename REQUIRED_T, typename T, bool>
struct check_NUMBER_OF_THREADS_type;
template <typename REQUIRED_T, typename T>
struct check_NUMBER_OF_THREADS_type<REQUIRED_T, T, true>
{
static yes_type test(REQUIRED_T);
template <typename X>
static no_type test(X);
static const bool value
= sizeof(test(T::NUMBER_OF_THREADS))>1; // line #1
};
template <typename REQUIRED_T, typename T>
struct check_NUMBER_OF_THREADS_type<REQUIRED_T, T, false>
{
static const bool value = false; // line #2
};
template <typename T>
struct has_valid_NUMBER_OF_THREADS
: check_NUMBER_OF_THREADS_type<size_t, T,
has_any_NUMBER_OF_THREADS<T>::value>
{
};
4.3.6.局限性和解决方法
SFINAE 技术最终依赖于编译器优雅地处理错误,因此它们特别容易受到编译器错误的攻击。
如果正确的代码无法编译,这里有一个工作区清单 :
- 给所有的功能一个身体。
- 将静态函数移到类之外的私有命名空间中。
- 移除 private 并使用 struct。
- 想一个更简单的算法。
表 4-1。变通办法前后的代码对比
| 模板
{
模板
static static _ value<T,0>* decide _ int(T *);
静态 void* decide_int(...);
模板<类型名 T >
静态选择器<(T(0)>T(-1))>
decide _ signed(static _ value<T,0>*);
静态选择器<假> decide_signed(...);
静态 yes_type cast(选择器<真>);
静态 no_type cast(选择器<假>);
静态 X * getX();
public:
static const bool value =
sizeof(cast(decide _ signed(decide _ int(getX()))))
= = sizeof(yes _ type);
}; |
namespace priv {
模板
static _ value<T,0>* decide _ int(T *);
void* decide_int(...);
模板<类型名 T >
选择器<(T(0)>T(-1))>
decide _ signed(static _ value<T,0>*);
选择器<假>判定 _ 签名(...);
yes_type cast(选择器
no_type cast(选择器<假>);
模板< typename X >
结构 is _ signed _ integer _ helper
{
X * getX();
静态常量 bool 值=
sizeof(cast(decide _ signed(decide _ int(getX()))))
= = sizeof(yes _ type);
};
} //命名空间结束
模板
struct is _ signed _ integer
:公共选择器<priv::is _ signed _ integer _ helper
{}; |
标准中的一个极端情况是 sizeof 内部的替换失败,该 sizeof 应该绑定到模板参数。下面的例子通常不会编译:
template <typename T>
class is_dereferenceable
{
template <size_t N>
class YES { char dummy[2]; };
template <typename X>
static YES<sizeof(*X())> test(X*);
static NO test(...);
static T* this_type();
public:
static const bool value = sizeof(test(this_type()))>1;
};
成员函数的检测很成问题。让我们在这里重写元函数。
template <typename S>
class has_swap_member
{
template <typename T, void (T::*)(T&) >
class YES { char dummy[2]; };
typedef char NO;
template <typename T>
static YES<T, &T::swap> test( T* );
static NO test(...);
static S* ptr();
public:
static const bool value = sizeof(test(ptr()))>1;
};
假设 D1 和 D2 类有一个名为 B
但是,has_swap_member
假设,它将匹配 YES
此外,该标准明确指出,不能将指针指向库对象的成员函数,因为只要语法按预期工作,实现就可以修改原型。例如,您可以拥有一个完全有效的 void T::swap(T & amp,int = 0)。
所以 has _ swap _ member
您能做的最好的事情就是将检测阶段与交换本身集成在一起,并创建一个函数,用最著名的方法交换两个引用。当交换检测失败时,ADL 通常会在正确的名称空间中找到一个等价的例程(至少对于所有 STL 容器是这样;参见第 1.4.2 节。
using std::swap;
struct swap_traits
{
template <typename T>
inline static void apply(T& a, T& b)
{
apply1(a, b, test(&a));
}
private:
template <typename T, void (T::*F)(T&)>
struct yes : public yes_type
{
yes(int = 0)
{}
};
template <typename T>
static yes<T, &T::swap> test(T*)
{ return 0; }
static no_type test(void*)
{ return 0; }
template <typename T>
inline static void apply1(T& a, T& b, no_type)
{
swap(a, b);
}
template <typename T>
inline static void apply1(T& a, T& b, yes_type)
{
a.swap(b);
}
};
template <typename T>
inline void smart_swap(T& x, T& y)
{
swap_traits::apply(x, y);
}
注意,所有函数都有一个体,因为它们是真正被调用的。
工作流程如下。smart_swap(x,y)调用 apply,而 apply 又是 apply1(x,y,[[T 上的条件]])。当条件为否时,apply1 是 ADL 交换,否则是成员交换调用。
#include <map>
struct swappable
{
void swap(swappable&)
{
}
};
int main()
{
std::map<int, int> a, b;
smart_swap(a, b); // if it fails detection of map::swap
// then it uses ADL swap, which is the same
swappable c, d;
smart_swap(c, d); // correctly detects and uses swappable::swap
int i = 3, j = 4;
smart_swap(i, j); // correctly uses std::swap
}
注意真解需要 C++0x 关键字 decltype。参见第 12.2 节。
最后一个警告是避免将 SFINAE 与私有成员混合。
C++ 2003 标准说访问控制发生在模板演绎之后。因此,如果 T::type 存在,但它是私有的,SFINAE 将根据 T::type 实际存在的信息选择一个操作,但编译器错误通常会立即发生(因为 T::type 不可访问)。 13
template <typename T>
typename T::type F(int);
template <typename T>
char F(...);
class X
{
typedef double type; // note: private, by default
};
// A condensed version of the usual SFINAE machinery...
// We would expect the code to compile and N==1.
// This occurs only in C++0x
int N = sizeof(F<X>(0));
error: type "X::type" is inaccessible
typename T::type F(int);
^
detected during instantiation of "F" based on template argument <X>
4.3.7.具有部分专门化的 SFINAE
SFINAE 也适用于类模板的部分专门化。当用于选择部分特化的条件是病态的时,该特化将从候选集中被无声地删除。本节通过一个例子展示了一个实际应用。 14
假设您有一个名为
首先,向添加第二个模板参数,并在第二个模板参数上添加部分专用化(稍后将定义 DEFAULT_TYPE 和 METAFUNC):
template <typename T, typename X = DEFAULT_TYPE>
struct A
{ ... };
template <typename T>
struct A<T, typename METAFUNC<typename T::iterator>::type >
{ ... };
根据 SFINAE 的说法,当 T::iterator 不存在时,会忽略特殊化,使用通用模板。然而,当 T::iterator 确实存在(并且 METAFUNC 定义良好)时,两种定义都有效。但根据 C++ 语言规则,如果 DEFAULT_TYPE 恰好与元函数<:iterator>:::TYPE 相同,则使用 A 的专门化。让我们更清楚地重写这个例子:
template <typename T>
struct METAFUNC
{
typedef int type;
};
template <typename T, typename X = int>
struct A
{ ... };
template <typename T>
struct A<T, typename METAFUNC<typename T::iterator>::type >
{ ... };
A<int> a1; // uses the general template
A<std::vector<int>> a2; // uses the specialization
4.4.Sizeof 的其他经典元函数
可以选择重载,因为可以成功转换参数。
本节展示了一个返回三个布尔常量的元函数—当 L(左)可转换为 R(右)时 has _ conversion
template <typename L, typename R>
class has_conversion
{
static yes_type test(R);
static no_type test(...);
static L left();
public:
static const bool L2R = (sizeof(test(left())) == sizeof(yes_type));
static const bool identity = false;
};
template <typename T>
class has_conversion<T, T>
{
public:
static const bool L2R = true;
static const bool identity = true;
};
这段代码传递一个假的 L 实例进行测试。如果 L 可转换为 R,则首选第一个重载,结果为 yes_type。
遵循 Alexandrescu, 16 你可以推导出一个类型是否公开地从另一个派生:
template <typename B, typename D>
struct is_base_of
{
static const bool value =
(
has_conversion<const D*, const B*>::L2R &&
!has_conversion<const B*, const void*>::identity
);
};
大卫·亚伯拉罕广泛使用了技巧 。 17 重点是在命名空间级别重载一个操作符,而不是作为成员。
struct fake_incrementable
{
template <typename T>
fake_incrementable(T); // non-explicit universal constructor
};
fake_incrementable operator++(fake_incrementable); // line #1
yes_type test(fake_incrementable);
template <typename T>
no_type test(T);
template <class T>
struct has_preincrement
{
static T& getT();
static const bool value = sizeof(test(++getT())) == sizeof(no_type);
};
++getT()语句可以解析为 x 自己的运算符++ 或者(优先级较低)解析为转换为 fake_incrementable ,后跟 fake_incrementable 增量。后一个函数是可见的,因为正如预期的那样,它在名称空间中被声明为全局实体,而不是成员函数。
要测试后增量,请将第 1 行替换为:
fake_incrementable operator++(fake_incrementable, int);
注意,sizeof(test ++ x)的计算必须在 fake_incrementable 所在的名称空间中完成。否则,它将失败:
namespace aux {
struct fake_incrementable
{
template <typename T>
fake_incrementable(T);
};
fake_incrementable operator++(fake_incrementable);
yes_type test(fake_incrementable);
template <typename T>
no_type test(T);
}
template <typename T>
struct has_preincrement
{
static T& getT();
static const bool value
= sizeof(aux::test(++getT())) == sizeof(no_type);
};
您还可以将计算移动到名称空间内部,并在外部调用结果:
namespace aux {
// ... (all as above)
template <typename T>
struct has_preincrement_helper
{
static T& getT();
static const bool value = sizeof(test(++getT())) == sizeof(no_type);
};
}
template <typename T>
struct has_preincrement : selector<aux::has_preincrement_helper<T>::value>
{
};
4.5.函数指针上的重载
用于选择重载函数的最方便的标记对象之一是函数指针,它随后被丢弃。
指针构建起来很便宜,但是可以传递大量的静态信息,这使得它适合于模板参数推导。
4.5.1.擦除
下面是主要的例子。它在一个 STL 容器上迭代,所以你需要擦除迭代器 I 指向的元素,擦除应该推进(而不是无效)迭代器本身。不幸的是,语法不同。对于某些容器,正确的语法是 i = c.erase(i),但是对于关联容器,正确的语法是 c.erase(i++)。
利用 C::erase 必须存在这一事实(否则您将不知道该做什么,并且对 erase_gap 的调用将是错误的),您只需用一个伪指针选择正确的一个:
template <typename C, typename iterator_t, typename base_t>
void erase_gap2(C& c, iterator_t& i, iterator_t (base_t::*)(iterator_t))
{
i = c.erase(i);
}
template <typename C, typename iterator_t, typename base_t>
void erase_gap2(C& c, iterator_t& i, void (base_t::*)(iterator_t))
{
c.erase(i++);
}
template <typename C>
void erase_gap(C& c, typename C::iterator& i)
{
erase_gap2(c, i,&C::erase);
}
int main()
{
for (i = c.begin(); i != c.end(); )
{
if (need_to_erase(i))
erase_gap(c, i);
else
++i;
}
}
观察擦除是通过指针调用的而不是。重要的只是指针的类型。
此外,擦除的类型可能不是...(丙:😗)(...),因为一个容器可能有一个“隐藏的底座”。因此,确切的类型留给编译器推导。
4.5.2.交换
前面的技术可以通过 SFINAE 扩展到未知成员函数是否存在的情况。为了演示,您需要扩展 swap_traits(在 4.3.6 节中介绍)来执行下面的 18 :
- 如果 T 有 void T::swap(T&),使用 a.swap(b)。
- 如果 T 有静态 void swap(T & amp,T & amp),则使用 T::swap(a,b)。
- 如果 T 有两个交换,调用是不明确的。
- 在任何其他情况下,使用 ADL swap。
第一部分简单地重用了前几节中的技术。特别要注意的是,所有的 yes 类型都是从一个公共的“yes-base”派生出来的,因为第一个测试只是为了确保可能的交换成员函数存在。
struct swap_traits
{
template <typename T, void (T::*F)(T&)>
class yes1 : public yes_type {};
template <typename T, void (*F)(T&, T&)>
class yes2 : public yes_type {};
template <typename T>
inline static void apply(T& a, T& b)
{
apply1(a, b, test(&a));
}
private:
// first test: return a yes_type* if any allowed T::swap exists
template <typename T>
static yes1<T, &T::swap>* test(T*)
{ return 0; }
template <typename T>
static yes2<T, &T::swap>* test(T*)
{ return 0; }
static no_type* test(void*)
{ return 0; }
当测试为假时,调用 ADL swap。否则,执行基于功能指针的测试。通过获取 swap 的地址来调用 apply2,这是可行的,因为至少存在一个 swap。
private:
template <typename T>
inline static void apply1(T& a, T& b, no_type*)
{
swap(a, b);
}
template <typename T>
inline static void apply1(T& a, T& b, yes_type*)
{
apply2(a, b,&T::swap);
}
template <typename T>
inline static void apply2(T& a, T& b, void (*)(T&, T&))
{
T::swap(a, b);
}
template <typename T, typename BASE>
inline static void apply2(T& a, T& b, void (BASE::*)(BASE&))
{
a.swap(b);
}
template <typename T>
inline static void apply2(T& a, T& b, ...)
{
swap(a, b);
}
};
4.5.2.论点优势
当一个函数模板有几个类型必须推导出来的参数时,您可能会产生歧义:
template <typename T>
T max(T a1, T a2) { ... }
max(3, 4.0); // error: ambiguous, T may be int or double
通常情况下,一个参数更重要,因此您可以明确指示编译器在类型推导过程中忽略所有其他参数:
// here T must be the type of arg1
template <typename T>
void add_to(T& a1, T a2) { ... }
double x = 0;
add_to(x, 3); // we would like this code to compile
解决这个问题的方法是用一个间接元函数代替 T,这个间接元函数会产生相同的结果。仅对非依赖名称执行类型推导,然后编译器确保结果与任何其他依赖名称兼容:
template <typename T>
void add_to(T& a1, typename instance_of<T>::type a2)
{ ... }
在这个例子中,T&对于类型检测是可行的。T=double 是唯一匹配的。
当 a1 是函数指针,a2 是 a1 的自变量时,这个习惯用法非常流行:
template <typename A, typename R>
R function_call(R (*f)(A), R x)
{ return f(x); }
函数指针是一个主要的参数,因为你可以在任何可转换的东西上调用 f。因此,您应该考虑禁用对 x:
template <typename A, typename R>
R function_call(R (*f)(A), typename instance_of<R>::type x)
{ return f(x); }
15.3 节和 5.3.1 节说明了如何检测 T 是否有成员函数 T T::abs() const。
2 当然,您可以编写一个方法,该方法采用选择器< false >,但是使用模板代替 C 省略可能会有些意思。
例如,非法语句可能是对未声明的函数的调用。回想一下,编译器不需要诊断未实例化的模板中的错误。MSVC 甚至跳过一些基本的语法检查,而 GCC 确实禁止使用未声明的函数和类型。另请参见第 5.2.3 节关于平台特定特征的内容。
4 大多数情况下,选择将取决于发布和指针是独立的还是由相同的特征提供。
5 参见 2.3.3 节。
6 静态 _ 最高 _ 位的详细内容在 3.4.1 节。
7 参见 1.2.1 节。
8 如果 X::pointer 是一个引用,这将失败;目前,你不需要担心这个。
9 互换——检测问题实际上要困难得多;这将在本节稍后讨论。
10 光靠“主算法”是不够的。当 T 是一个基本类型时,它会起作用。有些编译器在 T 为指针时,将表达式 T(0) < T(-1)求值为真;如果 T 是一个没有构造函数的类型,其他编译器会给出错误。这就是为指针、引用和类类型添加显式专门化的原因。但是,请注意,这种方法优于专门化的显式列表,因为它完全独立于编译器/预处理器。
有时候,记录 C++ 代码是可取的,不是按字面意思,而是按照用户应该使用它的方式。这种功能文档也是 C++ 风格的一部分。这里举例说明了 f(T)是一个单参数函数,即使它不是。所有的实现细节都应该隐藏起来。
12 在流行的 STL 实现中,让 D1=std::map,D2=std::set 和 B < T >是一个表示平衡树的无文档类。
13 这是在 C++11 标准中改变的。见www . open-STD . org/JT C1/sc22/wg21/docs/cwg _ defects . html # 1170
。
14 沃尔特·布朗最近让这种技术流行起来。见www . open-STD . org/JT C1/sc22/wg21/docs/papers/2014/n 3911
。
15 左右符号可能不是最优雅的,但对于记住类是如何工作的来说,它确实是极好的。
16 参见参考书目。
17boost::is _ incrementable 正确地从 T 中剥离了限定符,但它允许 operator++ 返回 void,这在一般情况下并不可取。在这种情况下,这里给出的更简单的版本给出了一个编译时错误。
18 这个扩展算是一个练习,但不一定是个好主意。
五、接口
模板以两种不同的方式用作接口:提供原子函数集和获得编译时多态性 。
如果几个函数使用一个对象接口的同一个部分,你可以在一个模板中把它们分解出来:
void do_something(std::vector<double>& v)
{
if (v.empty())
// ...
... v.size();
for_each(v.begin(), v.end(), my_functor());
...
}
void do_something(std::list<double>& L)
{
if (L.empty())
// ...
... L.size();
for_each(L.begin(), L.end(), my_functor());
...
}
变成:
template <typename T>
void do_something(T& L)
{
if (L.empty())
// ...
... L.size();
for_each(L.begin(), L.end(), my_functor());
...
}
当您遵循容器(如 1.4 节所列)的通用指南时,这种代码统一会更简单。
如有必要,如 3.4.3 节所述,您可以将对成员函数的调用替换为对小型全局函数的调用。假设您有第三个 do_something,它执行一个稍微不同的测试:
void do_something(MyContainer<double>& M)
{
if (M.size() == 0)
...
最好将“空性”的测试隔离在不同的函数中:
template <typename T>
bool is_empty(const T& c)
{
return c.empty();
}
template <typename T>
bool is_empty(const MyContainer<T>& c)
{
return c.size() == 0;
}
template <typename T>
void do_something(T& L)
{
if (is_empty(L))
...
5.1.包装参考文献
类模板及其专门化可以用来使接口统一:
class Dog
{
public:
void bark();
void go_to_sleep();
};
class Professor
{
public:
void begin_lesson();
void end_lesson();
};
template <typename T>
class Reference
{
T& obj_;
public:
Reference(T& obj) : obj_(obj) {}
void start_talking() { obj_.talk(); }
void end_talking() { obj_.shut(); }
};
template <>
class Reference<Dog>
{
Dog& obj_;
public:
Reference(Dog& obj) : obj_(obj) {}
void start_talking() { for (int i=0; i<3; ++i) obj_.bark(); }
void end_talking() { obj_.go_to_sleep(); }
};
template <>
class Reference<Professor>
{
Professor& obj_;
public:
Reference(Professor& obj) : obj_(obj) {}
void start_talking() { obj_.begin_lesson(); }
void end_talking() { obj_.end_lesson(); }
};
注意,包装器可能确实包含一些逻辑。最后:
template <typename T>
void DoIt(T& any)
{
Reference<T> r(any);
r.start_talking();
// ...
r.end_talking();
}
5.2.静态接口
当函数模板操作未指定类型 T 的对象时,它实际上强制该对象实现一个接口。例如,这个非常简单的函数包含许多关于所涉及的(未知)类型的隐藏假设:
template <typename iter1_t, typename iter2_t>
iter2_t copy(iter1_t begin, const iter1_t end, iter2_t output)
{
while (begin != end)
*(output++) = *(begin++),
return output;
}
这里 iter1_t 和 iter2_t 必须有一个复制构造函数,叫做 operator++(int)。iter1_t 也需要运算符!=.此外,每个 operator++ 返回一个可取消引用的实体,在 iter2_t 的情况下,最终结果是一个 l 值,其赋值盲目接受*(begin++)返回的任何内容。
简而言之,模板代码假装所有指令都编译了,直到编译器可以证明它们没有编译。
一般来说,在类型接口上列出假设太冗长和/或通常没有用。在前面的例子中,iter1_t::operator++ 可能会返回 iter1_t,它也实现了 operator,但不需要完全是这种情况(例如,如果 iter1_t::operator++ 返回了 int,copy 也可以工作)。
因此,您必须尝试明确列出模板参数必须满足的概念 的最小集合。非正式地说,概念是对类型的一个要求,它意味着 C++ 语句是合法的,不管它的实现如何。 1
例如,这个对象将愉快地扮演 iter2_t 的角色:
struct black_hole_iterator
{
const black_hole_iterator& operator++ () const
{
return *this;
}
const black_hole_iterator& operator++ (int) const
{
return *this;
}
const black_hole_iterator& operator* () const
{
return *this;
}
template <typename T>
const black_hole_iterator& operator= (const T&) const
{
return *this;
}
};
这里满足了“运算符 *返回的对象必须是 l 值”的概念,即使是以一种不寻常的方式(赋值不修改黑洞)。
通常,您不会列出任何通用函数的确切概念。然而,一些概念集有一个标准的名称,所以只要有可能,你就会采用它,即使它是实际需要的超集。
在前面的复制模板中,最好使用一个输入迭代器 和一个输出迭代器 ,因为这些是标识概念(超)集的最小的众所周知的标签。正如你将在第六章中读到的,一个真正的输出迭代器满足更多的属性(例如,它必须提供一些 typedefs,这在这里是不相关的);然而,对于可重用性来说,这是一个合理的价格。 2
模板代码的作者通常需要明确概念。如果它们有一个简单的名称,它们可以用作模板参数:
template <typename FwdIter, typename RandIter>
FwdIter special_copy(RandIter beg, RandIter end, FwdIter output);
注意,在这个函数中,除了名字(这是对人类的提示,而不是对编译器的提示),没有任何约束要求成为迭代器。模板参数 FwdIter 将匹配任何东西,比如 double 或 void*,如果幸运的话,函数体将报告错误。可能会发生这样的情况:您传递了一个有效的类型,但它的行为并不像预期的那样。T33
另一方面,经典 C++ 确实提供了一个约束类型的工具:继承。您编写了接受 BASE*的代码片段,在运行时它们调用正确的虚函数。
静态接口是它们在 TMP 中的等价物。它们提供了比“扁平”类型 T 更少的通用性,但是具有相同级别的静态优化。
一个静态接口是一个骨架类,它将模板的有效范围限制在从接口派生的类型,同时它提供了“虚拟”回调机制的默认(静态)实现。
细节如下。
5.2.1.静态接口
最初的语言习惯被称为“奇怪地重复出现的模板”模式( CRTP ) ),它基于以下观察:static_cast 可以仅使用编译时信息遍历类层次结构。简单来说,static_cast 可以将 BASE转换为 DERIVED。如果 DERIVED 和 BASE 之间的继承关系不正确或不明确,强制转换将不会编译。但是,只有在运行时 BASE*指向一个真正的派生对象时,结果才有效。
作为一个特例,有一个简单的方法来确保演员会成功;也就是说,当每个派生类 从一个“个人基”继承时:
template <typename DERIVED_T>
class BASE
{
protected:
~BASE() {}
};
class DERIVED1 : public BASE<DERIVED1>
{
};
class DERIVED2 : public BASE<DERIVED2>
{
};
类型为 BASE
因此 BASE
template <typename DERIVED_T>
struct BASE
{
DERIVED_T& true_this()
{
return static_cast<DERIVED_T&>(*this);
}
const DERIVED_T& true_this () const
{
return static_cast<const DERIVED_T&>(*this);
}
double getSomeNumber() const
{
return true_this().getSomeNumber();
}
};
struct DERIVED_rand : public BASE<DERIVED_rand>
{
double getSomeNumber() const
{
return std::rand();
}
};
struct DERIVED_circle : public BASE<DERIVED_circle>
{
double radius_;
double getSomeNumber() const
{
return 3.14159265359 * sq(radius_);
}
};
就像虚函数一样,通过派生类接口的普通调用是廉价的:
DERIVED_rand d;
d.getSomeNumber(); // normal call; BASE is completely ignored
但是,您可以编写一个函数模板,它采用一个对基的引用 ,并对派生的成员函数进行一次廉价的调用。true _ 这不会产生任何开销。
template <typename T>
void PrintSomeNumber(BASE<T>& b) // crucial: pass argument by reference
{
// here BASE methods will dispatch to the correct T equivalent
std::cout << b.getSomeNumber();
}
DERIVED_circle C;
DERIVED_rand R;
...
PrintSomeNumber(C); // prints the area of the circle
PrintSomeNumber(R); // prints a random number
从概念上讲,前面的函数与这里更简单(但更模糊)的函数相同:
template <typename T>
void PrintSomeNumber(T& b)
{
std::cout << b.getSomeNumber();
}
然而,替换看起来是可以接受的,因为 PrintSomeNumber 是一个命名函数,而不是一个操作符(想想用两个 T 类型的参数写一个全局操作符+)。下面的示例演示如何使用带有运算符的静态接口。 4 它将只实现 operator+=并免费拥有 operator+,简单地从 summatable<中导出...>界面 。
template <typename T>
struct summable
{
T& true_this()
{
return static_cast<T&>(*this);
}
const T& true_this () const
{
return static_cast<const T&>(*this);
}
T operator+ (const T& that) const
{
T result(true_this());
result += that; // call dispatch to native T::operator+=
return result;
}
};
struct complex_number : public summable<complex_number>
{
complex_number& operator+= (const complex_number& that)
{
...
}
};
complex_number a;
complex_number b;
...
complex_number s = a+b;
(看起来很简单)最后一行执行下面的编译时步骤 :
- a 没有自己的运算符+,所以将 a 强制转换为它的基,即 const summable <complex_number>&。</complex_number>
- const sumtable<complex_number>&可以求和为一个复数,所以 b 照原样就可以了。</complex_number>
- summable <complex_number>::operator+构建一个名为 result 的复数,它是 true_this 的副本,因为 true_this 是一个复数。</complex_number>
- 将执行分派给 complex_number::operator+=,计算并返回结果。
请注意,您可以将基类 重写为:
template <typename T>
struct summable
{
// ...
T operator+ (const summable<T>& that) const
{
T result(true_this());
result += that.true_this();
return result;
}
};
让我们称接口为基类,称专门化为派生类。
5.2.2.常见错误
您刚刚遇到了一种情况,接口类制作了自己的专用副本:
T result(true_this());
这不是问题,因为接口是静态的,它知道自己的“真实类型”。
但是,true_this 的正确行为可以被切片破坏:
template <typename DERIVED_T>
void PrintSomeNumber(BASE<DERIVED_T> b)// argument by value
{
std::cout << b.getSomeNumber(); // error: slicing
// b is not a DERIVED_T any more
}
通常,有必要将基析构函数声明为非虚拟的和受保护的,有时将保护扩展到复制构造函数是个好主意。算法不需要复制静态接口。如果他们需要克隆对象,正确的习惯用法是调用 DERIVED_T 构造函数并传递 true_this(),如前所示。
template <typename DERIVED_T>
struct BASE
{
DERIVED_T& true_this()
{
return static_cast<DERIVED_T&>(*this);
}
const DERIVED_T& true_this() const
{
return static_cast<const DERIVED_T&>(*this);
}
protected:
~BASE()
{
}
BASE(const BASE&)
{
}
};
派生的接口仅在基成员函数体内可见:
template <typename DERIVED_T>
struct BASE
{
// ...
typedef DERIVED_T::someType someType; // compiler error
void f()
{
typedef DERIVED_T::someType someType; // ok here
}
};
class DERIVED : public BASE<DERIVED>
{
....
派生的 Typedefs 和 enums 在 BASE 中的类级别不可用。这是显而易见的,因为 DERIVED 是在其 base 之后编译的,也就是 BASE
让 BASE 为 DERIVED_T 公开一个 typedef 是一个好主意(不是错误),这允许外部函数制作一个专门的 BASE 副本。
template <typename DERIVED_T>
struct BASE
{
typedef DERIVED_T static_type;
但是,如果没有完全限定,DERIVED 不能访问基成员,因为模板基类超出了派生对象的范围。 5
template <typename DERIVED_T>
struct BASE
{
typedef double value_type;
value_type f() const
{
return true_this().f();
}
// ...
};
struct DERIVED1 : public BASE<DERIVED1>
{
value_type f() const // error: value_type is undefined
{
true_this(); // error: true_this is undefined
return 0;
}
};
struct DERIVED2 : public BASE<DERIVED2>
{
BASE<DERIVED2>::value_type f() const // ok
{
this->true_this(); // ok
return 0;
}
};
再次注意,范围限制只在类的“内部”有效。外部用户将正确看到 DERIVED1::value_type:
template <typename T>
struct value_type_of
{
typedef typename T::value_type type;
};
value_type_of<DERIVED1>::type Pi = 3.14; // ok, Pi has type double
最后,开发人员必须确保所有派生类都向基类正确地声明了它们的名称,以避免经典的复制和粘贴错误:
class DERIVED1 : public BASE<DERIVED1>
{
};
class DERIVED2 : public BASE<DERIVED1>
{
};
|
利益
|
问题
|
| --- | --- |
| 编写采用“不太普通的对象”的算法,并通过静态已知的接口使用它们。 | 开发人员必须确保所有算法都通过引用获取参数,并避免其他常见错误。 |
| 仅实现派生(专用)类中的部分代码,并移动基类中的所有公共代码。 | 实验测量表明,非虚拟受保护析构函数和多重继承的存在可能会抑制或降低代码优化。 |
5.2.3.静态接口实现
以前的许多观念可以归入 一类:
template <typename T>
struct clone_of
{
typedef const T& type;
};
template <typename static_type, typename aux_t = void>
class static_interface
{
public:
typedef static_type type;
typename clone_of<static_type>::type clone() const
{
return true_this();
}
protected:
static_interface() {}
~static_interface() {}
static_type& true_this()
{
return static_cast<static_type&>(*this);
}
const static_type& true_this() const
{
return static_cast<const static_type&>(*this);
}
};
在这一章的后面,你将回到额外的模板参数。
助手元函数 clone_of 可以定制,返回常量引用是一个合理的默认选择。对于小对象,返回副本可能会更快:
template <typename T, bool SMALL_OBJECT = (sizeof(T)<sizeof(void*))>
struct clone_of;
template <typename T>
struct clone_of<T, true>
{
typedef T type;
};
template <typename T>
struct clone_of<T, false>
{
typedef const T& type;
};
首先,您可以使用一些宏来简化接口声明 。
接口的定义如下
#define MXT_INTERFACE(NAME) \
\
template <typename static_type> \
class NAME : public static_interface<static_type>
#define MXT_SPECIALIZED this->true_this()
这里有一个实际的例子。接口宏 类似于普通的类声明。 6
MXT_INTERFACE(random)
{
protected:
~random()
{
}
public:
typedef double random_type;
random_type max() const
{
return MXT_SPECIALIZED.max();
}
random_type operator()() const
{
return MXT_SPECIALIZED(); // note operator call
}
};
- random 只能通过显式限定来访问 true_this()(就像 MXT_SPECIALIZED 那样)。
- random 需要声明一个受保护的析构函数。
- static_type 在 random 中是一个有效的类型名,即使 static_interface 超出了范围,因为它是模板参数名。
现在让我们实现一些随机算法 :
#define MXT_SPECIALIZATION(S, I) class S : public I< S >
MXT_SPECIALIZATION(gaussian, random)
{
public:
double max() const
{
return std::numeric_limits<double>::max();
}
double operator()() const
{
// ...
}
};
MXT_SPECIALIZATION(uniform, random)
{
public:
double max() const
{
return 1.0;
}
// ...
};
如果你需要一个模板 静态接口,比如:
template <typename RANDOM_T, typename SCALAR_T>
class random
{
public:
typedef SCALAR_T random_type;
// ...
};
template <typename T>
class gaussian : public random<gaussian<T>, T>
{
// ...
};
很容易为模板静态接口提供更多的宏(带有少量参数)。一个天真的想法是:
#define MXT_TEMPLATE_INTERFACE(NAME,T) \
\
template <typename static_type, typename T> \
class NAME : public static_interface<static_type>
#define MXT_TEMPLATE_SPECIALIZATION(S,I,T) \
\
template <typename T> \
class S : public I< S<T> >
它是这样使用的:
MXT_TEMPLATE_INTERFACE(pseudo_array, value_t)
{
protected:
~pseudo_array()
{
}
public:
typedef value_t value_type;
value_type operator[](const size_t i) const
{
return MXT_SPECIALIZED.read(i, instance_of<value_type>());
}
size_t size() const
{
return MXT_SPECIALIZED.size(instance_of<value_type>());
}
};
非模板类可以使用模板静态接口。例如,您可以拥有一个行为类似于位数组、半字节数组或字节数组的 bitstring 类 :
typedef bool bit;
typedef char nibble;
typedef unsigned char byte;
class bitstring
: public pseudo_array<bitstring, bit>
, public pseudo_array<bitstring, nibble>
, public pseudo_array<bitstring, byte>
{
...
一个接口不需要像真正的专门化那样尊重相同的成员名。在这种情况下,operator[]将执行分派给函数模板 read。这是有意义的,因为底层的 bitstring 可以以多种方式读取位置 I 处的元素(有三个不同的第 I 个元素)。但是在 pseudo_array 内部,要检索的类型是静态已知的,所以使用一个 bitstring 作为 pseudo_array 相当于“切片”了 bitstring 接口。这使得代码更加简单。
您需要解决的第一个问题是,当宏展开时,编译器会读取:
template <typename static_type, typename value_t>
class pseudo_array : public static_interface<static_type>
因此,bitstring 从 static_interface
图 5-1。不明确的继承图
为了避免这个问题,在静态接口中使用一个额外的参数来消除歧义。最明确的类型名不是 T 就是整个接口(pseudo_array
#define MXT_TEMPLATE_INTERFACE(NAME,T) \
\
template <typename static_type, typename T> \
class NAME \
: public static_interface<static_type, NAME<static_type, T> >
#define MXT_TEMPLATE_SPECIALIZATION(S,I,T) \
\
template <typename T> \
class S : public I< S<T>, T >
图 5-2。改进的继承图
5.2.4.会员空间问题
到目前为止,静态接口被描述为限制某些模板参数范围的技术。所以不用 F(T),你写 F(random
静态接口的第二个应用是成员空间问题。7member space 这个名字相当于一个名称空间,相对于一个类的成员函数。换句话说,它是一个子空间,一个类可以在其中放置重名的成员函数。
假设 C 是一个遵循 STL 约定的容器,那么 C 的第一个元素是begin(),最后一个是rbegin()。
这是划分接口的经典解决方案,其中函数名具有唯一的前缀/后缀,例如 push+front、push+back、r+begin 等等。
最好有一个真正的分区,前后都是容器,有自己的接口: 8
C MyList;
// ...
first = MyList.front.begin();
last = MyList.back.begin();
MyList.front.push(3.14);
MyList.back.push(6.28);
MyList.back.pop();
的确,你可以使用静态接口来编写代码,比如: 9
class bitstring
: public pseudo_array<bitstring, bit>
, public pseudo_array<bitstring, nibble>
, public pseudo_array<bitstring, byte>
{
char* data_;
size_t nbits_;
public:
pseudo_array<bitstring, bit>& as_bit() { return *this; }
pseudo_array<bitstring, nibble>& as_nibble() { return *this; }
pseudo_array<bitstring, byte>& as_byte() { return *this; }
size_t size(instance_of<byte>) const { return nbits_ / CHAR_BIT; }
size_t size(instance_of<bit>) const { return nbits_; }
size_t size(instance_of<nibble>) const { return nbits_ / (CHAR_BIT / 2); }
bit read(size_t n, instance_of<byte>) const { return ...; }
nibble read(size_t n, instance_of<bit>) const { return ...; }
byte read(size_t n, instance_of<nibble>) const { return ...; }
};
bitstring b;
int n1 = b.as_bit().size();
int n2 = b.as_byte().size();
与之相比:
bitstring b;
int n1 = b.size(instance_of<bit_tag>());
b.as_bit()本身也是一种容器,它可以通过引用算法来传递:
template <typename T, typename X>
X parity(pseudo_array<T, X>& data)
{
X result = 0;
for (size_t i=0; i<data.size(); ++i)
result ^= data[i];
return result;
}
这种技术非常好,但是它有一个局限性。如上所述,专门化中提供的 typedef 在静态接口中不可用,因此您无法声明返回迭代器的成员函数。这是因为静态接口必须从专门化中借用迭代器类型。
MXT_INTERFACE(front)
{
typename static_type::iterator begin() // <-- error here
{
return MXT_SPECIALIZED.begin();
}
typename static_type::iterator end() // <-- error again
{
return MXT_SPECIALIZED.end();
}
};
MXT_INTERFACE(back)
{
typename static_type::reverse_iterator begin() // <-- another error
{
return MXT_SPECIALIZED.rbegin();
}
typename static_type::reverse_iterator end() // <-- lots of errors
{
return MXT_SPECIALIZED.rend();
}
};
class C : public front<C>, public back<C>
{
// ...
public:
front<C>& front()
{ return *this; }
back<C>& back()
{ return *this; }
};
C MyList;
MyList.front().begin(); // error
MyList.back().begin(); // error
// ...
注意,这不是语法问题。由于 C 还不完整,C::iterator 还不存在。然而,有一些设计修复:
- 在 C 之前定义迭代器:
class C_iterator
{
// ...
};
class C
{
// container implementation
typedef C_iterator iterator;
};
- 在 C 和接口之间插入一个附加层,以便静态接口在 C 之后(包装类之前)编译:
class C
{
// container implementation
class iterator { ... };
};
MXT_TEMPLATE_INTERFACE(front, impl_t)
{
typename impl_t::iterator begin()
{
return MXT_SPECIALIZED.begin();
}
typename impl_t::iterator end()
{
return MXT_SPECIALIZED.end();
}
};
// ...
class C_WRAPPER : public front<C_WRAPPER, C>, public back<C_WRAPPER, C>
{
C c_;
public:
// reproduce C's interface
// dispatch all execution to c_
typename C::iterator begin()
{
return c_.begin();
}
// ....
};
5.2.5.成员选择
用于合并特征的相同技术(参见 4.2.4 节)可以成功地应用于值对象。下一个故意不完整的列表暗示了一个可能的动机:
enum
{
empty = 0,
year = 1,
month = 2,
day = 4,
// ...
};
template <unsigned CODE> struct time_val;
template <> struct time_val<empty> { }; // empty, I really mean it
template <> struct time_val<year> { int year; };
template <> struct time_val<month> { short month; };
// ...
template <unsigned CODE>
struct time_val
: public time_val<CODE & static_highest_bit<CODE>::value>
, public time_val<CODE – static_highest_bit<CODE>::value>
{
};
// an algorithm
template <unsigned CODE>
time_val<(year | month | day)> easter(const time_val<CODE>& t)
{
time_val<(year | month | day)> result;
result.year = t.year;
result.month = compute_easter_month(t.year);
result.day = compute_easter_day(t.year);
return result;
}
time_val<year | month> tv1;
time_val<month | day> tv2;
easter(tv1); // ok.
easter(tv2); // error: tv2.year is undefined.
请注意,该算法无条件地运行,就好像任何 time_val 都有成员年份一样。必要时,您可以使用包装器隔离这种假设:
template <unsigned CODE>
time_val<year | month | day> easter(const time_val<CODE>& t, selector<true>)
{
// implementation
}
template <int CODE>
time_val<year | month | day> easter(const time_val<CODE>& t, selector<false>)
{
// put whatever here: throw exception, static assert...
}
template <int CODE>
time_val<year | month | day> easter(const time_val<CODE>& t)
{
return easter(t, selector<CODE & year>());
}
5.3.类型隐藏
经典的 C++ 程序将对象的实例转换成可能具有不同类型的其他实例(通过函数调用)。
int i = 314;
double x = f(i); // transform an instance of int into an instance of double
使用模板,C++ 可以操作实例、编译时常数和类型(常数在中间,因为它们与两者共享一些属性)。您可以将类型和常量转换为实例(一般而言),将类型转换为类型(通过 traits 和元函数),将类型转换为常量(通过元函数和其他运算符,如 sizeof),将实例转换为常量(通过 sizeof),将类型转换为一些特殊的系统对象(使用 typeid)。然而,经典 C++ 只有非常有限的语言工具来将实例转换成类型。10
最常见的例子来自迭代器处理:
T t = *begin; // store a copy of the first element
// who is T?
目前,元函数提供了一个合适的类型:
typename std::iterator_traits<iterator_t>::value_type t = *begin;
最简单的方法是将*begin 作为一个未使用的伪参数传递给一个模板函数,该函数将推断出它的类型:
template <typename iterator_t>
void f(iterator_t beg, iterator_t end)
{
if (beg == end)
return;
f_helper(beg, end, *beg);
}
template <typename iterator_t, typename value_t>
void f_helper(iterator_t beg, iterator_t end, const value_t& )
{
// for most iterators,
// value_t ~ iterator_traits<iterator_t>::value_type
// however if *beg returns a proxy, value_t is the type of the proxy
// so this may not work with std::vector<bool> and in general,
// where value_t just stores a reference to the value.
}
在经典 C++ 中,有两种方法可以在不知道对象类型的情况下存储对象:
- 将它传递给一个模板函数,如前所示。但是,对象的寿命是有限的。
- 取消它的接口,可能通过模板和虚函数的组合。在最简单的情况下,对象可以是仅仅被存储的而不是别的: 11
class wrapper_base
{
public:
virtual ~wrapper_base() {}
virtual wrapper_base* clone() const = 0;
};
template <typename T>
class wrapper : public wrapper_base
{
T obj_;
public:
wrapper(const T& x)
: obj_(x) {}
wrapper<T>* clone() const
{
return new wrapper<T>(obj_);
}
};
template <typename T>
wrapper_base* make_clone(const T& x)
{
return new wrapper<T>(x);
}
有时需要为几种类型提供一个公共接口。最著名的例子是由variantobjects(也称为discribed union**)给出的,它们是静态类型固定,但内部存储可以传输不同类型的类。
本节的其余部分将详细讨论命令行解析的问题。假设您正在编写一个从命令行获取选项的工具。每个选项都有一个名称和一个固定类型的关联值。选项第一,其他都是论证:
tool.exe –i=7 –f=3.14 –d=6.28 –b=true ARGUMENT1 ARGUMENT2 ... ARGUMENTn
其中 I 是 int,f 是 float,依此类推。
理想情况下,您需要一种映射
为了更加简单,假设您从一个字符串数组开始,其中每个字符串或者是[前缀][名称]或者是[前缀][名称]=[值], 12 ,并且每个参数值将通过流提取(运算符> >)获得。
您可以生产两个容器。第一个名为 option_map ,存储名称-值对,比如 std::map,但是每个值都有一个任意的类型。名为 option_parser 的第二个容器是另一个映射,它在解析命令行之前知道所需的配对名称类型(例如,“f”是一个浮点数)。目标是编写如下代码:
int main(int argc, char* argv[])
{
option_parser PARSER;
PARSER.declare_as<float>("f"); // we tell the parser what it should
PARSER.declare_as<int>("i"); // expect, i.e. that "d" is a double,
PARSER.declare_as<double>("d");// etc. etc.
option_map<std::string> CL; // only key_type is a template parameter
try
{
const char* prefix = "–";
char** opt_begin = argv+1;
char** opt_end = argv+argc;
// finally we ask the parser to fill a map with the actual values
// this may throw an exception...
char** arg_begin = PARSER.parse(CL, opt_begin, opt_end, prefix);
double d;
if (!CL.get(d, "d"))
{
// the user did not specify a value for "d"
d = SOME_DEFAULT_VALUE;
}
}
catch (std::invalid_argument& ex)
{
// ...
}
}
5.3.1.蹦床
这种“多态性”的核心技术是使用蹦床 。
形式上,trampoline 是一个函数模板中的局部类,但是“局部”的含义不应该从字面上理解。
该类只有静态成员函数。它的公共接口接受固定类型的参数(比如 void*),但是嵌套在模板中,trampoline 的主体知道“外部”模板参数,并使用它来执行安全的静态强制转换。
下面是一个简单的例子——一个包含无类型指针的裸结构和一个知道对象静态类型并明显丢失信息的函数模板。
struct generic_t
{
void* obj;
void (*del)(void*);
};
template <typename T> // outer template parameter
generic_t copy_to_generic(const T& value)
{
struct local_cast // local class
{
static void destroy(void* p) // void*-based interface
{
delete static_cast<T*>(p); // static type knowledge
}
};
generic_t p;
p.obj = new T(value); // information loss: copy T* to void*
p.del = &local_cast::destroy;
return p;
}
实际上,只有 p.obj 不知道如何销毁它附加的对象,但是 p.del 指向(在伪代码中)copy _ to _ generic
p.del(p.obj); // it works!
del 在功能上相当于虚拟析构函数。蹦床和虚拟功能表之间的类比是正确的,但是:
- 蹦床技术允许你处理对象,而不是指针(一个经典的工厂会返回指向基的指针,而 copy_to_generic 产生一个对象)。
- 蹦床指针可以在运行时测试和修改。例如,如果指针的所有权被转移,del 可以在任何时候被一个什么都不做的函数替换。
- 蹦床比抽象的类层次结构更不清晰(也就是说,更难维护)。
像 generic_t 这样的结构的优点是,它们的类型是静态已知的,所以它们可以用在标准容器中,它们是类,所以它们可以管理自己的资源和不变量。
不幸的是,虽然类型 T 在内部被称为,但它不能被公开。像 del 这样的函数指针在它们的签名中不能有 T。trampoline 类的接口必须独立于 T,并且不能有模板成员函数(例如,不能有一个 trampoline 成员接受一个仿函数并将其应用于指针对象)。
*接下来,您需要另一个工具——STD::type _ info 的包装器。
5.3.2.Typeinfo 包装器
typeid 运算符是一个不太为人所知的 C++ 运算符,它在运行时确定表达式的类型,并返回对 std::type_info 类型的系统对象的常量引用。
type_info::before 是一个成员函数,可用于模拟类型的总体(但未指定)排序。
已经提出了几个包装器来给出 std::type_info 值语义。这段代码类似于[1]中的优雅实现,但是比较运算符确保默认构造的(null) typeinfo 小于任何其他实例。 十三
class typeinfo
{
const std::type_info* p_;
public:
typeinfo()
: p_(0)
{}
typeinfo(const std::type_info& t)
: p_(&t)
{}
inline const char* name() const
{
return p_ ? p_->name() : "";
}
inline bool operator<(const typeinfo& that) const
{
return (p_ != that.p_) &&
(!p_ || (that.p_ && static_cast<bool>(p_->before(*that.p_))));
}
inline bool operator==(const typeinfo& that) const
{
return (p_ == that.p_) ||
(p_ && that.p_ && static_cast<bool>(*p_ == *that.p_));
}
};
5.3.3.选项 _ 地图
回想一下,option_map 是在 5.3 节中引入的,作为一个容器来存储从命令行解析的值及其类型。option_map 的界面确实非常简单。
template <typename userkey_t>
class option_map
{
public:
// typed find:
// MAP.find<T>("name") returns true
// if "name" corresponds to an object of type T
template <typename T>
bool find(const userkey_t& name) const;
// typeless find:
// MAP.scan("name") returns true if "name" corresponds to any object
bool scan(const userkey_t& name) const;
// checked extraction:
// MAP.get(x, "name") returns true
// if "name" corresponds to an object of type T;
// in this case, x is assigned a copy of such object;
// otherwise, x is not changed
template <typename T>
bool get(T& dest, const userkey_t& name) const;
// unchecked extraction:
// MAP.get<T>("name") returns either the object of type T
// corresponding to "name", or T().
template <typename T>
T get(const userkey_t& name) const;
// insertion
// MAP.put("name", x) inserts a copy of x into the map
template <typename T>
bool put(const userkey_t& name, const T& value);
size_t size() const;
~option_map();
};
现在来看实现细节 generic _ t 的思想得到了进一步的发展,赋予了它复制和销毁的能力:
template <typename userkey_t>
class option_map
{
struct generic_t
{
void* obj;
void (*copy)(void* , const void*);
void (*del)(void*);
};
因为您希望通过名称和对(名称、类型)来搜索容器,所以您应该使用 typeinfo 包装类选择后一种结构作为键。
typedef std::pair<userkey_t, typeinfo> key_t;
typedef std::map<key_t, generic_t> map_t;
typedef typename map_t::iterator iterator_t;
map_t map_;
插入例程几乎与原型示例相同:
template <typename T>
bool put(const userkey_t& name, const T& value)
{
struct local_cast
{
static void copy(void* dest, const void* src)
{
*static_cast<T*>(dest) = *static_cast<const T*>(src);
}
static void destroy(void* p)
{
delete static_cast<T*>(p);
}
};
generic_t& p = map_[key_t(name, typeid(T))];
p.obj = new T(value);
p.copy = &local_cast::copy;
p.del = &local_cast::destroy;
return true;
}
std::map 顶部的一些函数是免费的:
size_t size() const
{
return map_.size();
}
以下是键入的查找结果:
template <typename T>
bool find(const userkey_t& name) const
{
return map_.find(key_t(name, typeid(T))) != map_.end();
}
要从 option_map 中检索数据,可以使用 copy 函数。首先,你做一个输入查找。如果成功并且对象为非空,则对用户提供的引用执行复制:
template <typename T>
bool get(T& dest, const userkey_t& name) const
{
const typename map_t::const_iterator i = map_.find(key_t(name, typeid(T)));
const bool test = (i != map_.end());
if (test && i->second.obj)
i->second.copy(&dest, i->second.obj);
return test;
}
未检查的检索是为了方便而实现的快捷方式:
template <typename T>
T get(const userkey_t& name) const
{
initialized_value<T> v;
get(v.result, name);
return v.result;
}
此时,您只需让析构函数清除所有对象。 14
~option_map()
{
iterator_t i = map_.begin();
while (i != map_.end())
{
generic_t& p = (i++)->second;
if (p.del)
p.del(p.obj);
}
}
最后,您可以利用 typeinfo 的排序属性进行无类型查找。由于对的排序方式,映射按名称排序,同名的条目按 typeinfo 排序。首先,搜索(name,typeinfo())的上界。任何其他同名的对都会更大,因为 typeinfo()是最不可能的值。因此,如果上限存在,并且与您要查找的名称相同,它将返回 true。
bool scan(const userkey_t& name) const
{
const typename map_t::const_iterator i
= map_.upper_bound(key_t(name, typeinfo()));
return i != map_.end() && i->first.first == name;
}
注意,容器可以保存更多不同类型的同名对象。
5.3.4.选项 _ 解析器
没有完整描述 option_parser,因为它没有为构建 option_map 中使用的概念添加任何内容。但是,请注意,蹦床可能有类型不是 void*的参数。我们把一些细节留给练习。
class option_parser
{
typedef option_map<std::string> option_map_t;
typedef bool (*store_t)(option_map_t&, const char*, const char*);
typedef std::map<std::string, store_t> map_t;
map_t map_;
public:
template <typename T>
void declare_as(const char* const name)
{
struct local_store
{
static bool store(option_map_t& m,
const char* name, const char* value)
{
std::istringstream is(value);
T temp;
return (is >> temp) && m.put(name, temp);
}
};
map_[name] = &local_store::store;
}
请注意,local_store::store 不接受 void*参数。蹦床唯一的要求就是发布一个独立于 t 的接口。
template <typename iterator_t>
iterator_t parse(option_map_t& m, iterator_t begin, iterator_t end)
{
for every iterator i=begin...end
{
get the string S = *i;
if S has no prefix
stop and return i;
else
remove the prefix
if S has the form "N=V"
split S in N and V
else
set N = S
set V = <empty string>
if N is not contained in map_
throw exception "unknown option"
else
set F := local_store::store
execute F(m, N, V)
if it fails, throw exception "illegal value"
}
}
5.3.5.最终添加
由于 declare_as 的工作方式,可以从字符串流中提取的每种类型在命令行解析器中都是可以接受的。
要包含无参数选项,只需添加一个空类:
struct option
{
};
inline std::istream& operator>>(std::istream& is, option&)
{
return is;
}
这将启用命令行开关,例如:
tool.exe –verbose
如果名称是唯一的,检索开关值的最简单方法是使用无类型查找。如果省略开关,这将产生 false。
PARSER.declare_as<option>("verbose");
char** arg_begin = PARSER.parse(CL, opt_begin, opt_end, prefix);
if (CL.scan("verbose"))
{
// ...
}
蹦床技术可以很容易地优化空间。不是为每个“虚函数 ”创建一个指针,而是可以在一个结构的静态实例中将类型 T 的函数分组,因此只有一个指针,就像虚函数表的传统实现一样。
这种方法也是可扩展的。如果您需要向接口添加额外的“功能”,它需要更少的修改,几乎不需要额外的内存(因为您只有一个指针表,而不是每个实例有许多指针)。
struct virtual_function_table
{
void (*copy)(void* , void*);
void (*del)(void*);
void* (*clone)(const void*);
};
struct generic_t
{
void* obj;
const virtual_function_table* table; // single pointer-to-const
};
// identical implementation, but not a local class any more...
template <typename T>
struct local_cast
{
static void copy(void* dest, void* src)
{
*static_cast<T*>(dest) = *static_cast<T*>(src);
}
static void destroy(void* p)
{
delete static_cast<T*>(p);
}
static void* clone(const void* p)
{
return new T(*static_cast<const T*>(p));
}
};
template <typename T>
bool put(const userkey_t& name, const T& value)
{
static const virtual_function_table pt =
{
&local_cast<T>::copy,
&local_cast<T>::destroy,
&local_cast<T>::clone
};
generic_t& p = map_[key_t(name, typeid(T))];
p.obj = new T(value);
p.table = &pt;
return true;
}
当然,你应该写 p.table->del 而不是 p.del,并支付一个额外的 indirection。
最后,通过三个规则使 generic_t 成为真值:实现复制构造函数、赋值和析构函数。
struct generic_t
{
void* obj;
const virtual_function_table* table;
generic_t()
: obj(0), table(0)
{
}
generic_t(const generic_t& that)
: table(that.table)
{
if (table)
obj = table.clone(that.obj);
}
generic_t& operator=(const generic_t& that)
{
generic_t temp(that);
swap(obj, temp.obj);
swap(table, temp.table);
return *this;
}
~generic_t()
{
if (table && obj)
(table->del)(obj);
}
};
5.3.6.蹦床穿越边界
本节简要总结了最后几段。当 trampoline 函数包含足够的信息来恢复原始类型时,它可以作为 void 指针的伴侣:
void* myptr_;
void (*del_)(void*);
template <typename T>
struct secret_class
{
static void destroy(void* p)
{
delete static_cast<T*>(p);
}
};
myptr_ = [[a pointer to T]];
del_ = &secret_class<T>::destroy;
关于 T 的信息不能返回给调用者,因为 T 不能出现在 trampoline 接口中。
因此,您通常会处理要求调用者指定类型 T 的问题,而 trampoline 只是确保它与原始类型相同(例如,调用 typeid,参见“类型化查找”)。这被非正式地称为精确铸造 。
简而言之,如果类型不是程序所期望的类型,精确强制转换将会失败:
template <typename T>
T* exact_cast() const
{
return &secret_class<T>::destroy == del_ ?
static_cast<T*>(myptr_) : 0;
}
第二种可能是抛出异常:
template <typename T>
struct secret_class
{
static void throw_T_star(void* p)
{
throw static_cast<T*>(p);
}
};
struct myobj
{
void* myptr_;
void (*throw_)(void*);
template <typename T>
myoby(T* p)
{
myptr_ = p;
throw_ = &secret_class<T>::throw_T_star;
}
template <typename T>
T* cast_via_exception() const
{
try
{
(*throw_)(myptr_);
}
catch (T* p) // yes, it was indeed a T*
{
return p;
}
catch (...) // no, it was something else
{
return 0;
}
}
};
这种方法要慢几个数量级(试...catch block 可能不便宜),但它增加了一个有趣的新功能。不仅可以强制转换为原始类型 T,还可以强制转换为 T 的任何基类,当 trampoline 函数抛出 DERIVED时,异常处理程序将成功捕获 BASE。
记住,直接动态铸造一个 void是不可能的,所以这实际上是你能做的最好的事情。如果效率是一个问题,实际上您可能想要采用一种方案,使用 trampolines 执行到 BASE的精确转换,并在稍后(在 trampoline 代码之后)对结果执行动态转换。
还要注意,根据精确的应用程序语义,您有时可以将“目的地”类型的数量限制在一个小范围内,并在 trampoline 中对它们进行硬编码:
struct virtual_function_table
{
bool (*safe_to_double)(void*, double&);
std::string (*to_string)(void*);
};
template <typename T1, typename T2>
struct multi_cast
{
static T2 cast(void* src)
{
return has_conversion<T1,T2>::L2R ?
T2(*static_cast<T1*>(src)) : T2();
}
static bool safe_cast(void* src, T2& dest)
{
if (has_conversion<T1,T2>::L2R)
dest = *static_cast<T1*>(src);
return has_conversion<T1,T2>::L2R;
}
};
to_double = &multi_cast<T, double>::safe_cast;
to_string = &multi_cast<T, std::string>::cast;
5.4.不同的
类型隐藏技术的关键点是决定谁记得对象的正确类型。在这个例子中,option_map 的客户机通过调用 option _ map::get
在某些情况下,客户端需要或者更喜欢忽略类型,盲目地委托“不透明”对象。这样,无论存储的对象是什么,它都会执行正确的操作。
5.4.1.使用虚拟调用删除参数
如果只需要传输任意类型对象的副本,可以将其包装在自定义的类模板中,从而将模板参数“隐藏”在非模板抽象基类后面。
以下粗略的代码片段将有助于阐明这一想法:
struct wrapper_base
{
virtual ~wrapper_base()
{
}
virtual wrapper_base* clone() const = 0;
// add more virtual functions if needed
virtual size_t size() const = 0;
};
template <typename T>
struct wrapper : wrapper_base
{
T obj_;
wrapper(const T& that)
: obj_(that)
{
}
virtual wrapper_base* clone() const
{
return new wrapper<T>(obj_);
}
// implement virtual functions delegating to obj_
virtual size_t size() const
{
return obj_.size();
}
};
class transporter
{
wrapper_base* myptr_;
public:
~transporter()
{
delete myptr_;
}
transporter(const transporter& that)
: myptr_(that.myptr_ ? that.myptr_->clone() : 0)
{
}
transporter()
: myptr_(0)
{
}
template <typename T>
transporter(const T& that)
: myptr_(new wrapper<T>(that))
{
}
// implement member functions delegating to wrapper_base
size_t size() const
{
return myptr_ ? myptr_->size() : 0;
}
};
您还可以添加自定义(朋友)动态转换:
template <typename T>
static T* transporter_cast(transporter& t)
{
if (wrapper<T>* p = dynamic_cast<wrapper<T>*>(t.myptr_))
return &(p->obj_);
else
return 0;
}
5.4.2.有访客的变体
不透明的接口经常使用访问者模式。访问者是一个未指定类型的仿函数,它被接口接受并被允许与真实对象通信,否则真实对象的类型是隐藏的。
换句话说,您需要一种通过非模板 trampoline 接口传递泛型仿函数的方法。
作为一个原型问题,您将编写一个概念类,它可以存储任何大小不超过固定限制的对象。 十五
template <size_t N>
class variant;
首先,您定义所需的蹦床。variant 将有一些固定大小的存储空间来放置对象:
template <size_t N>
class variant
{
char storage_[N];
const vtable* vt;
};
再次从规则三,暂定界面有三个功能:
struct vtable
{
void (*construct)(void*, const void*);
void (*destroy)(void*);
void (*assign)(void*, const void*);
};
template <typename T>
struct vtable_impl
{
static void construct(void* dest, const void* src)
{
new(dest) T(*static_cast<const T*>(src));
}
static void destroy(void* dest)
{
static_cast<T*>(dest)->~T();
}
static void assign(void* dest, const void* src)
{
*static_cast<T*>(dest) = *static_cast<const T*>(src);
}
};
template <>
struct vtable_impl<void>
{
static void construct(void* dest, const void* src)
{
}
static void destroy(void* dest)
{
}
static void assign(void* dest, const void* src)
{
}
};
template <typename T>
struct vtable_singleton
{
static const vtable* get()
{
static const vtable v =
{
&vtable_impl<T>::construct,
&vtable_impl<T>::destroy,
&vtable_impl<T>::assign
};
return &v;
}
};
template <size_t N>
class variant
{
char storage_[N];
const vtable* vt;
public:
~variant()
{
(vt->destroy)(storage_);
}
variant()
: vt(vtable_singleton<void>::get())
{
}
variant(const variant& that)
: vt(that.vt)
{
(vt->construct)(storage_, that.storage_);
}
template <typename T>
variant(const T& that)
: vt(vtable_singleton<T>::get())
{
MXT_ASSERT(sizeof(T)<=N);
(vt->construct)(storage_, &that);
}
};
构造器初始化“虚函数表指针”并调用原始内存上的构造。 16
赋值操作符依赖于一个微妙的问题:异常。如果构造函数抛出一个异常,由于对象从未被完全构造,它也不会被销毁,这正是你所需要的。但是,如果您需要用 ?? 的实例覆盖 ?? 的实例,您首先销毁 ??,但是构建 ?? 可能会失败。
因此,您需要将虚拟表指针重置为无操作版本,销毁 ??,构造 ??,然后最终存储正确的指针。
void rebuild(const void* src, const vtable* newvt)
{
const vtable* oldvt = vt;
vt = vtable_singleton<void>::get();
(oldvt->destroy)(storage_);
// if construct throws,
// then variant will be in a consistent (null) state
(newvt->construct)(storage_, src);
vt = newvt;
}
多亏了 rebuild,您可以复制另一个变体和任何其他 T 类型的对象:
variant& operator=(const variant& that)
{
if (vt == that.vt)
(vt->assign)(storage_, that.storage_);
else
rebuild(that.storage_, that.vt);
return *this;
}
template <typename T>
variant& operator=(const T& that)
{
MXT_ASSERT(sizeof(T)<=N);
if (vt == vtable_singleton<T>::get())
(vt->assign)(storage_, &that);
else
rebuild(&that, vtable_singleton<T>::get());
return *this;
}
};
这种变体仅仅是纯粹的存储,但请考虑以下附加内容:
class variant
{
// ...
template <typename visitor_t>
void accept_visitor(visitor_t& v)
{
// ???
}
};
由于蹦床需要有固定的非模板签名,这里的解决方案是虚拟继承。您为任何未指定的访问者定义了一个接口,为访问 T 类型的访问者定义了另一个接口。
虚拟继承是必要的,因为访问者可能想要访问不止一种类型。
class variant_visitor_base
{
public:
virtual ~variant_visitor_base()
{
}
};
template <typename T>
class variant_visitor : public virtual variant_visitor_base
{
public:
virtual void visit(T&) = 0;
virtual ~variant_visitor()
{
}
};
struct bad_visitor
{
};
struct vtable
{
// ...
void (*visit)(void*, variant_visitor_base*);
};
template <typename T>
struct vtable_impl
{
// ...
static void visit(void* dest, variant_visitor_base* vb)
{
if (variant_visitor<T>* v = dynamic_cast<variant_visitor<T>*>(vb))
v->visit(*static_cast<T*>(dest));
else
throw bad_visitor();
}
};
template <>
struct vtable_impl<void>
{
// ...
static void visit(void* dest, variant_visitor_base* vb)
{
}
};
template <size_t N>
class variant
{
public:
variant& accept_visitor(variant_visitor_base& v)
{
(vt->visit)(storage_, &v);
return *this;
}
最后,这里有一个具体的访问者(它将访问三种类型,因此虚拟基类很重要):
struct MyVisitor
: public variant_visitor<int>
, public variant_visitor<double>
, public variant_visitor<std::string>
{
virtual void visit(std::string& s)
{
std::cout << "visit: {s}" << s << std::endl;
}
virtual void visit(int& i)
{
std::cout << "visit: {i}" << i << std::endl;
}
virtual void visit(double& x)
{
std::cout << "visit: {d}" << x << std::endl;
}
};
variant<64> v1, v2, v3;
std::string s = "hello world!";
double x = 3.14;
int j = 628;
v1 = s;
v2 = x;
v3 = j;
MyVisitor mv;
v1.accept_visitor(mv);
v2.accept_visitor(mv);
v3.accept_visitor(mv);
visit: {s}hello world!
visit: {d}3.14
visit: {i}628
注意,为了完整起见,有界的有区别的联合,比如 boost::variant,采用了不同的方法。variant 是一个具有 N 个类型参数 ?? 的类模板...Tn。在任何时候,variant 只保存 Tj 的一个实例。构造函数可以将 T 类型的任何对象明确转换为一个 Tj,否则将失败。
5.5.包装容器
新的容器通常建立在传统的 STL 对象之上:
template <typename T, typename less_t = std::less<T> >
class sorted_vector
{
typedef std::vector<T> vector_t;
vector_t data_;
排序后的向量基本上相当于一组操作向量的函数,强制执行一些不变量(即保持排序)。因此,sorted_vector 是一种容器适配器 、,因为它代表实际的存储,并且它只改变数据存储的方式。
假设你已经有了一个向量,想把它当作一个排序向量。记住暴露类的内部是个坏主意(回想 1.4.4 节)。
template <typename T, typename less_t = std::less<T> >
class sorted_vector
{
public:
vector_t& base() // very bad
{ return data_; }
const vector_t& base() const // moderately bad
{ return data_; }
};
void causeDamage(sorted_vector<double>& s)
{
std:random_shuffle(s.base().begin(), s.base().end());
laugh(); // evil laugh here
}
相反,可以有一个额外的参数来定义存储,类似于 std::stack 的容器类型,类似于 std::vector 的分配器参数。
template <typename T, typename less_t = ..., typename vector_t = std::vector<T> >
class sorted_vector
{
vector_t data_;
public:
sorted_vector(vector_t data)
: data_(data)
{
}
};
void treatVectorAsSorted(vector<double>& v)
{
sorted_vector<double, less<double>, vector<double>&> sorted_v(v);
// ...
}
当您编写 sorted_vector 的代码时,您应该表现得好像 vector_t 是 std::vector,它的接口是明确定义的。任何替换类型都必须满足相同的合同。
无论如何,这个解决方案是最复杂的编码,因为你需要知道引用的函数。您应该显式地支持 vector_t 是对某个向量的引用的情况,这在决定通过值/通过引用获取参数时很可能会导致问题。这将是特征的一个好例子。
template <typename T>
struct s_v_storage_traits
{
typedef const T& argument_type;
typedef T value_type;
};
template <typename T>
struct s_v_storage_traits<T&>
{
typedef T& argument_type;
typedef T& value_type;
};
template <typename T, typename less_t = ..., typename vector_t = vector<T> >
class sorted_vector
{
typename s_v_storage_traits<vector_t>::value_type data_;
public:
sorted_vector(typename
s_v_storage_traits<vector_t>::argument_type data)
: data_(data)
{
}
};
隔离存储参数的强烈需求来自于序列化。现代操作系统可以轻松地将任意存储设备中的内存映射到程序地址空间,而无需任何成本。
换句话说,你可以从操作系统中得到一个指针(或者一个指向常量的指针),它看起来像普通的内存,但实际上是指向别的地方,例如,指向硬盘。
现在你可以创建一个直接指向映射内存的 sorted_vector,插入一个合适的类作为 vector_t: 17
template <typename T>
class read_only_memory_block
{
const T* data_;
size_t size_;
public:
// constructor, etc....
// now implement the same interface as a const vector
typedef const T* const_iterator;
const_iterator begin() const { return data_; }
const_iterator end() const { return data_ + size_; }
const T& operator[](size_t i) const { return data_[i]; }
// ...
};
请注意,您不需要 vector 的真正替代产品。如果你只是调用常量成员函数,接口的子集就足够了。
1概念的概念在 2.2.4 节中介绍。
2 黑洞迭代器是一个 hack,不是一个完美的输出迭代器。
3
4boost 库包含一些更通用的代码。参见www . boost . org/doc/libs/1 _ 57 _ 0/libs/utility/operators . htm
。
5 参见【2】第 135 页。
6 这种技术的缺点是,宏可能会混淆一些 ide,这些 ide 解析头文件来构建项目的图形表示。
7 此外,双端队列示例来自同一作者。
8 在后面的伪代码中,你要假装 C 是一个类;当然,非模板容器是一种不寻常的东西。
9 这段代码不编译,因为为了简洁,我们删除了所有 const 版本的成员函数。然而,修复应该很明显。
10 现代 C++ 提供了两个新的关键字:decltype 和 auto。前者返回任何表达式的确切类型,类似于 sizeof。后者允许一个实例“复制”它的初始化器的类型,所以 auto i = f()会声明一个具有最佳类型的变量 I 来本地存储 f()的结果。详见第十二章。
11 这个例子很重要,将在 5.4.1 节再次分析。
12 前缀是固定的字符序列,通常为“-”、“-”、或“/”。
13 实现使用短路来防止空指针取值,非常简洁。参见附录 b 中的一个练习。
14 实施明显有缺陷;无法安全地复制/分配选项映射。为了使代码尽可能简单,甚至更简单,这个主题的讨论推迟到 5.35 节。
15 这也被称为无界歧视联盟。该代码应被视为概念验证,而不是生产就绪。没有考虑两个大问题:一致性和对齐存储。我建议,作为一个快速而不明智的解决方案,您可以将 variant::storage_ 放在一个具有单成员 double 的伪结构的联合中。看到没。A. Alexandrescu 的《C++ 中歧视性联合的实现》。
16 我们去掉了用虚拟蹦床初始化成员的“如果指针为空”测试。
17 映射内存区通常是不可调整大小的。仅仅为了简单起见,我们假设它是 const,但是不需要这样。一个 vector 需要存储三段独立的数据(例如,“begin”、“size”和“capacity”);其他都可以推导出来)。read_write_memory_block 也需要这些成员,但是容量从一开始就是一个常数,等于“最大大小”。**
六、算法
算法的实现需要一个通用的 I/O 接口。您需要决定函数获取数据和写入结果的方式和位置,以及如何和保留哪些中间结果。迭代器是帮助解决这个问题的现有抽象。
一个迭代器 是一个小的数据类型,它提供了一个数据集的顺序视图。简单地说,它是一个实现指针可以执行的操作子集的类。
迭代器 的重要性在于它们将函数与实际的数据存储解耦。一个算法通过几个迭代器读取它的输入...end ),并经常将其输出写入另一个范围:
template <typename iterator_t>
... sort(iterator_t begin, iterator_t end);
template <typename iter1_t, typename iter2_t >
... copy(iter1_t input_begin, iter1_t input_end, iter2_t output_begin);
可以根据算法的 I/O 接口对它们进行粗略的分类。非变异算法迭代一个或多个只读范围。有两个子系列:
- “find”算法返回一个指向结果(比如 std::min_element)的迭代器,如果没有结果,则返回 end。
- “累加”算法返回一个任意值,该值不需要对应于范围内的任何元素。
选择性复制算法 取一个输入只读范围和一个输出范围,结果写入其中。输出范围被认为是可写的。如果输出范围可以存储任意数量的元素或者简单地存储与输入范围一样多的元素,那么只给出最左边的位置(比如 std::copy)。
- 通常每个算法都描述了如果输入和输出范围重叠会发生什么。“变换”算法接受与 begin 不相交或完全重合的输出范围...结束。
重新排序算法 对输入范围的元素进行混洗,并确保结果将在某个特殊的位置,或者等价地,结果将是某个特定的子范围(例如 std::nth_element 和 std::partition)。
- “收缩”算法(std::remove_if)在 begin 中重新排列数据...end,如果结果比输入序列短,则返回新的 end1,在 end1 范围内留下未指定的元素..结束。
根据迭代器编写算法可以提供显著的优势:
- 所有的容器,标准的和非标准的,都可以很容易地提供迭代器,所以这是一种解耦算法和数据存储的方法。
- 迭代器有一个默认的和方便的方式来表示“失败”,即返回 end。
- 根据算法的细节,忽略迭代器下实际的“指向类型”可能是可行的。
另一方面,有两个困难:
-
迭代器是数据集的视图。您经常需要修改一个给定的视图来匹配另一个算法所期望的。例如,您编写了一个函数,它获取一个 pair
序列作为输入,但是在内部您可能需要调用一个需要 x 序列的例程。 -
应该尽可能避免迭代器的确切类型。假设 v 是一个容器的 const-reference,比较下面两种遍历所有元素的方法(姑且非正式地说两个“循环”)。
for (vector<string>::const_iterator i = v.begin(); i != v.end(); ++i) { ... } std::for_each(v.begin(), v.end(), ...);
第一个“循环”不太通用,也更详细。它与您正在使用的容器(即 vector
第二个循环也有它的缺点。您必须传递一个函数对象作为“循环体”,这可能不太方便。
6.1.算法输入/输出
算法通常是通过通用范围执行输入/输出操作的函数。在这种情况下,一个范围由一对泛型类型 iterator_t 的迭代器表示,函数假设 iterator_t 支持所有需要的操作。然而,你会看到,这种假设不仅是一种方便的简化,而且通常是最好的,因为如果一个泛型类型 T 是迭代器,那么很难检测出。
这些假设是:
- *i 返回 STD::iterator _ traits
::reference,其行为类似于对底层对象的引用。 2 - 无论*i 返回什么,指向值的副本都可以存储为 STD::iterator _ traits
:::value _ type;通常,您会进一步强调这种类型是可分配的或可交换的。3 - 对 I 的任何基本操作(复制、解引用、递增等等)都是廉价的。
- 可以使用 STD::iterator _ traits
::iterator _ category 作为类型标签,为不同类型的迭代器分派专门的算法。 - 所有在 I 上有效的递增/递减操作符都返回一个可取消引用的对象(通常是 T 的另一个实例)。这使您可以安全地编写*(i++)。
有时你会隐含地假设同一个迭代器的两个副本是独立的。这通常会被 I/O 相关的迭代器所违背,比如读/写文件或内存的对象,像 std::back_insert_iterator,因为*i 在概念上为一个新对象分配空间;它不检索该范围的现有元素。
6.1.1.基于交换还是基于拷贝
作为基本假设的结果,算法中的大多数(如果不是全部)I/O 应该在没有显式声明类型的情况下编写。如果可能的话,应该尽量少用引用和值类型,通常是通过交换和直接取消引用和赋值。
例如,copy 解决了输出问题。它只是询问一个有效的迭代器,结果写在哪里:
template <typename iter1_t, typename iter2_t>
iter2_t copy(iter1_t begin, iter1_t end, iter2_t output)
{
while (begin != end)
*(output++) = *(begin++); // dereference-and-assign
return output;
}
在不知道元素是什么的情况下,您可以假设交换操作没有普通的赋值操作繁重。POD 交换执行三个赋值,所以稍微差一点,但是如果对象包含资源句柄(比如指向堆分配的内存的指针),交换通常被优化以避免临时对象的构造(这可能会失败或抛出)。如果 s1 是一个短字符串,s2 是一个很长的字符串,那么赋值 s1=s2 将需要大量的内存,而 swap(s1,s2)则不需要任何开销。
例如,std::remove_if 的实现可以用 smart_swap 覆盖不合适的元素。
move 是一个破坏性的复制过程,其中原始值保持一致,但调用者不知道。
template <typename iterator_t>
void move_iter(iterator_t dest, iterator_t source)
{
if (dest == source)
return;
if (is_class<std::iterator_traits<iterator_t>::value_type>::value)
smart_swap(*dest, *source);
else
*dest = *source;
}
template <typename iterator_t, typename func_t>
iterator_t remove_if(iterator_t begin, iterator_t end, func_t F)
{
iterator_t i = begin;
while (true)
{
while (i != end && F(*i))
++i;
if (i == end)
break;
move_iter(begin++, i++);
}
return begin;
}
该算法返回新的范围终点。它将使用赋值来“移动”一个基本类型,使用交换来“移动”一个类。由于决策规则是隐藏的, 4 因此,该算法将在新旧范围端点之间留下不可预测的对象:
struct less_than_3_digits
{
bool operator()(const std::string& x) const
{
return x.size()<3;
}
bool operator()(const int x) const
{
return x <= 99;
}
};
std::string A1[] = { "111", "2", "3", "4444", "555555", "66" };
int A2[] = { 111 , 2 , 3 , 4444 , 555555 , 66 };
remove_if(A1, A1+6, less_than_3_digits());
remove_if(A2, A2+6, less_than_3_digits());
执行这段代码后,数组 A1 和 A2 将会不同。尾随范围由未指定的对象填充,并且它们确实不同。
| [0] | One hundred and eleven | "111" |
| [1] | Four thousand four hundred and forty-four | "4444" |
| [2] | Five hundred and fifty-five thousand five hundred and fifty-five | "555555" |
| [3] | Four thousand four hundred and forty-four | "2" |
| [4] | Five hundred and fifty-five thousand five hundred and fifty-five | "3" |
| [5] | Sixty-six | "66" |
注意 C++0x 有一个移动语义的语言构造:R 值引用。
声明为对 T 的 R 值引用(写为 T&&)的函数参数将绑定到非常数临时对象。由于是临时的,该函数可以自由地从中窃取资源。特别是,您可以编写一个特殊的“移动构造函数”,从一个临时对象初始化一个新的实例。
此外,将引用转换为 R 值引用具有将现有对象标记为“可移动”的效果(这种转换封装在 STL 函数 std::move 中)。
结合这些功能,三拷贝交换可以重写为:
void swap(T& a, T& b)
{
T x(std::move(a));
a = std::move(b);
b = std::move(x);
}
所以如果 T 实现了一个 move 构造函数,这个函数的复杂度和原生交换一样。
move_iter 的其他实现可以:
- 测试是否(!has _ trivial _ destructor<...>::value)。值得交换一个拥有资源的类,这样的类应该有一个不平凡的析构函数。但是,请注意,如果类型不是可交换的,这种方法可能会慢一些,因为它最终会调用三个副本的交换,而不是一个赋值。
- 测试交换成员函数的存在,并在任何其他情况下使用赋值。
template <typename iterator_t>
void move_iter(iterator_t dest, iterator_t source, selector<true>)
{
dest->swap(*source);
}
template <typename iterator_t>
void move_iter(iterator_t dest, iterator_t source, selector<false>)
{
*dest = *source;
}
template <typename iterator_t>
void move_iter(iterator_t dest, iterator_t source)
{
typedef typename std::iterator_traits<iterator_t>::value_type val_t;
if (dest != source)
move_iter(dest, source, has_swap<val_t>());
}
6.1.2.算法分类
回想一下非变异、选择性复制和重新排序算法之间的区别。本节展示了有时即使算法的数学细节很清楚,几种实现也是可能的,并讨论了每种实现的副作用。
假设您想同时找到一个范围的最小值和最大值。如果范围有 N 个元素,一个简单的算法使用大约 2N 次比较,但是有可能做得更好。迭代时,您可以一次检查两个连续的元素,然后将较大的元素与最大值进行比较,将较小的元素与最小值进行比较,因此每两个元素使用三次比较,或者总共大约 1.5*N 次比较。
首先考虑一个非变异函数(宏只是为了简洁) 5 :
#define VALUE_T typename std::iterator_traits<iterator_t>::value_type
template <typename iterator_t, typename less_t>
std::pair<VALUE_T, VALUE_T> minmax(iterator_t b, iterator_t e, less_t less)
minmax(begin,end)从头到尾扫描范围一次,不改变任何元素,它返回一对(min,max)。如果范围为空,您可以返回默认构造的对,或者使用 std::numeric_limits 打破 result.first < result.second 的假设。
下面是一个合理的实现,它只需要前向迭代器:
template <typename scalar_t, typename less_t>
inline scalar_t& mmax(scalar_t& a, const scalar_t& b, less_t less)
{
return (less(a, b) ? a=b : a);
}
template <typename scalar_t, typename less_t>
inline scalar_t& mmin(scalar_t& a, const scalar_t& b, less_t less)
{
return (less(b, a) ? a=b : a);
}
template <typename iterator_t, typename less_t>
std::pair<...> minmax(iterator_t begin, const iterator_t end,
less_t less)
{
typedef
typename std::iterator_traits<iterator_t>::value_type value_type;
std::pair<value_type, value_type> p;
if (begin != end)
{
p.first = p.second = *(begin++);
}
while (begin != end)
{
const value_type& x0 = *(begin++);
const value_type& x1 = (begin != end) ? *(begin++) : x0;
if (less(x0, x1))
{
mmax(p.second, x1, less);
mmin(p.first , x0, less);
}
else
{
mmax(p.second, x0, less);
mmin(p.first , x1, less);
}
}
return p;
}
通常,返回迭代器更有价值,原因有二。首先,复制对象的成本可能很高,其次,如果没有答案,则返回 end。
因此,考虑到解引用迭代器的开销不大,一种可能的改进是:
template <typen.ator_t, typename less_t>
std::pair<iterator_t, iterator_t> minmax(...)
{
std::pair<iterator_t, iterator_t> p(end, end);
if (begin != end)
{
p.first = p.second = begin++;
}
while (begin != end)
{
iterator_t i0 = (begin++);
iterator_t i1 = (begin != end) ? (begin++) : i0;
if (less(*i1, *i0))
swap(i0, i1);
// here *i0 is less than *i1
if (less(*i0, *p.first))
p.first = i0;
if (less(*p.second, *i1))
p.second = i1;
}
return p;
}
请注意,您再也没有提到 value_type。
最后,您可以概述重新排序的变体:
template <typename iterator_t>
void minmax(iterator_t begin, iterator_t end);
该函数对范围重新排序,以便在执行后,begin 是最小值,(end-1)是最大值。所有其他元素将被移动到未指定的位置。迭代器是双向的,所以 end-1 只是一种形式符号。
假设 F 取一个范围 begin...结束)。它比较第一个和最后一个元素,如果它们没有按顺序排列,就交换它们,然后继续处理第二个和倒数第二个元素。当迭代器交叉时,它停止并返回一个迭代器 H,指向范围的中间。f 执行大约 N/2 次“比较和交换”操作,其中 N 是范围的长度。
显然,最大值不能属于左半部分,最小值不能属于右半部分。你必须在两个半区间上再次调用 F,让 HL=F(begin,HL)和 HR=F(HR,end)。
当一个区间中只有一个元素时,它一定是极值。
如果一个复杂性单元是一个“比较和交换”,则该算法在迭代 0 时执行 N/2 以找到 H,对第二个分区执行 2 (N/4 ),对第三个分区执行 2 (N/8 ),依此类推,因此操作的总数也是大约 3/2 N
![9781484210116_Fig06-01.jpg
图 6-1。重新排序最小最大算法的图形表示
6.1.3.迭代器需求
算法对迭代器必须提供的操作类型有要求。根据经验,“平均”迭代器是双向的。 6 支持单增单减(+和-)、等式/不等式(==和!=).然而,它不提供任意整数、差和运算符<的加法。随机访问迭代器用在需要最高速度的地方,用于排序,所以它们通常需要特殊算法的特殊处理。
如前所述,您可以通过分派给另一个接受“迭代器类别”类型的附加形式参数的函数来确保满足迭代器的要求:
template <typename iter_t>
void do_something(iter_t begin, iter_t end)
{
return do_something(begin, end,
typename std::iterator_traits<iter_t>::iterator_category());
}
template <typename iter_t>
void do_something(iter_t begin, iter_t end, std::bidirectional_iterator_tag)
{
// do the work here
}
发明这种技术是为了调用任何迭代器类型的算法的优化版本,但是它也可以用来限制调用。标准迭代器标签形成了一个类层次结构,因此一个“强”标签可以很好地转换为“弱”需求。
以下是一些指导原则:
- 有时你会先写一个算法,然后再推导出算法运行需要哪个迭代器。虽然后验推理是完全可以接受的,但人们很容易低估子程序的要求。
- 将具有不同需求的算法分开通常是好的设计。比如不要先排序再迭代,只规定范围应该已经排序了。这可能会降低对双向迭代器的要求。
template <typename iterator_t>
void do_something(iterator_t begin, iterator_t end)
{
// the following line has stronger requirements than all the rest
std::sort(begin, end, std::greater<...>());
std::for_each(begin, end, ...);
}
template <typename iterator_t>
void do_something_on_sorted_range(iterator_t begin, iterator_t end)
{
// much better: all lines have the same complexity
std::reverse(begin, end);
std::for_each(begin, end, ...);
}
6.1.4.一个例子:集合分区
假设给你一组整数 X,你需要把它分成两个子集,使得每个子集的和大致相同。 7
对于这个问题,快速找到可接受的分区(可能是次优的)的启发式算法是已知的。最简单的是贪婪算法,它规定:
Let P1={} and P2={} be empty sets;
While X is not empty, repeat:
{
Assign the largest remaining integer in X to the set Pi which currently has the lower sum
(break ties arbitrarily);
}
这个处方听起来像是重新排序,所以你可以考虑一个变异算法。您对输入范围重新排序并返回一个迭代器 h,这样[begin,h]和 h,end 就是所需的分区。另外,作为一个额外的奖励,您可以计算两个分区之和的差![image,这是要最小化的目标。因此,结果将是 std::pair <迭代器,value_type >。
该实现的行为如下:
- 该范围被分成三个逻辑块:左边的分区 A [begin,end _ of _ A],右边的分区 B[begin _ of _ B,end],以及剩余的中间块 m。
- a 和 B 最初是空的,M = begin,end。
- 当 M 不为空时,重复:
- M 的元素按降序排序。
- 迭代 m 的元素。分配给 A 的对象被交换到 A 的右边(在位置“A 的结尾”),分配给 B 的对象被交换到 B 的左边(在位置“B 的开头减 1”)。 [8
这段代码是变异算法的简明示例:
- 它不分配临时内存
- 它的运行时复杂性是有文档记录的
#define mxt_value_type(T) typename std::iterator_traits<T>::value_type
template <typename iterator_t>
std::pair<iterator_t, mxt_value_type(iterator_t)>
equal_partition (iterator_t begin, iterator_t end)
{
typedef mxt_value_type(iterator_t)> scalar_t;
scalar_t sum_a = 0;
scalar_t sum_b = 0;
iterator_t end_of_A = begin;
iterator_t beg_of_B = end;
while (end_of_A != beg_of_B)
{
std::sort(end_of_A, beg_of_B, std::greater<scalar_t>());
iterator_t i = end_of_A;
do
{
if (sum_b < sum_a)
{
sum_a = sum_a - sum_b;
sum_b = *i;
smart_swap(*i, *(--beg_of_B));
}
else
{
sum_b = sum_b - sum_a;
sum_a = *i;
smart_swap(*i, *(end_of_A++));
}
}
while ((i != beg_of_B) && (++i != beg_of_B));
}
return std::make_pair(end_of_A,
sum_a<sum_b ? sum_b-sum_a : sum_a-sum_b);
}
让我们检查一下实现,以确定对迭代器和类型的要求。
- 乍一看,它可能看起来像一个双向 iterator_t 就足够了,因为代码只使用了复制构造、不等式、++、和-。但是,std::sort 需要随机访问迭代器。 9
- 底层的 scalar_t 需要实现运算符
sum_b = sum_b - sum_a;
sum_b -= sum_a;
第二个选项将引入一个新的需求(即操作符-=)。
6.1.5.识别迭代器
如果 T 是迭代器或指针,元函数 STD::iterator _ traits
template
<
typename T,
typename IS_ITERATOR = std::iterator_traits<T>::value_type
>
class require_iterator
{
// similar to a static assertion,
// this will compile only if T is a compliant iterator
};
您可以通过使用迭代器需要提供的五个基本类型定义来猜测一个类型是否是符合迭代器。 10
再次使用 SFINAE 技巧 11 ,您将编写:
template <typename T>
struct is_iterator
{
static const bool value =
static_AND
<
has_type_value_type<T>,
static_AND
<
has_type_reference<T>,
static_AND
<
has_type_pointer<T>,
static_AND
<
has_type_iterator_category<T>,
has_type_difference_type<T>
>
>
>
>::value;
};
template <typename T>
struct is_iterator<T*>
{
static const bool value = true;
};
启发式的理由如下:
- std::map 不是迭代器,但是它定义了除 iterator_category 之外的所有类型。因此,您确实需要测试这五种类型是否都存在。
- 您无法测试 std::iterator_traits 是否定义良好,因为如果 T 无效,它将无法编译。
- 存在 is_iterator 为真的类型,但它们甚至不是可取消引用的(很简单,假设 T 是 std::iterator_traits
)。
这里有一个测试,可以很精确地识别非常数迭代器。 12
主要动机如下:
- 迭代器会定义一个值类型 T 和一个引用类型,通常是 T&或者 const T&。
- T&可以转换成 T,但不能反过来
- const T&和 T 是可以相互转换的。 十三
有几种可能的情况:
- 如果 T 不是迭代器,它甚至不是可变迭代器(这由最后一个部分专门化处理)。
- 如果 reference 是 value_type&那么答案为真(这种情况由 helper 类处理)。
- 如果 reference 可以转换为 value_type,但不能反过来,那么答案也是正确的。
template <typename T1, typename T2>
struct is_mutable_iterator_helper
{
static const bool value = false;
};
template <typename T>
struct is_mutable_iterator_helper<T&, T>
{
static const bool value = true;
};
template <typename T, bool IS_ITERATOR = is_iterator<T>::value>
class is_mutable_iterator
{
typedef typename std::iterator_traits<T>::value_type val_t;
typedef typename std::iterator_traits<T>::reference ref_t;
public:
static const bool value =
static_OR
<
is_mutable_iterator_helper<ref_t, val_t>,
selector
<
has_conversion<ref_t, val_t>::L2R &&
!has_conversion<val_t, ref_t>::L2R
>
>::value;
};
template <typename T>
class is_mutable_iterator<T, false>
{
public:
static const bool value = false;
};
- Has_conversion <ref_t val_t="">:根据值类型的定义,L2R 应该为 true。</ref_t>
- 您将静态 bool 包装在选择器中,因为 static_OR 需要两种类型,而不是常量。
一些迭代器被认为是有序集合上的视图,例如,set
事实上,这个问题是病态的:set
template <typename T>
void wrong(std::set<T>::iterator i) // error: T is non-deducible
{
}
但是,如果将选项限制在一些特殊的候选人身上,就可以让问题变得简单一些。事实上,一个集合被声明为 set
下面的代码给出了一个实际的例子:
template <typename T, typename less_t, typename alloc_t = std::allocator<T> >
class sorted_vector
{
std::vector<T, alloc_t> data_;
less_t less_;
public:
template <typename iterator_t>
sorted_vector(iterator_t begin, iterator_t end, less_t less = less_t())
: data_(begin, end), less_(less)
{
// this is unnecessary if begin...end is already sorted
std::sort(data_.begin(), data_.end(), less_);
}
};
由于底层排序算法即使在范围已经排序的情况下也会消耗 CPU,所以尝试猜测是否可以避免这一步。 15
有两种不同的测试。首先,一些迭代器保证它们指向的范围是有序的(这是一个“静态测试”,因为它只取决于迭代器的类型);第二,任何迭代器对都可能恰好指向一个排序范围(这是一个“运行时测试”)。您可以按以下顺序组合静态测试和运行时测试:
if (!is_sorted_iterator<iterator_t, less_t>::value)
{
if (!is_sorted(begin, end, less_)
std::sort(begin, end, less_);
}
一个非常重要的观察是,is_sorted_iterator <iterator_t less_t="">被允许返回假阴性而不是假阳性。您可以容忍不必要的排序,但是不能让未排序的范围通过。</iterator_t>
注意测试一个范围是否已经排序需要线性时间。
在 C++0x 中,有一个专用的算法:
template <typename FwdIter>
bool is_sorted(FwdIter begin, FwdIter end);
template <typename FwdIter, typename less_t>
bool is_sorted(FwdIter begin, FwdIter end, less_t LESS);
在经典的 C++ 中,后一个函数的实现非常简洁:
using std::adjacent_find;
using std::reverse_iterator;
return
adjacent_find(reverse_iterator<FwdIter>(end), reverse_iterator<FwdIter>(begin), LESS)
== reverse_iterator<FwdIter>(begin);
is_sorted_iterator <iterator_t less_t="">可以简单地尝试将 iterator_t 与一些特殊的标准迭代器进行匹配:</iterator_t>
#define ITER(C,T1) typename std::C<T1,less_t>::iterator
#define CONST_ITER(C,T1) typename std::C<T1,less_t>::const_iterator
template
<
typename iter_t,
typename less_t,
typename value_t = typename std::iterator_traits<iter_t>::value_type
>
struct is_sorted_iterator
{
static const bool value =
static_OR
<
static_OR
<
typeequal<iter_t, ITER(set, value_t)>,
typeequal<iter_t, CONST_ITER(set, value_t)>
>,
static_OR
<
typeequal<iter_t, ITER(multiset, value_t)>,
typeequal<iter_t, CONST_ITER(multiset, value_t)>
>
>::value;
};
地图有部分专门化:
#define ITER(C,T1,T2) typename std::C<T1,T2,less_t>::iterator
#define CONST_ITER(C,T1,T2) typename std::C<T1,T2,less_t>::const_iterator
template
<
typename iter_t,
typename less_t,
typename T1,
typename T2
>
struct is_sorted_iterator< iter_t, less_t, std::pair<const T1, T2> >
{
static const bool value =
static_OR
<
static_OR
<
static_OR
<
typeequal<iter_t, ITER(map,T1,T2)>,
typeequal<iter_t, CONST_ITER(map,T1,T2)>
>,
static_OR
<
typeequal<iter_t, ITER(multimap,T1,T2)>,
typeequal<iter_t, CONST_ITER(multimap,T1,T2)>
>
>,
static_OR
<
static_OR
<
typeequal<iter_t, ITER(map,const T1,T2)>,
typeequal<iter_t, CONST_ITER(map,const T1,T2)>
>,
static_OR
<
typeequal<iter_t, ITER(multimap,const T1,T2)>,
typeequal<iter_t, CONST_ITER(multimap,const T1,T2)>
>
>
>::value;
};
6.1.6.通过迭代器值类型选择
采用迭代器的函数可能想要调用另一个模板,用迭代器值类型标记调用。特别是,这允许一些变异算法处理异常,比如具有常量引用的可变迭代器(例如 std::map)。
template <typename iterator_t>
iterator_t F(iterator_t b, iterator_t e)
{
typedef typename std::iterator_traits<iterator_t>::value_type value_type;
return F(b, e, instance_of<value_type>());
}
template <typename iterator_t, typename T1, typename T2>
iterator_t F(iterator_t b, iterator_t e, instance_of< std::pair<const T1, T2> >)
{
// modify only i->second
}
template <typename iterator_t, typename T>
iterator_t F(iterator_t b, iterator_t e, instance_of<T>)
{
// modify *i
}
选择性复制算法可以使用输出迭代器值类型来决定返回什么。假设计算产生一系列值和相应的权重;如果输出类型是一对,转储将同时写入两者;否则,它只写入值:
template <[...], typename iterator_t>
void do_it([...], iterator_t out_begin, iterator_t out_end)
{
typedef typename
std::iterator_traits<iterator_t>::value_type value_type;
// ...
dump([...], out_begin, out_end, instance_of<value_type>());
}
private:
template <[...], typename iterator_t, typename T1, typename T2>
void dump([...], iterator_t b, iterator_t e, instance_of< std::pair<T1, T2> >)
{
for (i=b; i!=e; ++i)
// write value in b->first and weight in b->second
}
template <typename iterator_t, typename T>
void dump([...], iterator_t b, iterator_t e, instance_of<T>)
{
for (i=b; i!=e; ++i)
// write value in *b
}
注意,可以使用访问器来统一实现。详情见下一节。
6.2.一般化
本节讨论使用不同的 I/O 接口编写函数的替代方法。由于迭代器提供了数据视图,它们可能不够灵活,尤其是对于具有特殊语义的算法。
一些计算可以用属性来描述,例如“找到价格最小的对象”。当然,你需要迭代器来扫描对象,但是如何读取价格呢?
6.2.1.属性和访问器
接受迭代器的算法可能不使用指向类型的实际接口。通常,它们有两个版本,一个版本中所需的操作由指向的类型直接处理,另一个版本采用一个完全取代对象接口的额外函子。
例如,std::sort(b,e)假设所指向的类型小于可比类型,它使用指针的运算符
作为这个概念的概括,算法可以根据属性来定义。
属性一般化数据成员:(只读)属性只是单个(常量)参数的非空函数,默认情况下,它调用参数的常量成员函数或返回参数的数据成员的副本。这里有一个小例子。
template <typename T>
struct property_size
{
typedef size_t value_type;
value_type operator()(const T& x) const
{
return x.size();
}
};
传递给算法的 property_size 实例被称为属性的访问器。
许多计算算法可以用性质来定义。他们忽略尖型,但需要读“其大小”;因此,它们需要合适的访问器。
根据假设,应用访问器并不昂贵。
注意一个属性是一个仿函数,因此正确的 typedef 应该是 result_type,但是用户需要存储一个属性值的副本,这个副本在概念上位于对象中,只被仿函数“访问”。因此,value_type 是首选。
一个读写属性有一个额外的成员写回一个值:
template <typename T>
struct property_size
{
typedef size_t value_type;
value_type operator()(const T& x) const
{
return x.size();
}
value_type operator()(T& x, const value_type v) const
{
x.resize(v);
return x.size();
}
};
访问器在不同的上下文中很有用。在最简单的情况下,它们保存对 std::transform 或自定义二元运算符的调用。
假设您有一个 std::string 范围,需要找到总大小和最大大小。使用经典的 STL,你可以写一个自定义的“sum”和一个自定义的“less”。从字符串到整数(大小)的转换是在这些函子内部执行的。
struct sum_size
{
size_t operator()(size_t n, const std::string& s) const
{
return n + s.size();
}
};
struct less_by_size
{
bool operator()(const std::string& s1, const std::string& s2) const
{
return s1.size() < s2.size();
}
};
// assume beg!=end
size_t tot = std::accumulate(beg, end, 0U, sum_size());
size_t max = std::max_element(beg, end, less_by_size())->size();
使用访问器,您将有更多的代码重用:
#define VALUE typename accessor_t::value_type
template <typename iterator_t, typename accessor_t>
VALUE accumulate(iterator_t b, iterator_t e, accessor_t A, VALUE init = 0)
{
while (b != e)
init = init + A(*b++);
return init;
}
template <typename iterator_t, typename accessor_t>
iterator_t max_element(iterator_t b, iterator_t e, accessor_t A)
{
if (b == e)
return e;
iterator_t result = b;
while ((++b) != e)
{
if (A(*result) < A(*b))
result = b;
}
return result;
}
size_t tot = accumulate(beg, end, property_size<std::string>());
size_t max = max_element(beg, end, property_size<std::string>());
默认访问器返回对象本身:
template <typename T>
struct default_accessor
{
typedef T value_type;
T& operator()(T& x) const
{
return x;
}
};
在一次需要几个“命名属性”的复杂计算算法中,访问器提供了很好的抽象度。我们举背包问题作为例子。
每个对象都有两个属性:一个(非负)价格和一个(非负)质量。给你一笔初始的钱,你的目标是购买具有最高质量的物品子集。在计算结束时,(部分)结果是原始范围的子集,因此您选择一种重新排序算法。您返回一个划分范围的迭代器,与算法的一个额外的副产品配对——在本例中是总质量。
访问器方面的函数原型很长,但是非常清楚:
#define QUALITY typename quality_t::value_type
#define PRICE typename price_t::value_type
template <typename price_t, typename quality_t, typename iterator_t>
std::pair<iterator_t, QUALITY> knapsack(iterator_t begin, iterator_t end,
PRICE budget,
price_t price,
quality_t quality)
price_t 和 quality_t 是必需属性的访问器。
所以元素i 的价格就是 price(i),它可以存储在一个类型为 typename price_t::value_type 的变量中。
为了说明用法,这里有一个非变异函数,它简单地评估解决方案的总体质量,假设您从头开始购买所有可能的元素:
template <typename price_t, typename quality_t, typename iterator_t>
QUALITY knapeval(iterator_t begin, iterator_t end, PRICE money,
price_t price, quality_t quality)
{
typename quality_t::value_type total_q = 0;
while (begin != end)
{
const typename price_t::value_type p = price(*begin);
if (p > money)
break;
money -= p;
total_q += quality(*begin++);
}
return total_q;
}
对于算法测试,通常需要固定访问器。生成符合以下条件的占位符结构会很方便:
struct property_price
{
typedef unsigned value_type;
template <typename T>
value_type operator()(const T& x) const
{
return x.price();
}
};
struct price_tag_t {};
struct quality_tag_t {};
struct knapsack_object
{
property<unsigned, price_tag_t> price;
property<unsigned, quality_tag_t> quality;
};
接下来描述属性类。
extra 标记禁止在具有相同基础类型(例如,unsigned int)的不同属性之间赋值。
template <typename object_t, typename tag_t = void>
class property
{
object_t data_;
public:
property()
: data_() // default-constructs fundamental types to zero
{}
property(const object_t& x)
: data_(x)
{}
const object_t& operator()() const
{
return data_;
}
const object_t& operator()(const object_t& x)
{
return data_ = x;
}
const char* name() const
{
return typeid(tag_t).name();
}
};
6.2.2.拟态
一些通用算法接受一个范围——即两个迭代器[begin,end 和一个附加值——或者一个一元谓词。这些算法实现了两次。后一个版本使用谓词来测试元素,前一个版本测试“与给定值相等”。
一个经典的例子是 std::find 对 std::find_if。
| 模板
| iter_t find(iter_t begin,iter_t end, object_t x ) | iter_t find_if(iter_t begin,iter_t end, functor_t f |
| { | { |
| for(;开始!=结束;++begin) | for(;开始!=结束;++begin) |
| { | { |
| if (begin == x) | if (f(begin)) |
| 打破; | 打破; |
| } | } |
| 返回开始; | 返回开始; |
| } | } |
原则上,find 可以用 find_if 来重写:
template <typename iter_t, typename object_t>
iter_t find(iter_t begin, const iter_t end, object_t x)
{
std::equal_to<object_t> EQ;
return std::find_if(begin, end, std::bind2nd(EQ, x));
}
但是反过来也是可能的:
template <typename functor_t>
class wrapper
{
functor_t f_;
public:
wrapper(functor_t f = functor_t())
: f_(f)
{
// verify with a static assertion that
// functor_t::result_type is bool
}
bool operator==(const typename functor_t::argument_type& that) const
{
return f_(that);
}
};
template <typename iter_t, typename functor_t>
iter_t find_if(iter_t begin, const iter_t end, functor_t F)
{
return std::find(begin, end, wrapper<functor_t>(F));
}
类型 T 的模拟对象非正式地表现为 T 的实例,但在内部它是一元谓词。一个拟态实现 operator==(const T &),operator!=(const T &),以及运算符“cast to T”(所有运算符均为 const)。
要调用谓词,您需要编写:
if (f(x))
要调用模拟,等效的语法应该是:
if (f == x)
这些要求有些不完整:
- 等式和不等式应该以任意顺序接受拟态本身和 T,以防止不希望的使用“cast to T”进行比较(您将在后面读到更多相关内容)。
- 转换运算符应该返回满足相同条件的原型值。
换句话说,如果 M 是 T 类型的模拟,那么 M 的基本属性是:
M<T> m;
assert(m == static_cast<T>(m));
T 型最简单的模拟就是 T 本身。
作为一个非常简单的例子,让我们实现一个识别正数的模拟:
template <typename scalar_t >
struct positive
{
bool operator==(const scalar_t& x) const
{
return 0<x;
}
bool operator!=(const scalar_t& x) const
{
return !(*this == x);
}
operator scalar_t() const
{
return 1; // an arbitrary positive number
}
};
这是第一个不再需要 find_if 的应用程序。
double a[] = { -3.1, 2.5, -1.0 };
std::find(a, a+3, positive<double>()); // fine, returns pointer to 2.5
关键是 find 中的 value 参数有独立的模板类型,所以正的
推导出的模板类型,例如:
template <typename I>
iter_t find(I, I, typename std::iterator_traits<I>::value_type x)
将导致模拟衰减到其默认值(因此,将返回错误的查找结果)。
模拟界面实际上可以更加丰富:
template <typename scalar_t, bool SIGN = true>
struct positive
{
bool operator==(const scalar_t& x) const
{
return (0<x) ^ (!SIGN);
}
bool operator!=(const scalar_t& x) const
{
return !(*this == x);
}
operator scalar_t() const
{
// arbitrary positive and non-positive numbers
return SIGN ? +1 : -1;
}
positive<scalar_t, !SIGN> operator!() const
{
return positive<scalar_t, !SIGN>();
}
};
template <typename scalar_t, bool SIGN>
inline bool operator==(const scalar_t& x, const positive<scalar_t, SIGN> p)
{
return p == x;
}
template <typename scalar_t, bool SIGN>
inline bool operator!=(const scalar_t& x,
const positive<scalar_t, SIGN> p)
{
return p != x;
}
因此,正数
请注意,用户只需写正的
std::find(a, a+3, !positive<double>());
您已经看到,编写拟态比编写仿函数要花费更多的精力,但这是值得的,尤其是对于将特殊值作为参数的通用函数。下一节提供了另一个应用程序。
6.2.3.范围结束
基于迭代器的算法不能动态计算范围的结尾。例如,您不能表达“查找 5.0,但在第一个负数处停止”的概念,因为范围是预先计算的。您需要两个函数调用。
using namespace std;
find(begin, find_if(begin, end, bind2nd(less<double>(), 0.0)), 5.0)
C 字符串给出了范围低效的典型例子。假设您正在复制一个 C 字符串,并得到一个到目标的输出迭代器:
const char* c_string = "this is an example";
// can we avoid strlen?
std::copy(c_string, c_string+strlen(c_string), destination);
strlen 必须遍历字符串寻找终止符,然后 copy 再次遍历它。这个过程在实践中非常快,但它做了一个不必要的传递。
假设你重写了副本。你不改变函数体,只是允许范围的端点有不同的类型。
template <typename iter1_t, typename iter2_t, typename end_t>
iter2_t copy_2(iter1_t begin, end_t end, iter2_t output)
{
while (begin != end)
*(output++) = *(begin++),
return output;
}
这相当于要求 end 是 iter1_t 类型的模拟。
比较以下代码:
template <typename char_t, char_t STOP = 0>
struct c_string_end
{
typedef char_t* iterator_t;
operator iterator_t() const { return 0; }
bool operator!=(const iterator_t i) const
{
return !(*this == i);
}
bool operator==(const iterator_t i) const
{
return i==0 || *i==STOP;
}
};
// implement operator== and != with arguments in different order
// ...
const char* begin = "hello world!";
copy_2(begin, c_string_end<const char>(), output); // ok and efficient!
copy_2(begin, begin+5, output); // also ok!
对 copy_2 的后一个调用等效于 std::copy。
总而言之,模仿有两种用途:
- 接受“测试”的算法,测试可以是值,也可以是谓词。
- 处理范围的算法开始...end,其中 end 只是一个终止条件(也就是说,它不会递减)。
注意两者的区别。第一点中提到的“测试”是元素上的跳过并继续条件;end 是迭代器上的终止和退出准则。
当对象充当跳过并继续过滤器时,模拟接口中的 cast 操作符证明是有用的。假设您正在计算满足某些标准的所有元素的平均值。首先,你写一个试探性的“经典”版本。
template <class iter_t, class predicate_t>
typename std::iterator_traits<iter_t>::value_type
average_if(iter_t begin, iter_t end, predicate_t f)
{
size_t count = 0;
typename std::iterator_traits<iter_t>::value_type result = 0;
for (; begin != end; ++begin)
{
if (f(*begin))
{
result += *begin;
++count;
}
}
return count>0 ? result/count : [[???]];
}
如果谓词拒绝所有元素,你不知道返回什么,除了可能 STD::numeric _ limits<...>::quiet _ NaN()(希望 has_quiet_NaN 为真)。
然而,最好的选择是询问函数对象返回什么。如果 F 被视为拒绝逻辑(而不是接受),它还应该负责提供被拒绝元素的原型,这正是模拟的基本属性。
这就是为什么你用一个代表安静的拟态来重写算法 _NaN: 16
template <typename iter_t, typename end_t, typename nan_t>
typename std::iterator_traits<iter_t>::value_type
average(iter_t begin, const end_t end, nan_t NaN)
{
size_t count = 0;
typename std::iterator_traits<iter_t>::value_type result = 0;
for (; begin != end; ++begin)
{
if (NaN != *begin)
{
result += *begin;
++count;
}
}
return count>0 ? result/count : NaN;
}
模拟的典型作用是表示“排除过滤器”:
template <typename scalar_t>
struct ieee_nan
{
operator scalar_t() const
{
return std::numeric_limits<scalar_t>::quiet_NaN();
}
bool operator!=(const scalar_t& x) const
{
return x == x;
}
bool operator==(const scalar_t& x) const
{
return x != x;
}
};
强制转换运算符的危险之处在于它可能会被意外调用。再次考虑四页前的例子:
template <typename iterator_t, char STOP = 0>
struct c_string_end
{
// ...
};
// ooops. forgot to implement operator== and !=
// with arguments in different order
// later...
while (begin != end)
{
// ...
}
开始!=end 会实际调用 bool 运算符!=(const char,const char)传递已经是指针的 begin,并对 end 应用强制转换(这会产生一个空指针)。因此,循环永远不会退出。
还要注意,可以包装一个拟态并将其转换为谓词,反之亦然。
6.3.迭代器换行
编写符合 STL 的迭代器是一项复杂的活动,它涉及到大量的代码重复。幸运的是,编写 const _ iterators 要容易得多。
包装迭代器,常量或非常量,是一个包含另一个迭代器作为成员的类。包装器将每个“定位操作”(例如,递增和递减)转发给成员,但是它截取解引用,改变结果以便表达底层数据集的逻辑视图。
由于最终用户可能看不到实际的数据,而是一个定制的伪造值,所以通常不可能通过视图修改原始对象,所以包装的迭代器大多是 const_iterators 。
假设您有一个整数向量和一个迭代器包装器,它返回乘以 5 的实际值。
template <typename iterator_t>
class multiplier_iterator
{
// ...
};
// Later...
int main()
{
std::vector<int> data;
data.push_back(8);
multiplier_iterator<std::vector<int>::iterator> i(data.begin(), 5);
int a = *i; // now a = 5*8
*i = 25; // what about data[0] now???
assert(*i == 25);
*i = 24; // what about data[0] now???
assert(*i == 25); // are you sure?
}
即使 multiplier_iterator 可以在位置 data[0]物理上写一个整数,它应该怎么做?如果它足够聪明的话,它会写 25/5=5,这样*i 从那一点开始返回 25。
但是,指令*i = 24 就更成问题了。它应该抛出异常吗?还是什么都不做?还是反正 set data[0]=(24+(5-1))/5?
运算符-> 的正确实现确实是最困难的问题。幸运的包装器会简单地将执行递归地分派给包装的迭代器,但是由于这通常揭示了底层的“真实”数据,所以它可能与包装逻辑不兼容。
考虑省略不太可能被使用的操作符。>——算符是第一候选,除非它们的实现既琐碎又正确。 17
箭头操作符用于访问指向类型的成员,但是在这种类型是泛型的代码部分(例如,可以推导出一个模板参数),这些成员通常是未知的,所以不应该使用箭头。 18
*例如,std::vector::assign 即使在没有操作符的迭代器上也能正常工作。
6.3.1.迭代器扩展器
迭代器包装器会将大多数操作委托给被包装的对象,比如 operator++。
使用一个静态接口 19 (名为 iterator_expander )调度部分非常容易自动化:
class wrapper
: public iterator_expander<wrapper>
, public std::iterator_traits<wrapped>
{
wrapped w_;
public:
wrapped& base()
{
return w_;
}
const wrapped& base() const
{
return w_;
}
wrapper(wrapped w)
: w_(w)
{
}
[...] operator* () const
{
// write code here
}
[...] operator-> () const
{
// write code here
}
};
iterator_expander 接口(下面列出)负责所有可能的定位(++、++、+=、-=、+和-)和比较操作符。都实现了,照例只有用了才会编译。如果 wrapped 不支持它们中的任何一个,就会发出一个错误(不需要静态断言,因为错误的原因很明显)。
还要注意,接口中的每个操作符都返回 true_this(),而不是this,因为否则像(i++)这样的组合表达式将不起作用。iterator_expander 不实现运算符*,但 true_this()返回实际的包装器。
template <typename iterator_t, typename diff_t = ptrdiff_t>
class iterator_expander
{
protected:
// the static interface part, see Section 6.2
~iterator_expander() {}
iterator_expander() {}
iterator_t& true_this()
{ return static_cast<iterator_t&>(*this); }
const iterator_t& true_this() const
{ return static_cast<const iterator_t&>(*this); }
public:
iterator_t& operator++() { ++true_this().base(); return true_this(); }
iterator_t& operator--() { --true_this().base(); return true_this(); }
iterator_t& operator+=(diff_t i)
{ true_this().base() += i; return true_this(); }
iterator_t& operator-=(diff_t i)
{ true_this().base() -= i; return true_this(); }
iterator_t operator++(int)
{ iterator_t t(true_this()); ++(*this); return t; }
iterator_t operator--(int)
{ iterator_t t(true_this()); --(*this); return t; }
iterator_t operator+(diff_t i) const
{ iterator_t t(true_this()); t+=i; return t; }
iterator_t operator-(diff_t i) const
{ iterator_t t(true_this()); t-=i; return t; }
diff_t operator-(const iterator_expander& x) const
{ return true_this().base() - x.true_this().base(); }
bool operator<(const iterator_expander& x) const
{ return true_this().base() < x.true_this().base(); }
bool operator==(const iterator_expander& x) const
{ return true_this().base() == x.true_this().base(); }
bool operator!=(const iterator_expander& x) const
{ return !(*this == x); }
bool operator> (const iterator_expander& x) const
{ return x < *this; }
bool operator<=(const iterator_expander& x) const
{ return !(x < *this); }
bool operator>=(const iterator_expander& x) const
{ return !(*this < x); }
};
您还需要一个外部运算符:
template <typename iterator_t, typename diff_t>
iterator_t operator+(diff_t n, iterator_expander<iterator_t, diff_t> i)
{
return i+n;
}
注意 difference_type 是取的,不是推导的。iterator_expander
因此包装器将被声明如下:
template <typename iterator_t>
class wrapper
: public iterator_expander
<
wrapper<iterator_t>,
typename std::iterator_traits<iterator_t>::difference_type
>
{
// ...
};
这里有一个简单的实际例子,它也显示了迭代器的基底可以是一个简单的整数。
class random_iterator
: public iterator_expander<random_iterator>
, public std::iterator_traits<const int*>
{
int i_;
public:
int& base() { return i_; }
const int& base() const { return i_; }
explicit random_iterator(const int i=0)
: i_(i)
{
}
int operator*() const
{
return std::rand();
}
};
int main()
{
std::vector<int> v;
v.assign(random_iterator(0), random_iterator(25));
// now v contains 25 random numbers
//...
}
注意,这个例子跳过了 arrow 操作符,解引用返回一个值,而不是一个引用(但是由于这个类继承了 const int* traits,仍然可以将一个引用绑定到*iterator,因为引用是 const int&)。
注意不在迭代器中存储值的副本。虽然这实际上允许返回真正的引用和指针,但被引用的实体有一个绑定到迭代器的生存期,而不是绑定到“容器”(换句话说,破坏迭代器,引用变得无效),这将导致微妙的错误。下面是一些不好的代码:
class random_iterator
: public iterator_expander<random_iterator>
, public std::iterator_traits<const int*>
{
int i_;
int val_; // bad
public:
const int& operator*() const
{
return val_ = std::rand(); // bad
}
const int* operator->() const
{
return &*(*this); // even worse
}
};
迭代器包装器解决了对映射中的值进行迭代的问题(或者等价地,对键进行常量迭代的问题)。
这一次,这个例子将是一个真正的非常数迭代器实现,因为您迭代了现有的元素,所以您可以返回指针和引用。
template <typename T, int N>
struct component;
template <typename T1, typename T2>
struct component<std::pair<T1, T2>, 1>
{
typedef T1 value_type;
typedef T1& reference;
typedef const T1& const_reference;
typedef T1* pointer;
typedef const T1* const_pointer;
};
template <typename T1, typename T2>
struct component<std::pair<const T1, T2>, 1>
{
typedef T1 value_type;
typedef const T1& reference;
typedef const T1& const_reference;
typedef const T1* pointer;
typedef const T1* const_pointer;
};
template <typename T1, typename T2>
struct component<std::pair<T1, T2>, 2> : component<std::pair<T2, T1>, 1>
{
};
假设 iterator_t(包装类型)指向一个类似 std::pair 的类。如果不是这样,编译器将在编译一个 ref 重载时给出一个错误。
template <typename iterator_t, int N>
class pair_iterator
: public iterator_expander< pair_iterator<iterator_t, N> >
{
static const bool IS_MUTABLE =
is_mutable_iterator<iterator_t>::value;
iterator_t i_;
typedef std::iterator_traits<iterator_t> traits_t;
typedef component<typename traits_t::value_type, N> component_t;
typedef typename component_t::reference ref_t;
typedef typename component_t::const_reference cref_t;
typedef typename component_t::pointer ptr_t;
typedef typename component_t::const_pointer cptr_t;
template <typename pair_t>
static ref_t ref(pair_t& p, static_value<int, 1>)
{ return p.first; }
template <typename pair_t>
static ref_t ref(pair_t& p, static_value<int, 2>)
{ return p.second; }
template <typename pair_t>
static cref_t ref(const pair_t& p, static_value<int, 1>)
{ return p.first; }
template <typename pair_t>
static cref_t ref(const pair_t& p, static_value<int, 2>)
{ return p.second; }
public:
explicit pair_iterator(iterator_t i)
: i_(i)
{}
iterator_t& base() { return i_; }
const iterator_t& base() const { return i_; }
typedef typename typeif<IS_MUTABLE, ref_t, cref_t>::type reference;
typedef typename typeif<IS_MUTABLE, ptr_t, cptr_t>::type pointer;
typedef typename component_t::value_type value_type;
typedef typename traits_t::iterator_category iterator_category;
typedef typename traits_t::difference_type difference_type;
reference operator* () const
{
return ref(*i_, static_value<int, N>());
}
pointer operator->() const
{
return &*(*this);
}
};
这是一个驱动函数:
template <int N, typename iterator_t>
inline pair_iterator<iterator_t, N> select(iterator_t i)
{
return pair_iterator<iterator_t, N>(i);
}
最后是一些示例代码。驱动程序的语法是 select
template <typename T>
struct Doubler
{
void operator()(T& x) const
{
x *= 2;
}
};
template <typename T>
struct User
{
void operator()(const T& x) const
{
std::cout << x << ';';
}
};
typedef std::map<int, double> map_t;
MXT_ASSERT(!is_mutable_iterator<map_t::const_iterator>::value);
MXT_ASSERT(is_mutable_iterator<map_t::iterator>::value);
map_t m;
const map_t& c = m;
m[3] = 1.4;
m[6] = 2.8;
m[9] = 0.1;
// print 3;6;9; via iterator
std::for_each(select<1>(m.begin()), select<1>(m.end()), User<int>());
// print 3;6;9; via const_iterator
std::for_each(select<1>(c.begin()), select<1>(c.end()), User<int>());
// multiplies by 2 each value in the map
std::for_each(select<2>(m.begin()), select<2>(m.end()), Doubler<double>());
std::vector<double> v1;
v1.assign(select<1>(c.begin()), select<1>(c.end()));
std::vector< std::pair<int, double> > v2(m.begin(), m.end());
// multiplies by 2 each key in the vector (the key is not constant)
std::for_each(select<1>(v2.begin()), select<1>(v2.end()), Doubler<int>());
// these two lines should give an error:
// std::for_each(select<1>(m.begin()), select<1>(m.end()), Doubler<int>());
// std::for_each(select<1>(c.begin()), select<1>(c.end()), Doubler<int>());
6.3.2.假对子
逆问题是“合并”两个逻辑视图,并获得一个使它们看起来像对的迭代器。使用 pair_iterator ,您可以构建一个键向量和一个值向量来读取一个映射,但不能反过来。
std::vector<int> key;
std::vector<double> value;
std::map<int, double> m = /* ??? */;
实际上,您可以扩展迭代器扩展器的接口,以允许派生类有多个基的可能性。简单地让 base 有 N 个重载,带一个 static_value <size_t n="">,每个重载都可能返回一个对不同类型迭代器的引用。</size_t>
您可以分离出要应用于基底的基本修饰符,并编写一个非常简单的静态递归方法。 二十
由于您事先不知道 base(static_value <size_t k="">)是什么,所以您必须引入一些带有模板成员函数的辅助“修饰符”对象,如下所示:</size_t>
struct plusplus
{
template <typename any_t>
void operator()(any_t& x) const { ++x; }
};
class pluseq
{
const diff_t i_;
public:
pluseq(const diff_t i) : i_(i) {}
template <typename any_t>
void operator()(any_t& x) const { x += i_; }
};
template <typename iterator_t, size_t N, typename diff_t>
class iterator_pack
{
protected:
typedef static_value<size_t, N> n_times;
~iterator_pack() {}
iterator_pack() {}
iterator_t& true_this()
{
return static_cast<iterator_t&>(*this);
}
const iterator_t& true_this() const
{
return static_cast<const iterator_t&>(*this);
}
/* static recursion */
template <typename modifier_t, size_t K>
void apply(const modifier_t modifier, const static_value<size_t, K>)
{
modifier(true_this().base(static_value<size_t, K-1>()));
apply(modifier, static_value<size_t, K-1>());
}
template <typename modifier_t>
void apply(const modifier_t modifier, const static_value<size_t, 0>)
{
}
public:
typedef diff_t difference_type;
iterator_t& operator++()
{
apply(plusplus(), n_times());
return true_this();
}
iterator_t& operator+=(const diff_t i)
{
apply(pluseq(i), n_times());
return true_this();
}
您需要再添加几个成员函数。为了简单起见,一些操作符,比如比较,只使用第一个元素: 21
typedef static_value<size_t,0> default_t;
diff_t operator-(const iterator_pack& x) const
{
const default_t d;
return true_this().base(d) - x.true_this().base(d);
}
bool operator<(const iterator_pack& x) const
{
const default_t d;
return true_this().base(d) < x.true_this().base(d);
}
bool operator==(const iterator_pack& x) const
{
const default_t d;
return true_this().base(d)==x.true_this().base(d);
}
所有其他操作符都以通常的方式从基本操作符派生而来——后缀++ 和操作符+来自前缀++ 和+=以及其他比较来自
有了这个新工具,这里有一个不完全标准的迭代器,它假装在 std::pair 上迭代。
首先,一些亮点:
- 这里指针是 void,因为你不想支持 operator->,但是要编译 std::iterator_traits < iterator_couple<...>>指针需要定义;然而,这个定义将阻止任何其他用途。
- iterator_category 是两个类别中较弱的一个;然而,您可以静态地断言两个类别应该是可比较的,以避免不寻常的配对(比如输入/输出迭代器)。当然,这种限制可以取消。
- 主要问题是如何定义引用。显然,你必须依赖 r1_t 和 r2_t,但不能使用 std::pair <r1_t r2_t="">。(主要是因为,在经典 C++ 中,std::pair 不支持,不会编译。) 22</r1_t>
#define TRAITS(N) std::iterator_traits<iterator##N##_t>
template <typename iterator1_t, typename iterator2_t>
class iterator_couple
: public iterator_pack
<
iterator_couple<iterator1_t, iterator2_t>,
2,
typename TRAITS(1)::difference_type
>
{
typedef typename TRAITS(1)::value_type v1_t;
typedef typename TRAITS(2)::value_type v2_t;
typedef typename TRAITS(1)::reference r1_t;
typedef typename TRAITS(2)::reference r2_t;
typedef typename TRAITS(1)::iterator_category cat1_t;
typedef typename TRAITS(2)::iterator_category cat2_t;
public:
iterator_couple(iterator1_t i1, iterator2_t i2)
: i1_(i1), i2_(i2)
{
}
typedef typename
typeif
<
is_base_of<cat1_t, cat2_t>::value,
cat1_t,
cat2_t
>::type iterator_category;
typedef std::pair<v1_t, v2_t> value_type;
typedef void pointer;
struct reference
{
/* see below... */
};
iterator1_t& base(static_value<size_t, 0>) { return i1_; }
iterator2_t& base(static_value<size_t, 1>) { return i2_; }
const iterator1_t& base(static_value<size_t, 0>) const
{ return i1_; }
const iterator2_t& base(static_value<size_t, 1>) const
{ return i2_; }
reference operator* () const
{
MXT_ASSERT
(
(is_base_of<cat1_t, cat2_t>::value
|| is_base_of<cat2_t, cat1_t>::value)
);
return reference(*i1_, *i2_);
}
private:
iterator1_t i1_;
iterator2_t i2_;
};
您必须模拟一对引用,而 std::pair 不允许:
struct reference
{
r1_t first;
r2_t second;
reference(r1_t r1, r2_t r2)
: first(r1), second(r2)
{
}
operator std::pair<v1_t, v2_t>() const
{
return std::pair<v1_t, v2_t>(first, second);
}
template <typename any1_t, typename any2_t>
operator std::pair<any1_t, any2_t>() const
{
return std::pair<any1_t, any2_t>(first, second);
}
reference& operator= (const std::pair<v1_t, v2_t>& p)
{
first = p.first;
second = p.second;
return *this;
}
void swap(reference& r)
{
swap(first, r.first);
swap(second, r.second);
}
void swap(std::pair<v1_t, v2_t>& p)
{
swap(first, p.first);
swap(second, p.second);
}
};
模板 cast-to-pair 操作符是必需的,因为 std::map 可能不会将引用转换为 pair
这种实现可能足以编写如下代码:
template <typename iter1_t, typename iter2_t>
iterator_couple<iter1_t, iter2_t> make_couple(iter1_t i1, iter2_t i2)
{
return iterator_couple<iter1_t, iter2_t>(i1, i2);
}
std::vector<int> k;
std::list<double> v1;
std::vector<double> v2;
std::map<int, double> m;
std::pair<int, double> p = *make_couple(k.begin(), v1.begin());
m.insert(make_couple(k.begin(), v1.begin()),
make_couple(k.end(), v1.end()));
std::vector< std::pair<int, double> > v;
v.assign(make_couple(k.begin(), v2.begin()),
make_couple(k.end(), v2.end()));
注意,第一次插入得到一个双向迭代器,而最后一次赋值得到一个随机访问迭代器。 23
6.4.收据
收据是空类,只能由“合法授权的实体”创建,并作为必需的参数解锁功能的执行。有些收据可以储存起来以备后用,但有些则必须立即传递。
在一个非常简单的例子中,当您需要强制函数 F 在 G 之前被调用时,您修改 F 并让它返回一个回执 R,否则无法构造它。最后,G 将 R 作为一个附加的形式参数。
当每个派生类中的虚拟成员函数 foo 应该在某个时候调用 BASE::foo 时,receiving 在类的层次结构中非常有用。
假设 foo 返回 void。
有两种类似的解决方案:
- 在公共非虚拟/受保护虚拟技术中,基类实现了一个公共非虚拟 foo,它在适当的时候调用受保护的虚函数。
class BASE
{
protected:
virtual void custom_foo()
{
}
public:
void foo()
{
/* ... */
custom_foo();
}
};
- 使用收据。BASE::foo 返回一个秘密收据,私有给 BASE。
class BASE
{
protected:
class RECEIPT_TYPE
{
friend class BASE;
RECEIPT_TYPE() {} // constructor is private
};
public:
virtual RECEIPT_TYPE foo()
{
/* ... */
return RECEIPT_TYPE();
}
};
class DERIVED : public BASE
{
public:
virtual RECEIPT_TYPE foo()
{
/* ... */
// the only way to return is...
return BASE::foo();
}
};
如果 RECEIPT_TYPE 有公共复制构造函数,DERIVED 可以随时存储 BASE::foo 的结果。否则,它将被迫在返回行上调用它。
注意,非 void 返回类型 T 可以更改为 std::pair
收据在对象中特别有用,在对象中你想要控制成员函数的执行顺序(8.6 节描述了算法):
class an_algor
{
public:
bool initialize();
void iterate();
bool stop() const;
double get_result() const;
};
double execute_correctly_algor(an_algor& a)
{
if (!a.initialize())
throw std::logic_error("something bad happened");
do
{
a.iterate();
} while (!a.stop());
return a.get_result();
}
double totally_crazy_execution(an_algor& a)
{
if (a.stop())
a.iterate();
if (a.initialize())
return a.get_result();
else
return 0;
}
通常,您希望在迭代之前调用 initialize,并在至少一次迭代之后调用 get_result。因此,您需要修改接口,如下所示:
template <int STEP, typename T>
class receipt_t : receipt_t<STEP-1, T>
{
friend class T;
receipt_t() {} // note: private
};
template <typename T>
class receipt_t<0, T>
{
friend class T;
receipt_t() {} // note: private
};
class a_better_algor
{
public:
typedef receipt_t<0, a_better_algor> init_ok_t;
typedef receipt_t<1, a_better_algor> iterate_ok_t;
init_ok_t initialize();
iterate_ok_t iterate(init_ok_t);
bool stop(iterate_ok_t) const;
double get_result(iterate_ok_t) const;
};
有了模板友谊声明(它还不是标准的)的必要的邪恶,想法应该是清楚的:因为用户不能伪造收据,她必须存储 initialize 的返回值并传递它来迭代。最后,为了得到结果,需要证明至少执行了一次迭代: 24
a_better_algor A;
a_better_algor::init_ok_t RECEIPT1 = A.initialize();
while (true)
{
a_better_algor::iterate_ok_t RECEIPT2 = a.iterate(RECEIPT1);
if (a.stop(RECEIPT2))
return a.get_result(RECEIPT2);
}
注代码:
template <typename T>
class ...
{
friend class T;
在经典 C++ 中不是标准的,因为当 T 是本机类型|(比如 int)时,该语句可能是无意义的。然而,它被一些编译器接受为扩展。在 C++0x 中,它是合法的,但是语法是:
朋友
作为一种变通方法,一些(但不是全部)经典 C++ 编译器接受这一点。这种变通方法的基本原理是,它引入了一个额外的间接方式,允许编译器将 T 视为“间接”类型,而不是模板参数。
模板
{
struct nested _ T { typedef T type;};
友类嵌套 _ t::type;
6.5.代数要求
6.5.1.少和楠
泛型类型 T 的对象通常被假定为小于可比的。
这意味着要么 T::运算符< is defined, or an instance of a binary predicate “less” is given as an extra argument. 25
一个算法应该避免混合不同的比较操作符,比如操作符<= and operator>,因为它们可能不一致。最好的解决方案是用运算符
| X | (假设有效) |
| X>Y | Y |
| x≤y) | !(Y |
| X≥Y | !(X |
| X==Y | !(X |
| x!=Y | (X |
运算符==是否应该被假定为有效,或者用等价测试来代替,这是有问题的。事实上,对操作符<的两次调用可能会慢很多(如在 std::string 中)。然而,在某些情况下,有了额外的假设,其中一个测试可能会被省略。例如,如果一个范围是排序的,那么一个带有迭代器i == (i+k)的测试可以替换为!减去(i,(i+k))。
NaN(非数字)是导致任何比较运算符“失败”的 T 的一个实例。换句话说,如果 x 和 y 中至少有一个是 NaN,那么 x OP y 如果 OP 是,<=,> =,==则返回 false,如果 OP 是!=.事实上,NaN 可以通过这个简单的测试检测出来:
template <typename T>
bool is_nan(const T& x)
{
return x != x;
}
类型 double 和 float 有一个本机 NaN。
如果 T 有一个 NaN,它可能会给排序算法带来问题。如果两个元素都不小于另一个,那么它们就是等价的, 26 所以一个 NaN 等价于任何其他元素。例如,如果你写:
std::map<double, int> m;
// insert elements...
m[std::numeric_limits<double>::quiet_NaN()] = 7;
您实际上是用 7 覆盖了一个随机(即依赖于实现的)值。
处理可能包含 NaN 的范围的正确方法是在排序前将它们分隔开,或者修改比较运算符,例如,它们位于范围的开头:
template <typename T>
struct LessWithNAN
{
bool operator()(const T& x, const T& y) const
{
if (is_nan(x))
return !is_nan(y);
else
return x<y;
}
};
6.6.巴顿-纳克曼骗局
Knuth 写道,一个技巧是一个聪明的想法,只使用一次,而一个技巧是一个至少使用两次的技巧。巴顿-纳克曼技术,也称为受限模板扩展,是一种在类中声明非成员函数和操作符的方法,将它们标记为朋友:
template <typename T>
class X
{
public:
friend int f(X<T> b) // global function #1
{
return 0;
}
friend bool operator==(X<T> a, X<T> b) // global operator #2
{
return ...;
}
};
X<double> x;
f(x); // calls #1
x == x; // calls #2
这里显示的非成员函数和运算符是非模板函数,在类被实例化时被注入到 X < T >的范围内。换句话说,它们是用 ADL 找到的,所以至少有一个参数必须有 X 类型< T >。
这种技术的主要用途是声明接受模板类内部类的全局函数。
template <typename T>
struct outer
{
template <int N>
struct inner {};
};
你不能为任意的 T 编写一个带外
template <typename T>
struct outer
{
template <int N>
struct inner
{
friend int f(inner<N>)
{ return N; }
};
};
不管 f 不是模板,您都可以随意操作模板参数:
template <typename T>
struct outer
{
template <int N>
struct inner
{
friend inner<N+1> operator++(inner<N>)
{ return inner<N+1>(); }
};
};
outer<double>::inner<0> I1;
++I1; // returns outer<double>::inner<1>
你也可以用同样的方法写模板函数,但是所有的参数都应该是可推导的因为 ADL 找不到有显式模板参数的函数。以下代码是正确的:
template <typename T>
struct outer
{
template <int N>
struct inner
{
inner(void*) {}
template <typename X>
friend inner<N+1> combine(inner<N>, X x)
{ return inner<N+1>(&x); }
};
};
outer<double>::inner<0> I;
combine(I, 0);
相反,以下示例仅在 outer 位于全局范围内时有效,但如果它包含在命名空间中则无效:
template <typename T>
struct outer
{
template <typename S>
struct inner
{
template <typename X>
friend inner<X> my_cast(inner<S>)
{ return inner<X>(); }
};
};
outer<double>::inner<int> I;
outer<double>::inner<float> F = my_cast<float>(I);
这里显示的具有功能性 my_cast 的唯一解决方法是静态接口,基类在名称空间级别,但是所需的机制是不可忽略的:
// note: global scope
template <typename T, typename S>
struct inner_interface
{
};
namespace XYZ
{
template <typename T>
struct outer
{
template <typename S>
struct inner : inner_interface<T, S>
{
inner(int = 0) {}
};
};
}
// note: global scope
template <typename X, typename T, typename S>
typename XYZ::outer<T>::template inner<X>
my_cast(const inner_interface<T,S>& x)
{
// cast x to outer<T>::inner<S> if necessary
return 0;
}
int main()
{
XYZ::outer<double>::inner<int> I;
my_cast<float>(I);
}
显然,my_cast 可能只是 inner 的一个模板成员函数,但这可能会迫使客户端在点和函数名之间引入一个模板关键字:
template <typename S>
struct inner
{
template <typename X>
inner<X> my_cast() const
{ return inner<X>(); }
};
outer<double>::inner<int> I;
outer<double>::inner<float> F = I.my_cast<float>(); // Ok.
template <typename T>
void f(outer<T>& o)
{
o.get_inner().my_cast<float>();
// error: should be
// o.get_inner().template my_cast<float>()
}
//...
1 在现代 C++ 中,有一些名为 cbegin()和 cend 的成员函数总是返回常量迭代器,所以循环会是 for(auto I = v . C begin();我!= v . cend();++i)。
2 不是所有的容器都是符合标准的,事实上,甚至 std::vector < bool >也不是。
3 value_type 被标准授予非常数合格,但这还不够。比如 std::map 的 value_type 是 pair < const key_type,mapped_type >不可赋值;关于这个问题的更多信息在第 6.3 节。
4 并且可能是次优的,但是这在这里并不真正相关。
5 类似的函数是现代 C++ 标准的一部分。参见 http://en.cppreference.com/w/cpp/algorithm/minmax。
6 这当然公平,但是武断。大约一半的 STL 容器有双向迭代器和一半的随机访问。然而,如果您根据使用情况对它们进行加权,并且包含普通的指针,那么一般的迭代器会更加随机访问。
理想情况下,你希望这些和相差 0 或 1。反正这是 NP 难问题。
8 交换过程会留下一些元素。这就是在 M 的大小上循环的原因,最坏的情况下会跳过 M 的一半左右的成员,所以算法复杂度还是超线性的;即在处理 n 个元素时,需要的时间与~ n log(n)成正比。
9 最快的经典算法 quicksort 和 heapsort 可以在超线性时间内对随机存取容器进行就地排序(通常 std::sort 是两者的组合)。mergesort 是第三种超线性方法,使用前向迭代器,但是它需要额外的内存来保存整个输入的副本。然而,在实践中,如果有这样的额外内存可用,那么复制/交换向量中的输入、对向量进行排序并返回结果可能会很方便。
10 参考【6】之第三章中的精彩描述。
11 参见 SFINAE 第 4.3.1 节。
当然,如果已知某个元函数对于某个特定的类型来说是失败的,用户总是有可能显式地专门化它。还要注意,boost 库采用了另一种方法:如果 x 是 T 的实例,它会检查*x 是否可以转换为 T 的 value_type。请参见 boost::is_readable_iterator。
13 记住如果给定两个函数 void F(X)和 Y G(),则 X 可转换为 Y,调用 F(G())是合法的。如果 X=T 且 Y=T &或 Y=const T &,则通话正常。或者,如果 X=T &,Y=T,则调用无效。这正是 has_conversion 的工作方式。
14 正如在第一章中所说的,实际上有一种方法可以做到这一点,但是它不能很好地扩展,而且很麻烦。它需要 std::set 作者的合作;这种技术是 6.6 节的主题。
15 在所有的超线性算法中,只有一些 mergesort 变体可以利用排序后的输入;另一方面,quicksort 和 heapsort 并不明显依赖于初始数据的“熵”。
16 算法现在应该命名为 average_if_not。
一般来说,迭代器包装器不需要 100%符合标准,因为有一些常见问题不会影响它们的功能。最常见的是缺少 operator- >和 operator*返回值而不是引用(换句话说:iterator::reference 和 iterator::value_type 是一样的)。另一方面,这些简化功能的实现可能会容易得多。请参阅本章后面的 random_iterator 示例。
18 一个例外是 std::map,可以名正言顺地称 i- >第一,i- >第二。
像往常一样,Boost 库提供了一个更完整的解决方案,但这个更简单,功能也更全。
20 为简洁起见,省略了所有“减法”功能。
21 在综合中,迭代器包是一个类似迭代器的类,它保持 N 个不同迭代器之间的同步。如果 P 是这样一个包,那么只有当所有迭代器都是随机访问时,才能调用 P += 2。否则,代码将无法编译。然而,如果第一个组件是一个随机访问迭代器,那么这个包将有一个恒定的时间差。
22 std::pair 通过常量引用在构造函数中接受参数,但如果其中一个类型是引用,它会创建对引用的引用,这是禁止的。
有趣的是,全局帮助函数的使用避免了构造函数和函数声明之间所有令人讨厌的歧义。在[7]的“第 6 项”中描述并解决了该问题。
24 一个基于类型的收据系统不处理实例。例如,你可以有两个 algors,用第一个的收据解锁第二个。这可以减轻(在运行时!)通过在收据上添加州名。例如,您可能希望在收据本身中存储一个指向 algor 的指针,并在 algor 中添加断言,以强制收据中的指针确实等于“this”。
25 作为一个经验法则,如果 T 是这样的,有一个单一的方法来决定 A 是否< B,因为比较是琐碎的或固定的,那么你应该提供 T::运算符<(例如,T = RomanNumber)。相反,如果有不止一个可行的比较,你不应该每次都实现操作符<并传递正确的函数,以使你的意图明确(例如,T = Employee)。这些泛函可以在 t 内部定义。
26 或者,如果 x < y 和 y < x 都为真,那么比较运算符无效。*
七、代码生成器
本章讨论生成代码的模板——部分是静态的,部分是在运行时执行的。假设您必须执行一个简单的权力比较:
int x = ...;
if (34 < x5 < 47)
显然,您希望有 3 个 4 个和 4 个 7 个的静态常数和一个相应的运行时供电算法来获得 x 5 个。然而,对 std::pow(x,5)的调用可能不是最理想的,因为 5 是一个编译时常数,可能“嵌入”在调用中。
事实上,TMP 的目标之一就是让编译器获得最大限度的信息,这样它就可以利用这些信息。
7.1.静态代码生成器
迭代可以用在纯静态的上下文中;回想一下第三章中的重复平方算法:
#define MXT_M_SQ(a) ((a)*(a))
template <size_t X, size_t Y>
struct static_raise;
template <size_t X> struct static_raise<X,2>
{ static const size_t value = X*X; };
template <size_t X> struct static_raise<X,1>
{ static const size_t value = X; };
template <size_t X> struct static_raise<X,0>
{ static const size_t value = 1; };
template <size_t X, size_t Y>
struct static_raise
{
static const size_t v0 = static_raise<X, Y/2>::value;
static const size_t value = ((Y % 2) ? X : 1U) * MXT_M_SQ(v0);
};
double data[static_raise<3, 4>::value]; // an array with 81 numbers
static_raise 不生成任何代码,只生成一个编译时结果(即一个数值常量)。
相同的算法现在用于实现静态代码生成。静态递归为任何指定的指数值生成一个函数。
假设 1 是一个有效的标量。
template <typename scalar_t, size_t N>
struct static_pow
{
static inline scalar_t apply(const scalar_t& x)
{
return ((N % 2) ? x : 1) *
static_pow<scalar_t,2>::apply(static_pow<scalar_t,N/2>::apply(x));
}
};
template <typename scalar_t>
struct static_pow<scalar_t, 2>
{
static inline scalar_t apply(const scalar_t& x)
{ return x*x; }
};
template <typename scalar_t>
struct static_pow<scalar_t, 1>
{
static inline scalar_t apply(const scalar_t& x)
{ return x; }
};
template <typename scalar_t>
struct static_pow<scalar_t, 0>
{
static inline scalar_t apply(const scalar_t& x)
{ return 1; }
};
size_t x = 3;
size_t n = static_pow<size_t, 4>::apply(x); // yields 81
这里,模板递归不产生编译时结果,而是产生编译时算法;实际上,static_pow 是一个代码生成器模板。
还要注意,您可以避免乘以 1,这是由三元运算符暗示的:
template <typename scalar_t, size_t N>
struct static_pow
{
static inline scalar_t apply(const scalar_t& x, selector<false>)
{
return static_pow<2>::apply(static_pow<N/2>::apply(x));
}
static inline scalar_t apply(const scalar_t& x, selector<true>)
{
return x*apply(x, selector<false>());
}
static inline scalar_t apply(const scalar_t& x)
{
return apply(x, selector<(N % 2)>());
}
};
特别是这个代码生成器是强类型。用户必须事先指定参数类型。这不是算法正常工作所必需的。事实上,演绎其论点的较弱版本也很好:
template <size_t N>
struct static_pow
{
template <typename scalar_t>
static inline scalar_t apply(const scalar_t& x)
{ ... }
};
template <>
struct static_pow<2>
{
template <typename scalar_t>
static inline scalar_t apply(const scalar_t& x) { return x*x; }
};
// ...
对强类型模板 s 的调用更加冗长,因为用户显式地编写了一个可以推导出来的类型:
size_t x = 3;
size_t n1 = static_pow<size_t, 4>::apply(x); // verbose
size_t n2 = static_pow<4>::apply(x); // nicer
然而,有时明确是值得的。对参数的强制转换与对结果的强制转换完全不同,因为代码生成器将生成一个全新的函数:
double x1 = static_pow<double, 4>::apply(10000000); // correct
double x2 = static_pow<4>::apply(10000000); // wrong (it overflows)
double x3 = static_pow<4>::apply(10000000.0); // correct again
通常,通过借鉴群体的技巧,可以同时编写强代码生成器和弱代码生成器。您将弱生成器移动到部分专门化中,这将被通用模板调用。
struct deduce
{
};
template <size_t N, typename scalar_t = deduce>
struct static_pow;
template <>
struct static_pow<2, deduce>
{
template <typename scalar_t>
static inline scalar_t apply(const scalar_t& x)
{ ... }
};
template <size_t N>
struct static_pow<N, deduce>
{
template <typename scalar_t>
static inline scalar_t apply(const scalar_t& x)
{ ... }
};
// primary template comes last
template <size_t N, typename scalar_t>
struct static_pow
{
static inline scalar_t apply(const scalar_t& x)
{
return static_pow<N>::apply(x);
}
};
严格的参数检查实际上只由主模板执行,它会立即调用演绎专门化。声明的顺序很重要:static_pow
7.2.双重检查停止
编译时递归通常是通过用一组不同的模板参数调用模板“本身”来实现的。实际上,根本没有递归,因为模板参数的变化会生成不同的实体。您得到的是静态的“循环展开”。
静态递归的优点是显式展开的代码更容易优化。
以下代码片段对两个已知长度的数组执行矢量求和:
template <size_t N, typename T>
void vector_sum_LOOP(T* a, const T* b, const T* c)
{
for (int i=0; i<N; ++i)
a[i] = b[i] + c[i];
}
template <size_t N, typename T>
void vector_sum_EXPLICIT(T* a, const T* b, const T* c)
{
a[0] = b[0] + c[0];
a[1] = b[1] + c[1];
// ...
// assume that it's possible to generate exactly N of these lines
// ...
a[N-1] = b[N-1] + c[N-1];
}
对于小 N,显式展开的版本会更快,因为现代处理器可以并行执行一些算术/浮点运算。即使没有编译器的具体优化,处理器也会执行加法,比如说,一次四次。1
然而,对于较大的 N,代码将超过处理器缓存的大小,因此第一个版本从某个点开始会更快。
事实上,理想的解决方案是两者的混合:
static const int THRESHOLD = /* platform-dependent */;
template <size_t N, typename T>
void vector_sum(T* a, const T* b, const T* c)
{
if (N>THRESHOLD)
{
int i=0;
for (; (i+4)<N; i+=4) // the constant 4 and...
{
a[i+0] = b[i+0] + c[i+0]; //
a[i+1] = b[i+1] + c[i+1]; // ...the number of lines in this block
a[i+2] = b[i+2] + c[i+2]; // are platform-dependent
a[i+3] = b[i+3] + c[i+3]; //
}
for (; i<N; ++i) // residual loop
{
a[i] = b[i] + c[i];
}
}
else
{
vector_sum_EXPLICIT<N>(a, b, c);
}
}
无论如何,这个实现有一个问题。假设阈值是 1000。当编译器实例化 vector_sum <1000,double>时,它会浪费时间生成 1000 行永远不会被调用的代码:
if (true)
{
// ...
}
else
{
a[0] = b[0] + c[0];
a[1] = b[1] + c[1];
// ...
a[999] = b[999] + c[999];
}
要解决这个问题,您需要添加一个双重检查:
else
{
vector_sum_EXPLICIT<(N>THRESHOLD ? 1 : N)>(a, b, c);
}
双重检查不仅仅是优化。静态递归可以产生无限多的行数,再次假设你有一个长度为 N 的数组,需要用连续的整数填充它。您希望能够编写一个函数模板 integrize,其调用会生成本机代码,在逻辑上等价于:
{
data[0] = 0;
data[1] = 1;
// ...
data[N-1] = N-1;
}
但是你猜当 N 很大的时候,由于处理器缓存的影响,展开的循环会产生巨量的字节,其质量最终会拖慢执行。 2
所以您使用 integrize 来选择编译时策略或运行时策略:
template<typename T, int N>
void integrize(T (&data)[N])
{
if (N<STATIC_LOWER_BOUND)
integrize_helper<N>(data);
else
for (size_t i=0; i<N; ++i)
data[i] = i;
}
首先,从一个不正确的函数开始:
template <int N, typename T>
void integrize_helper(T* const data)
{
data[N-1] = N-1;
integrize_helper<N-1>(data);
}
递归没有限制,所以永远不会成功编译。
您可能会尝试进行以下改进:
template <int N, typename T>
void integrize_helper (T* const data)
{
data[N-1] = N-1;
if (N>1)
integrize_helper<N-1>(data);
}
这个版本仍然不工作,因为编译器将产生一个无限深度的调用序列。从某种意义上来说,if (N>1)的条件总是假的,但这没关系——这样的代码会被优化器删除,但编译器会报错并提前停止!
data[2-1] = 2-1; // here N=2
if (true) // 2>1?
{ // integrize_helper<2-1>
data[1-1] = 1-1; // here N=1
if (false) // 1>1?
{ // integrize_helper<1-1>
data[0-1] = 0-1; // here N=0
if (false) // 0>1?
{
//...
}
}
}
换句话说,编译器看到 integrize_helper <1>依赖于 integrize_helper <0>,因此出现了无限递归(在编译时)。
双重检查停止习惯用法再次解决了这个问题:
template <int N, typename T>
void integrize_helper(T* const data)
{
data[N-1] = N-1;
if (N>1)
integrize_helper<(N>1) ? N-1 : 1>(data);
}
注意 N>1 周围的额外括号(否则,N 和 1 之间的>将被解析为关闭模板的尖括号)。
由于双重检查,编译器将像这样扩展代码:
data[2-1] = 2-1; // here N=2
if (true) // 2>0?
{ // integrize_helper<2-1>
data[1-1] = 1-1; // here N=1
if (1>1)
call integrize_helper<1> again
}
扩展是有限的,因为 integrize_helper <1>只提到它自己(这是一个定义良好的实体,而不是一个新的实体),递归停止。当然,integrize_helper <1>永远不会在运行时调用自己。优化器将精简 if(真)分支,并删除最后一个 if(假)。
一般来说,双重检查停止习惯用法规定停止递归,提到一个已经实例化的模板(而不是一个新的)并同时阻止它的执行。
最后,您再次将这个习惯用法作为针对代码膨胀的优化:
template<typename T, int N>
void integrize(T (&data)[N])
{
if (N<STATIC_LOWER_BOUND)
integrize_helper<(N<STATIC_LOWER_BOUND) ? N : 1>(data);
else
for (size_t i=0; i<N; ++i)
data[i] = i;
}
7.3.静态和动态哈希
有时可以通过内核宏在静态和运行时实现之间共享一个算法。下面的示例显示了如何静态哈希字符串。
通常假设 hash 是一个存储在 size_t 中的整数,并且您有一个宏。以 x,旧的散列和一个叫做 c 的新字符为例,这里有一些可能性:
#define MXT_HASH(x, c) ((x) << 1) ^ (c)
#define MXT_HASH(x, c) (x) + ((x) << 5) + (c)
#define MXT_HASH(x, c) ((x) << 6) ^ ((x) & ((~size_t(0)) << 26)) ^ (c)
注意哈希宏要求 c 为正数。您可以用(c-CHAR_MIN)替换 c,但是这会使散列依赖于平台。如果 char 是有符号的,则“a'-CHAR_MIN 等于 97-(-128) = 225,如果 CHAR 是无符号的,则相同的表达式得出 97-0 = 97。
此外,std::string 和 std::wstring 中的相同文本不应返回两个不同的哈希代码。
假设您忽略了非 ASCII 字符的情况,一个优雅的解决方法是将 char c 转换为无符号 char。
常量不应该硬编码,而应该在编译时生成。
你可以替换经典代码
const char* text = ...;
if (strcmp(text, "FIRST")==0)
{
// ...
}
else if (strcmp(text, "SECOND")==0)
{
// ...
}
else if (strcmp(text, "THIRD")==0)
{
// ...
}
像这样的东西:
const char* text = ...;
switch (dynamic_hash(text))
{
case static_hash<'F','I','R','S','T'>::value:
// ...
break;
case static_hash<'S','E','C','O','N','D'>::value:
// ...
break;
}
- 散列将节省大量的字符串比较,即使它可能产生误报。 3
- 如果 static_hash 产生重复的值,开关就不会编译,所以它永远不会产生假阴性(也就是说,“第一”、“第二”等词总是会被匹配而不会产生歧义)。
静态算法使用模板旋转和一个非常简洁的实现:
template
<
char C0=0, char C1=0, char C2=0, char C3=0, ..., char C23=0,
size_t HASH = 0
>
struct static_hash
: static_hash<C1,C2...,C23,0, MXT_HASH(HASH, static_cast<unsigned char>(C0))>
{
};
template <size_t HASH>
struct static_hash<0,0,0,0,...,0, HASH>
: static_value<size_t, HASH>
{
};
dynamic_hash 中唯一的自由度是函数签名。
这里有一个相当一般的例子,带有一些普通的 C 语言技巧:
std::pair<size_t, const char*> dynamic_hash(const char* text,
const char* separ = 0,
const char* end = 0)
{
size_t h = 0;
const char* const end1 = separ ? text+strcspn(text, separ) : end;
const char* const end2 = (end && end<end1) ? end : end1;
while (end2 ? text<end2 : (*text != 0))
{
const size_t c = static_cast<unsigned char>(*(text++));
h = MXT_HASH(h, c);
}
return std::make_pair(h, text);
}
int main()
{
const char* text = “hello, dynamic hash”;
dynamic_hash(text); // hash all string, up to char(0)
dynamic_hash(text, ";,"); // hash up to any of the separators
dynamic_hash(text, ";,", text+10); // up to separator, at most 10 chars
}
我选择返回一个复合结果、哈希值和更新后的“迭代器”。
7.3.1.字符的功能集
正确函数集的选择可以通过推导出的模板参数(参见第 4.2.1 节中的 string_traits)或环境模板参数来完成。
一个自然的例子是字符集的问题:一些字符串转换函数可以被加速,给定一些字符集,比如说{'0 ',' 1 '...9'},是连续的。如果 c 属于集合,可以通过简单的减法 c-' 0 '将 c 转换为整数,但如果数字字符集是任意分散的,则需要更复杂的实现。
使用模板旋转扫描字符集:
namespace charset {
template
<
typename char_t,
char_t C0,
char_t C1 = 0,
char_t C2 = 0,
// ...
char_t C9 = 0
>
struct is_contiguous
{
static const bool value = (C0+1==C1) &&
is_contiguous<char_t,C1,C2,C3,C4,C5,C6,C7,C8,C9>::value;
};
template <char C0>
struct is_contiguous<char,C0>
{
static const bool value = true;
};
template <wchar_t C0>
struct is_contiguous<wchar_t,C0>
{
static const bool value = true;
};
}
接下来,静态测试的结果可以保存在全局特征结构中:
struct ascii
{
static const bool value_lowerc =
charset::is_contiguous<char,
'a','b','c','d','e','f','g','h','i','j'>::value
&&
charset::is_contiguous<char,
'j','k','l','m','n','o','p','q','r','s'>::value
&&
charset::is_contiguous<char,
's','t','u','v','w','x','y','z'>::value;
static const bool value_upperc =
charset::is_contiguous<char,
'A','B','C','D','E','F','G','H','I','J'>::value
&&
charset::is_contiguous<char,
'J','K','L','M','N','O','P','Q','R','S'>::value
&&
charset::is_contiguous<char,
'S','T','U','V','W','X','Y','Z'>::value;
static const bool value_09 =
charset::is_contiguous<char,
'0','1','2','3','4','5','6','7','8','9'>::value;
static const bool value = value_09 && value_lowerc && value_upperc;
};
假设 ascii::value 为真。你可以写一个函数集来处理这种特殊情况:
template <typename T, T lower, T upper>
inline bool is_between(const T c)
{
return !(c<lower) && !(upper<c);
}
struct ascii_traits
{
typedef char char_type;
static inline bool isupper(const char_type c)
{
return is_between<char,'A','Z'>(c);
}
static inline bool islower(const char_type c)
{
return is_between<char,'a','z'>(c);
}
static inline bool isalpha(const char_type c)
{
return islower(c) || isupper(c);
}
static inline bool isdigit(const char_type c)
{
return is_between<char,'0','9'>(c);
}
//...
static inline char tolower(const char c)
{
return isupper(c) ? c-'A'+'a' : c;
}
static inline char toupper(const char c)
{
return islower(c) ? c-'a'+'A' : c;
}
};
在不同的实现中,使用 std::locale:
template <typename char_t>
struct stdchar_traits
{
typedef char_t char_type;
static inline bool isupper(const char_t c)
{
return std::isupper(c, locale());
}
static inline bool islower(const char_t c)
{
return std::islower(c, locale());
}
static inline bool isalpha(const char_t c)
{
return std::isalpha(c, locale());
}
static inline bool isdigit(const char_t c)
{
return std::isdigit(c, locale());
}
...
static inline char_t tolower(const char_t c)
{
return std::tolower(c, std::locale());
}
static inline char_t toupper(const char_t c)
{
return std::toupper(c, std::locale());
}
};
并最终结合这些类型:
struct standard {};
struct fast {};
template <typename char_t, typename charset_t = fast>
struct char_traits : stdchar_traits<char_t>
{
};
template <>
struct char_traits<char, fast>
: typeif<ascii::value, ascii_traits, stdchar_traits<char> >::type
{
};
默认情况下,环境参数 charset_t 设置为 fast。如果在当前平台可能的话,首选快集;否则,将使用标准集。 4
7.3.2.更换箱子
本节列出了一些用于改变字符大小写的实用程序。首先,它介绍了一些标签。请注意,“区分大小写”被视为“不转换”标签。 5
struct case_sensitive {};
struct upper_case {};
struct lower_case {};
这个例子利用了 char_traits 提供了一个在运行时改变字符的有效接口这一事实(这个例子仅限于 char)。作品的经典部分是函子的集合。
template <typename mutation_t, typename traits_t = char_traits<char> >
struct change_case;
template <typename traits_t>
struct change_case<case_sensitive, traits_t>
{
typedef typename traits_t::char_type char_type;
char_type operator()(const char_type c) const
{
return c;
}
};
template <typename traits_t>
struct change_case<lower_case, traits_t>
{
typedef typename traits_t::char_type char_type;
char_type operator()(const char_type c) const
{
return traits_t::tolower(c);
}
};
template <typename traits_t>
struct change_case<upper_case, traits_t>
{
typedef typename traits_t::char_type char_type;
char_type operator()(const char_type c) const
{
return traits_t::toupper(c);
}
};
int main()
{
std::string s = "this is a lower case string";
std::transform(s.begin(), s.end(), s.begin(), change_case<upper_case>());
}
现在,您将在编译时进行类似的转换。
template <typename case_t, char C, bool FAST = ascii::value>
struct static_change_case;
FAST 是一个隐藏参数;不管其值如何,区分大小写的转换应该什么都不做:
template <char C, bool FAST>
struct static_change_case<case_sensitive, C, FAST>
{
static const char value = C;
};
如果 FAST 为真,则转换是琐碎的。如果 FAST 是假的,很遗憾,每个可以改变大小写的角色都需要自己的专精。宏将节省大量的打字时间。
template <char C>
struct static_change_case<lower_case, C, true>
{
static const char value = ((C>='A' && C<='Z') ? C-'A'+'a' : C);
};
template <char C>
struct static_change_case<upper_case, C, true>
{
static const char value = ((C>='a' && C<='z') ? C-'a'+'A' : C);
};
template <char C>
struct static_change_case<lower_case, C, false>
{
static const char value = C; // a generic char has no case
};
template <char C>
struct static_change_case<upper_case, C, false>
{
static const char value = C; // a generic char has no case
};
#define mxt_STATIC_CASE_GENERIC(C_LO, C_UP) \
\
template <> struct static_change_case<lower_case, C_UP, false> \
{ static const char value = C_LO; }; \
\
template <> struct static_change_case<upper_case, C_LO, false> \
{ static const char value = C_UP; }
mxt_STATIC_CASE_GENERIC('a', 'A');
mxt_STATIC_CASE_GENERIC('b', 'B');
...
mxt_STATIC_CASE_GENERIC('z', 'Z');
#undef mxt_STATIC_CASE_GENERIC
这对静态散列和动态散列都有直接的应用。
像往常一样,宏只是为了方便。注意,在动态散列中引入了一个非推导模板参数。
#define mxt_FIRST_CHAR(c) \
static_cast<unsigned char>(static_change_case<case_t, C>::value)
template
<
typename case_t,
char C0=0, char C1=0, char C2=0, char C3=0, ..., char C23=0,
size_t HASH = 0
>
struct static_hash
: static_hash<case_t,C1,C2,...,C23,0, MXT_HASH(HASH, mxt_FIRST_CHAR(C0))>
{
};
template <typename case_t, size_t HASH>
struct static_hash<case_t,0,0,0,0,...,0, HASH>
: static_value<size_t, HASH>
{
};
template <typename case_t>
inline ... dynamic_hash(const char* text, ...)
{
const change_case<case_t> CHANGE;
size_t h = 0;
const char* const end1 = (separ ? text+strcspn(text, separ) : end);
const char* const end2 = (end && end<end1) ? end : end1;
while (end2 ? text<end2 : (*text != 0))
{
const size_t c = static_cast<unsigned char>(CHANGE(*(text++)));
h = MXT_HASH(h, c);
}
return std::make_pair(h, text);
}
这种修改后的算法将改变哈希值计算中字符串的大小写,因此“大写哈希”实际上是一个不区分大小写的值:
switch (dynamic_hash<upper_case>(text).first)
{
case static_hash<'F','I','R','S','T'>::value:
// will match "First", "FIRST", "first", "fiRST"...
break;
}
7.3.3.模仿技术
本节使用模仿技术 重写 dynamic_hash。在新的原型中,end 不是可选的,所以您必须提供更多的重载来获得灵活的语法。至于原 C 版本:
template <typename case_t, typename iterator_t, typename end_t>
std::pair<size_t, iterator_t>
dynamic_hash(iterator_t begin, const end_t end, size_t h = 0)
{
typedef typename std::iterator_traits<iterator_t>::value_type char_t;
const change_case< case_t, char_traits<char_t> > CHANGE;
while (end != begin)
{
const size_t c = static_cast<unsigned char>(CHANGE(*(begin++)));
h = MXT_HASH(h, c);
}
return std::make_pair(h, begin);
}
template <typename case_t, typename iterator_t>
inline std::pair<size_t, iterator_t>
dynamic_hash(iterator_t begin, size_t h = 0)
{
return dynamic_hash(begin, c_string_end<iterator_t>(), h);
}
可以插入一些有用的拟态类物体 6 :
template <typename char_t, char_t CLOSE_TAG>
struct stop_at
{
template <typename iterator_t>
inline bool operator!=(const iterator_t i) const
{
return (*i != 0) && (*i != CLOSE_TAG);
}
};
size_t h = dynamic_hash<case_insensitive>(text, stop_at<char, ';'>()).first;
template <bool (*funct)(const char), bool NEGATE>
struct apply_f
{
template <typename iterator_t>
inline bool operator!=(const iterator_t i) const
{
return funct(*i) ^ NEGATE;
}
};
typedef apply_f<char_traits<char>::isspace, true> end_of_word;
typedef apply_f<char_traits<char>::isalpha, false> all_alpha;
end_of_word 在第一个空格处停止,all_alpha 在第一个非字母字符处停止。
7.3.4.不明确的重载
dynamic_hash 的发展导致了更多的模板参数和更多的重载。你需要小心不要因为模棱两可的过载 分辨率而导致编译问题。
在[2]的附录 B 中描述了确切的过载解决规则,但是这里描述了粗略的总结。
当编译器遇到一个函数调用时,它必须从所有同名函数的集合中挑选出与给定参数匹配的最专门化的集合。如果不存在这样的函数或者最佳匹配不明确,它必须发出一个错误。
如果你有几个名为 F 的函数模板,你可以用 F[1],F[2]等等来表示它们。 7 你说 F[1]比 F[2]更专门化如果 F[2]可以用在 F[1]用的任何地方,有精确的论元匹配,反之则不行。
例如:
template <typename T1, typename T2>
void F(T1 a, T2 b); // this is F[1]
template <typename T>
void F(T a, T b); // this is F[2]
template <typename T>
void F(T a, int b); // this is F[3]
第二个模板 F[2]比 F[1]更专门化,因为调用 F(X,X)可以引用其中任何一个,但只有 F[2]与 F(X,Y)完全匹配。同样,F[3]比 F[1]更专门化。
然而,这是一个部分排序标准。如果没有比其他函数更专门化的函数,编译器将中止,报告一个不明确的重载。实际上,在前面的例子中,F[2]和 F[3]是没有可比性的。F[3]不会与 F(X,X)完全匹配,F[2]也不会与 F(X,int)完全匹配。 8
int z = 2;
F(z, z); // error: could be F[2] with T=int or F[3] with T=int
非正式地,一个简单明确的特例是完全替换。如果一个模板参数完全被固定类型或以前的模板参数所取代,那么得到的函数将比原来的函数更加专门化。取 F[1],用 T1 代替 T2 的每一次出现,得到 F[2];用 int 替换 T2,得到 F[3]。
库编写器通常提供一组重载,其中一个或多个元素是函数模板。经常被低估或忽略的一个问题是,预先确定集合是否有序。一个有序的集合永远不会产生歧义错误。
缺省参数和模板的组合经常使演绎变得非常困难。
template <typename case_t, typename iterator_t, typename end_t>
[...] dynamic_hash(iterator_t begin, const end_t end,
size_t crc = 0); // dynamic_hash[1]
template <typename case_t, typename iterator_t>
[...] dynamic_hash(iterator_t begin, size_t crc = 0); // dynamic_hash[2]
要确定这个集合是否是有序的,只需要考虑带有两个参数的调用的情况,很明显,完全替换条件成立(用 size_t 替换 end_t)。
但是,请注意,dynamic_hash(T,int)将调用 dynamic_hash[1]:
dynamic_hash(text, 123); // invokes (1) [with end_t = int]
一个用户友好的库将试图避免歧义,首先通过使用额外的类型:
struct hash_type
{
size_t value;
hash_type() : value(0) {}
explicit hash_type(const size_t c) : value(c) {}
};
template <typename case_t, typename iterator_t, typename end_t>
[...] dynamic_hash(iterator_t begin, end_t end, hash_type h = hash_type());
template <typename case_t, typename iterator_t>
[...] dynamic_hash(iterator_t begin, hash_type h = hash_type());
虽然这不会改变编译器选择函数的方式,但它会使错误对用户更明显,因为现在 dynamic_hash(text,123)甚至不会编译。
dynamic_hash(text, hash_type(123)); // this instead is correct
相反,通过将原始返回类型包装在 typename only_if ::type 子句中,可以获得彻底的改变(参见 4.3.3 节)。
template <typename T1, typename T2>
struct different : selector<true>
{};
template <typename T>
struct different<T, T> : selector<false>
{};
template <typename case_t, typename iterator_t, typename end_t>
typename only_if<different<end_t, hash_type>::value, [...]>::type
dynamic_hash(iterator_t begin, const end_t end, hash_type h = hash_type());
假设您将 C 版本添加回(表示为 dynamic_hash[3]):
template <typename case_t>
[...] dynamic_hash(const char* text, const char* const separator = 0, const char* const end = 0, size_t h = 0)
这个函数可以生成一个不明确的调用。dynamic_hash(const char*)匹配 dynamic _ hash2 或 dynamic_hash[3]。该错误取决于两个函数都是模板。因为 case_t: had dynamic_hash[3]是一个经典函数,所以它会被优先选择。
要避免该问题,请删除分隔符和 end 的默认参数。
7.3.5.算法输入输出
可以让 dynamic_hash 返回一个包含更新后的迭代器位置和哈希值的对。
用户通常需要存储结果,以便进行拆分:
std:pair<size_t, const char*> p = dynamic_hash(text);
text = p.second;
switch (p.first)
{
//...
}
这可能很冗长,尤其是当迭代器具有长类型时。 9
C++11 为关键字 auto 赋予了新的含义:
auto p = dynamic_hash(text);
但是注意 auto 不能指代对象的一部分。以下行是非法的:
std::pair<auto, const char*> p = dynamic_hash(text);
您可以通过引用获取迭代器并更新它,但这不是一个公平的解决方案,因为如果您想保存原始值,它会迫使调用者复制迭代器。
相反,您可以修改返回类型。它将是一个概念上类似于 pair 的对象,具有用结果覆盖引用的选项:
template <typename iterator_t>
struct dynamic_hash_result
{
size_t value;
iterator_t end;
dynamic_hash_result(const size_t v, const iterator_t i)
: value(v), end(i)
{
}
dynamic_hash_result& operator>>(iterator_t& i)
{
i = end;
return *this;
}
};
您相应地更改 dynamic_hash 函数中的 return 语句(即,替换 std::make_pair(...)与动态散列结果(...)).
最后一个函数调用确实很紧凑。它更新文本,同时返回散列。此外。值后缀让你想起 static _ hash<>:::value。当然,更多的变化是可能的。 10
switch ((dynamic_hash(text) >> text).value)
{
case static_hash<'a','b','c'>::value:
//...
}
7.3.6.拟态界面
模仿对象是轻量级的,在概念上类似于仿函数,但是它们的表达能力接近于标量。因为它们确实被实例化了,所以让我们研究一下将它们与操作符结合的可能性:
size_t h = dynamic_hash<case_insensitive>(text,
stop_at<char, ';'>() || stop_at<char, ','>()).value;
这是一个静态接口 11 的好任务:
template <typename static_type>
class hash_end_type
{
public:
const static_type& true_this() const
{
return static_cast<const static_type&>(*this);
}
template <typename iterator_t>
inline bool operator!=(const iterator_t i) const
{
return true_this() != i;
}
};
// note the CRTP
template <bool (*funct)(const char), bool NEGATE>
struct apply_f : public hash_end_type< apply_f<funct, NEGATE> >
{
template <typename iterator_t>
inline bool operator!=(const iterator_t i) const
{
return funct(*i) ^ NEGATE;
}
};
// note again the CRTP
template <typename char_t, char_t CLOSE_TAG>
struct stop_at : public hash_end_type< stop_at<char_t, CLOSE_TAG> >
{
template <typename iterator_t>
inline bool operator!=(const iterator_t i) const
{
return (*i != CLOSE_TAG);
}
};
让所有对象继承相同的接口,您可以定义“组合类型”和逻辑运算符:
struct logic_AND {};
struct logic_OR {};
template <typename T1, typename T2, typename LOGICAL_OP>
class hash_end_type_combo
: public hash_end_type< hash_end_type_combo<T1, T2, LOGICAL_OP> >
{
T1 t1_;
T2 t2_;
public:
hash_end_type_combo(const T1& t1, const T2& t2)
: t1_(t1), t2_(t2)
{
}
template <typename iterator_t>
inline bool operator!=(const iterator_t i) const
{
return combine(i, LOGICAL_OP());
}
private:
template <typename iterator_t>
bool combine(const iterator_t i, logic_AND) const
{
return (t1_ != i) && (t2_ != i);
}
template <typename iterator_t>
bool combine(const iterator_t i, logic_OR) const
{
return (t1_ != i) || (t2_ != i);
}
};
template <typename K1, typename K2>
inline hash_end_type_combo<K1, K2, logic_AND>
operator&& (const hash_end_type<K1>& k1, const hash_end_type<K2>& k2)
{
return hash_end_type_combo<K1, K2, logic_AND>(k1.true_this(), k2.true_this());
}
template <typename K1, typename K2>
inline hash_end_type_combo<K1, K2, logic_OR>
operator|| (const hash_end_type<K1>& k1, const hash_end_type<K2>& k2)
{
return hash_end_type_combo<K1, K2, logic_OR>(k1.true_this(), k2.true_this());
}
注意 operation 标签的反直觉用法。您可能会尝试用一个“活动标签”来替换 logic_AND,比如 std::logical_and
template <typename iterator_t>
inline bool operator!=(const iterator_t i) const
{
return LOGICAL_OP()(t1_ != i, t2_ != i);
}
这是不正确的,因为它会造成短路(当你把 A&B 表示为 F(A,B)时,在调用 F 之前必须对所有的参数求值)。
size_t h = dynamic_hash<case_insensitive>(text,
stop_at<char,';'>() || stop_at<char,','>() || stop_at<char,0>()).value;
还要注意,在 stop_at 中删除了对空字符的检查。现在必须显式添加它,但它只执行一次。
这个语法是一个λ表达式的例子,这是 9.2 节的主题。
7.4.第 n 个最小值
本节给出了一个涉及数据结构的简单递归编译时函数的分步示例。
你写一个名为 nth_min
出于稍后将讨论的原因,让我们强加一个额外的要求,即容器不应该从动态内存中分配它的工作空间。
template <typename scalar_t, size_t N>
class nth_min
{
scalar_t data_[N];
public:
void insert(const scalar_t& x)
{
update(data_, x);
}
const scalar_t& operator[](const size_t i) const
{
return data_[i];
}
};
以下段落提供了一个合适的更新函数。 十三
template <typename scalar_t, int N>
inline void update(scalar_t (&data)[N], const scalar_t& x)
{
// now N is known, start iterations here
}
首先,您需要以递归形式可视化算法。假设作为归纳假设,data_ 包含目前为止遇到的 N 个最小值,按升序排列。
现在观察“丢弃 x”等价于“在不存在的位置 N 写 x”。您可以使用自定义选择器来提取写操作:
template <int N>
struct nth
{
};
template <typename scalar_t, int N, int SIZE>
void write(scalar_t (&data)[SIZE], const scalar_t& x, nth<N>)
{
data[N] = x;
}
template <typename scalar_t, int SIZE>
void write(scalar_t (&data)[SIZE], const scalar_t& x, nth<SIZE>)
{
}
第二个重载使用数组的维度。所以 write(data,x,nth ())实际上是“如果可能的话,在数组数据的第 I 个位置写 x;否则,什么也不做”。
这个小抽象允许您将相同的递归模式扩展到整个算法:
if (x ≥ data_[N-1])
// x is not in the N minima
data_[N] = x and return;
else
if (x ≥ data_[N-2])
data_[N-1] = x and return;
else
...
template <typename scalar_t, int N, int SIZE>
void iterate(scalar_t (&data)[SIZE], const scalar_t& x, nth<N>)
{
if (x < data[N])
{
data[N] = data[N-1];
iterate(data, x, nth<N-1>());
}
else
{
write(data, x, nth<N+1>()); // write x at position N+1
}
}
接下来,您必须编写一个迭代终止符,并且您可以开始识别模板参数的值,这些值使得代码的其余部分没有意义。当 N==0 时,data[N-1]肯定不是良构的,所以您专门化/重载了 N 为 0 的情况。事实上,如果您只需要追踪序列中最小的元素,就不需要移动了:
template <typename scalar_t, int SIZE>
void iterate(scalar_t (&data)[SIZE], const scalar_t& x, nth<0>)
{
// here N=0, after this point, stop iterations
// if x is less than minimum, keep x, else discard it
if (x < data[0])
data[0] = x;
else
write(data, x, nth<1>());
}
else 分支不能省略,但是如果 SIZE 为 1,优化编译器会将其清除。
最后,递归从数组的最后一个元素开始向后,所以传递 N-1:
template <typename scalar_t, int N>
void update(scalar_t (&data)[N], const scalar_t& x)
{
iterate(data, x, nth<N-1>());
}
这个实现的不优雅之处在于 iterate <0>包含了 iterate
还需要另一个概括。所有写操作都涉及移位数据[K] =数据[K-1]或插入数据[K] = x,这与数组界限有关。一个函数模板能代表两者吗?
是的,如果您能够用一个数据元素来标识 x,并且只指定要选取的元素的索引:
template <typename scalar_t, int N, int SIZE, int J>
void write(scalar_t (&data)[SIZE], const scalar_t& x, nth<N>, nth<J>)
{
data[N] = data[J];
}
template <typename scalar_t, int SIZE, int J>
void write(scalar_t (&data)[SIZE], const scalar_t& x, nth<SIZE>, nth<J>)
{
}
如果您比较来自实现的指令 data[K] = data[K-1]和 data[0] = x,您会看到 x 自然地被标识为 data[-1]。
因此,您添加了另外两个专门化:
template <typename scalar_t, int N, int SIZE>
void write(scalar_t (&data)[SIZE], const scalar_t& x, nth<N>, nth<-1>)
{
data[N] = x;
}
template <typename scalar_t, int SIZE>
void write(scalar_t (&data)[SIZE], const scalar_t& x, nth<SIZE>, nth<-1>)
{
}
综上所述,write(data,x,N,J)是 data[N] = data[J]的复杂说法;n 和 J 是选择器,不是整数。像往常一样,该函数推导出数组的长度,因此越界访问变成无操作。
template <typename scalar_t, int N, int SIZE>
void iterate(scalar_t (&data)[SIZE], const scalar_t& x, nth<N>)
{
if (x < data[N])
{
write(data, x, nth<N>(), nth<N-1>());
iterate(data, x, nth<N-1>());
}
else
{
write(data, x, nth<N+1>(), nth<-1>()); // line #1
}
}
template <typename scalar_t, int SIZE>
void iterate(scalar_t (&data)[SIZE], const scalar_t& x, nth<-1>)
{
}
当代码中的 N=0 时,根据需要,write 转换为 data[0] = x,iteration -1 为空。
注意,您在第 1 行中付出了一般性的代价,这乍一看相当不清楚,因为您必须显式地使用 nth 来访问 x。
如果 N 很大,最快的算法可能会将对象存储在一大块内存中,并在必要时对它们进行排序,在运行时完成所有工作。在最坏的情况下,如果 K 是插入的项目数,执行时间与 K.N 成正比,对于静态版本,但是对于小的 N 值和简单的 POD 类型(即当运算符< and assignment do not have significant overhead), the static version will usually perform faster, due to its compactness and absence of hidden constants. 14
最后,你可以用一个真实的赋值来替换 write 函数调用,它的隐含意义是一个赋值。只需使用代理:
struct null_reference
{
template <typename scalar_t>
null_reference& operator= (const scalar_t&)
{
return *this;
}
};
template <int K>
struct nth
{
template <typename scalar_t, int SIZE>
static scalar_t& element(scalar_t (&data)[SIZE], const scalar_t& x)
{
return data[K];
}
template <typename scalar_t>
static null_reference element(scalar_t (&data)[K], const scalar_t& x)
{
return null_reference();
}
};
template <>
struct nth<0>
{
template <typename scalar_t, int SIZE>
static scalar_t& element(scalar_t (&data)[SIZE], const scalar_t& x)
{
return data[0];
}
};
template <>
struct nth<-1>
{
template <typename scalar_t, int SIZE>
static const scalar_t& element(scalar_t (&data)[SIZE], const scalar_t& x)
{
return x;
}
};
struct nth_min
{
template <typename scalar_t, int SIZE>
static void update(scalar_t (&data)[SIZE], const scalar_t& x)
{
iterate(data, x, nth<SIZE-1>());
}
private:
template <typename scalar_t, int N, int SIZE>
static void iterate(scalar_t (&data)[SIZE], const scalar_t& x, nth<N>)
{
if (x < data[N])
{
nth<N>::element(data, x) = nth<N-1>::element(data, x);
iterate(data, x, nth<N-1>());
}
else
{
nth<N+1>::element(data, x) = nth<-1>::element(data, x);
}
}
template <typename scalar_t, int SIZE>
static void iterate(scalar_t (&data)[SIZE], const scalar_t& x, nth<-1>)
{
}
};
7.5.模板工厂模式
模板擅长做出编译时决策,但所有程序都需要做出运行时决策。
工厂 模式 通过多态解决了运行时决策问题。一个被称为工厂的隔离函数嵌入了所有的逻辑,并返回一个指向动态创建的对象的指针,该对象通过其虚拟成员函数调用来驱动程序流:
class abstract_task
{
public:
virtual void do_it() = 0;
virtual ~abstract_task()
{
}
};
class first_task : public abstract_task
{
public:
first_task(/* parameters */)
{
// ...
}
virtual void do_it()
{
// ...
}
};
enum task_type
{
FIRST_TASK, SECOND_TASK, THIRD_TASK
};
abstract_task* factory(task_type t)
{
switch (t)
{
case FIRST_TASK: return new first_task(...);
case SECOND_TASK: return new second_task(...);
case THIRD_TASK: return new third_task(...);
default: return 0;
}
}
int main()
{
task_type t = ask_user();
abstract_task* a = factory(t);
a->do_it();
delete a;
return 0;
}
请注意,唯一的开关...case 构造,即用户选择和程序流之间的链接,隐藏在工厂内部。
正如所料,模板没有精确的对等物,但是下面的模式绝对是相似的:
template <typename TASK_T>
void do_the_work(TASK_T task)
{
task.loadParameters(...);
task.run();
task.writeResult(...);
}
enum task_type
{
FIRST_TASK, SECOND_TASK, THIRD_TASK
};
void factory(task_type t)
{
first_task t1;
second_task t2;
third_task t3;
switch (t)
{
case FIRST_TASK: do_the_work(t1); break;
case SECOND_TASK: do_the_work(t2); break;
case THIRD_TASK: do_the_work(t3); break;
default: throw some_exception();
}
}
函数 do_the_work 是静态多态的一个例子。对象的用途决定了它的接口,反之亦然。语法有效的每个静态类型都可以自动使用。
这种方法提供了统一工作流的优势。只有一个函数需要调试和维护,显然,do_the_work 的三个重载会使这个好处最小化。
这是另一个例子,一个函数接受一个数组并计算所有元素的和或积。
enum compute_type { SUM, MULTIPLY };
double do_the_work(compute_type t, const double* data, size_t length)
{
switch (t)
{
case SUM:
return std::accumulate(data,data+length,0.0);
case MULTIPLY:
return std::accumulate(data,data+length,1.0,std::multiplies<double>());
default:
throw some_exception();
}
}
您希望重新编写代码,以便它从给定的文本文件中获取数字,并对所有元素执行请求的操作,并且所有计算都应该以用户提供的精度执行。
这就需要一个多层模板工厂 。粗略来说,你有 N 个函数模板。第 K 个函数有 N-K 个自变量和 K 个模板参数,它使用一个开关块将执行转移到可能的第(K+1)个函数之一。
enum result_type { SUM, MULTIPLY };
enum data_type { FLOAT, DOUBLE };
template <typename T>
T factory_LAYER3(result_type t, const std::vector<T>& data)
{
switch (t)
{
case SUM:
return std::accumulate(data.begin(),data.end(),T(0));
case MULTIPLY:
return std::accumulate(data.begin(),data.end(),T(1),std::multiplies<T>());
default:
throw some_exception();
}
}
template <typename T>
T factory_LAYER2(result_type t, std::istream& i)
{
std::vector<T> data;
std::copy(std::istream_iterator<T>(i), std::istream_iterator<T>(),
std::back_inserter(data));
return factory_LAYER3(t, data);
}
double ML_factory(result_type t, data_type d, const char* filename)
{
std::ifstream i(filename);
switch (d)
{
case FLOAT:
return factory_LAYER2<float>(t, i);
case DOUBLE:
return factory_LAYER2<double>(t, i);
default:
throw some_exception();
}
}
模板工厂中最难的设计问题通常是结果的类型。
这里的代码悄悄地利用了这样一个事实,即所有函数都返回一个可转换为 double 的结果。
7.6.类型的自动枚举
可以利用 LINE 宏创建一个易于扩展的类型集合,这些类型可以作为枚举进行访问。
考虑下面的原型——您可以简单地将一个整数索引映射到一个选择器中:
template <int N>
struct single_value : selector<false>
{
};
template <>
struct single_value<7> : selector<true> // user supplied case #1
{
};
template <>
struct single_value<13> : selector<true> // user supplied case #2
{
};
// ...
template <>
struct single_value<128> // terminator, equivalent to max size;
{ // it will be useful shortly
};
更概括地说,我们可以写:
template <>
struct single_value<7> : MyType1 // user supplied case #1
{
};
template <>
struct single_value<13> : MyType2 // user supplied case #2
{
};
事实上,single_value 是一个元函数,它映射一个整数范围,比如[0...127]为了简单起见,在类型上,它总是返回选择器
再次假设我的类型只是选择器
现在你会看到一个模板类 enum_hunter,它将个连续索引映射到用户提供的案例,这样 enum_hunter < 1 >是15single _ value<7>,enum_hunter < 2 >是 single_value < 13 >等等。
关键思想如下:
- 因为给定了默认实现,所以任何单值
都存在。 - 用户提供的专门化有其 member ::value == true。
- enum_hunter
将检查所有 single_value ,从 J==0 开始,直到找到第 n 个用户提供的值。 - enum_hunter
其实就是 enum_hunter 。 - enum_hunter
检查 single_value ::value。如果为假,则从 enum_hunter 继承。否则,它继承自 enum_hunter (除了当 N-1 为零时,您选择<0,J>,因为最终结果正好是 single_value )。 - 当 N 达到 0 时,你就完成了。您正好符合 N 个用户提供的值。如果初始 N 太大,J 会在 N 降到 0 之前到达终止符,由于终止符是一个空类,编译器会报错。
所有这些产生了一个惊人的紧凑实现(目前,忽略一切都是硬编码的事实):
template <int N, int J=0>
struct enum_hunter
: enum_hunter<N-single_value<J>::value, J+1-(N == single_value<J>::value)>
{
};
template <int J>
struct enum_hunter<0, J> : single_value<J>
{
};
template <>
struct enum_hunter<0, 0> : single_value<0>
{
};
这种框架技术可以带来几种不同的应用——最简单的是在任意(但很小)整数和类型之间构建一个稀疏的编译时数组:
#define MXT_ADD_ENUMERATION(N, TYPE) \
template <> struct single_value<N> : public TYPE, selector<true> {}
struct Mapped1
{
static double do_it() { return 3.14; }
};
struct Mapped2
{
static double do_it() { return 6.28; }
};
MXT_ADD_ENUMERATION(7, Mapped1);
MXT_ADD_ENUMERATION(13, Mapped2);
double xx1 = enum_hunter<1>::do_it(); // == 3.14
double xx2 = enum_hunter<2>::do_it(); // == 6.28
完善宏后,将 enum_hunter 的名称参数化为 enum,并将 single_value 重命名为 ENUM##_case。
#define MXT_BEGIN_ENUMERATION(ENUM) \
\
template <int N> struct ENUM##_case : static_value<int, 0> {}; \
\
template <int N, int J=0> struct ENUM \
: ENUM<N-ENUM##_case<J>::value, J+1-(N == ENUM##_case<J>::value)> {}; \
\
template <int N> struct ENUM<0, N> : ENUM##_case<N> {}; \
\
template <> struct ENUM<0, 0> : ENUM##_case<0> {}
struct empty_class {};
#define MXT_END_ENUMERATION(ENUM, K) \
template <> struct ENUM##_case<K> : {}
// we explicitly add a member “value” without using derivation.
// this allows TYPE itself to be selector<true>
#define MXT_ADD_ENUMERATION(ENUM, TYPE, K) \
template <> struct ENUM##_case<K> : TYPE \
{ static const int value = 1; }
使用宏时,begin/end 之间序列中的每个指令都将使用行号作为渐进索引自动添加。同一行上的两条指令不会被编译,因为你不能两次专门化一个类模板。
MXT_BEGIN_ENUMERATION(MyTypeEnum);
MXT_ADD_ENUMERATION(MyTypeEnum, Mapped1, 7); // this gets index 1
MXT_ADD_ENUMERATION(MyTypeEnum, Mapped2, 13); // this gets index 2
MXT_END_ENUMERATION(MyTypeEnum, 128);
所以 MyTypeEnum <1>是 Mapped1,MyTypeEnum <2>是 Mapped2,但是 MyTypeEnum_case <...>仍然是代码可用的。请注意,如果您计划通过连续索引使用枚举,则可能不需要示例中的 7 和 13。但是,您需要提供唯一的升序值。所以你可以把 LINE 作为参数 k 传递。
类型枚举的另一个应用是,与经典枚举不同,几个头可以添加它们自己的值。所以你可以在不同的文件之间“分配”一个函数。
假设您希望收集 cpp 中包含的文件列表,并且不希望每个文件头访问一个全局变量:
#include "H1.hpp"
#include "H2.hpp"
#include "H3.hpp"
int main(int argc, const char* argv[])
{
std::vector<std::string> global_list;
// here initialize global_list
}
大致的解决方案如下:
// flag_init.hpp
#define MXT_INIT_LIST
// equivalent to BEGIN_ENUMERATION
template <int N> struct flag_init
{
static void run(std::vector<std::string>& v)
{
}
};
template <int N>
void run_flag_init(std::vector<std::string>& v, static_value<int, N>)
{
flag_init<N>::run(v);
run_flag_init(v, static_value<int, N+1>());
}
// magic constant, terminator
inline void run_flag_init(std::vector<std::string>& v, static_value<int, 64>)
{
}
// H1.hpp
#ifdef MXT_INIT_LIST
// equivalent to ADD_ENUMERATION
// pick a random number < 64
template < > struct flag_init<7>
{
static void run(std::vector<std::string>& v)
{
v.push_back("hello, I am " __FILE__);
}
};
#endif
// the rest of H1.hpp, then write similarly H2 and H3
// main.cpp
#include "flag_init.hpp"
#include "H1.hpp"
#include "H2.hpp"
#include "H3.hpp"
int main(int argc, const char* argv[])
{
std::vector<std::string> global_list_of_flags;
run_flag_init(global_list_of_flags);
}
7.7.无假设代码
有时,程序逻辑可以嵌入知道该做什么的“智能对象”中,从而消除了对 if/switch 模块的需求。
7.7.1.智能常数
例如,假设您需要为日期类编写一个合适的打印函数:
class date
{
public:
int day() const;
int month() const;
int year() const;
};
enum dateformat_t
{
YYYYMMDD,
YYMMDD,
DDMMYYYY,
// many more...
};
void print(date d, dateformat_t f)
{
switch (f)
{
case YYYYMMDD:
// Very verbose...
}
}
相反,您可以编写无分支代码。像往常一样,TMP 技术利用了将信息存储在不明显可以存储有意义数据的地方的优势!
假设像 YYYYMMDD 这样的格式常量实际上是具有六个十进制数字的数字,形式为[f1 e1 f2 e2 f3 e3],其中 fi 是“要打印的日期字段”的索引(比如说,0 =年,1 =月,2 =日),ei 是数字的宽度。
例如,041222 将是“四位数的年(04),两位数的月(12),两位数的日(22),”或简称为 YYYY-MM-DD。这将使您能够编写:
const int pow10[] = { 1, 10, 100, 1000, 10000, ... };
const int data[3] = { d.year(), d.month(), d.day() };
const char* sep[] = { "-", "-", "" );
for (int i=0; i<3; ++i)
std::cout << std::setw(e[i]) << (data[f[i]] % pow10[e[i]]) << sep[i];
生成这样的常数很容易:
enum { Y, M, D };
template <unsigned F, unsigned W = 2>
struct datefield : static_value<unsigned, F*10 + (W % 10)>
{
};
template <typename T1, typename T2 = void, typename T3 = void>
struct dateformat
{
static const unsigned pow10 = 100 * dateformat<T2,T3>::pow10;
static const unsigned value = pow10 * T1::value + dateformat<T2,T3>::value;
};
template < >
struct dateformat<void, void, void>
{
static const unsigned value = 0;
static const unsigned pow10 = 1;
};
enum
{
YYYYMMDD = dateformat<datefield<Y,4>, datefield<M>, datefield<D> >::value,
DDMMYY = dateformat<datefield<D>, datefield<M>, datefield<Y> >::value,
YYYYMM = dateformat<datefield<Y,4>, datefield<M> >::value,
// ...
};
为了简单起见,这个实现只对三个参数使用旋转。 16
打印功能如下:
void print(date d, dateformat_t f)
{
const unsigned pow10[] = { 1, 10, 100, 1000, 10000, ... };
const int data[3] = { d.year(), d.month(), d.day() };
for (unsigned int fc = f; fc != 0; fc /= 100)
{
unsigned w = fc % 10;
unsigned j = (fc % 100) / 10;
std::cout << std::setw(w) << (data[j] % pow10[w]);
}
}
7.7.2.将枚举转换为字符串
与上一段类似,您可以在枚举值中编码一个短字符串。C++ 标准保证枚举 由一个大的无符号整数表示,如果任何值很大的话。实际上,您可以假设枚举将是一个 64 位整数。由于 2 64 > 40 12 ,可以将一个长度为 12 的字符串作为一个整数存储在 base 40 中,其中 A=1,B=2,以此类推。
首先你定义“字母表”:
template <char C> struct char2int;
template <size_t N> struct int2char;
#define C2I(C, I) \
template <> struct char2int<C> { static const size_t value = I; }
#define I2C(C, I) \
template <> struct int2char<I> { static const char value = C; }
#define TRANSLATE1(C1, N) \
C2I(C1, N); I2C(C1, N)
#define TRANSLATE2(C1, C2, N) \
C2I(C1, N); C2I(C2, N); I2C(C1, N)
TRANSLATE2('a', 'A', 1); // convert both ‘A’ and ‘a’ to 1, and 1 to ‘a’
TRANSLATE2('b', 'B', 2);
// ...
TRANSLATE2('z', 'Z', 26);
TRANSLATE1('0', 27);
TRANSLATE1('1', 28);
// ...
TRANSLATE1('9', 36);
TRANSLATE1('_', 37);
static const size_t SSTRING_BASE = 40;
template <size_t N, bool TEST = (N<SSTRING_BASE)>
struct verify_num
{
static const size_t value = N;
};
template <size_t N>
struct verify_num<N, false>
{
// this will not compile if a number >= 40 is used by mistake
};
template <char C1, char C2 = 0, ..., char C12 = 0>
struct static_string
{
static const size_t aux
= verify_num< char2int<C1>::value >::value;
static const size_t value
= aux + static_string<C2,...,C12>::value * SSTRING_BASE;
};
template <>
struct static_string<0>
{
static const size_t value = 0;
};
template <size_t VALUE>
std::string unpack(static_value<size_t, VALUE>)
{
std::string result(1, char(int2char<VALUE % SSTRING_BASE>::value));
return result + unpack(static_value<size_t, VALUE/SSTRING_BASE>());
}
std::string unpack(static_value<size_t, 0>)
{
std::string result;
return result;
}
#define MXT_ENUM_DECODER(TYPE) \
template <TYPE VALUE> \
std::string decode() \
{ return unpack(static_value<size_t, VALUE>()); }
请注意,您将通用代码从“实现”中分离出来。现在定义一个枚举:
enum MyEnum
{
first = static_string<'f','i','r','s','t'>::value,
verylong = static_string<'v','e','r','y','l','o','n','g'>::value
};
MXT_ENUM_DECODER(MyEnum); // Write this to get a “decode” function
std::cout << decode<first>(); // prints “first”
为了简单起见,这个例子实现了静态解码(也就是说,解码后的枚举值在编译时是已知的)。然而,相同的操作可以在运行时执行。 17
通常,当枚举的实际值对程序没有意义时,这种技术是有效的。
7.7.3.自修改功能表
考虑一个循环容器的小例子,其中元素被“推回”(此时,假设任何东西都是公共的):
template <typename T, size_t N>
struct circular_array
{
T data_[N];
size_t pos_;
circular_array()
: data_(), pos_(0)
{
}
void push_back(const T& x)
{
data_[pos_] = x;
if (++pos_ == N)
pos_ = 0;
}
};
你可以将 push_back 转换成一种自我修改的函数,类似于蹦床(参见第 5.3.1 节)。您将使用一个用合适的函数模板初始化的函数指针。
template <typename T, size_t N>
struct circular_array
{
T data_[N];
typedef void (*push_back_t) (circular_array<T, N>& a, const T& x);
push_back_t pb_;
template <size_t K>
struct update_element_at
{
static void apply(circular_array<T, N>& a, const T& x)
{
a.data_[K] = x;
a.pb_ = &update_element_at<(K+1) % N>::apply;
}
};
circular_array()
: data_(), pb_(&update_element_at<0>::apply)
{
}
void push_back(const T& x)
{
pb_(*this, x);
}
};
这个模式的关键点是你有一个函数集合,其中所有的元素都知道接下来的动作,所以它们可以用这个信息更新一个指针。
更新函数指针不是强制性的。函数可以选择自己作为下一个候选者。假设您更改容器策略,以便保留前 N-1 个元素,然后不断覆盖最后一个元素:
if ((K+1)<N)
a.pb_ = &update_element_at<K+1>::apply;
自我修改功能通常是优雅的,但效率略低于经典开关,主要是因为缓存或程序流预测器等技术因素。
应用程序包括这样的数据结构,其在初始化期间的行为是不同的(“预热”阶段),直到插入了最小数量的元素。
注 ICF ,全同代码折叠,是编译器普遍应用的优化技术。简单地说,链接器将试图寻找“重复的函数”,并且只生成一次二进制代码。比如 vector < int* >和 vector < double* >很可能会生成相同的代码,所以可以合并。
虽然这减小了二进制文件的大小,但它有副作用,即函数指针的相等性可能不会像预期的那样工作。如果 F 和 G 是相同的(假设它们有一个空体),有可能 F!在调试版本中= G,在 ICF 优化版本中 F == G。
当编写依赖于函数指针的相等/不相等的自修改函数时要小心(显然,与空指针的比较很好)。
1 通常,这需要额外的假设,即 a、b 和 c 指向不相关的内存区域,但现代编译器会试图了解这些优化是否可以安全地应用。
2 这就是通常所说的代码膨胀。
3 有 26 个 N 个的 N 个字母的序列,而“唯一”表示 2 个 64 个不同的 hash 值,所以对于 N 个> 14,没有 hash 可以内射;然而,一个好的散列算法将“分散”冲突,所以具有相同散列的字符串将真正不同。
4 作为一个练习,读者可能会将这个想法推广到 wchar_t,它在这个实现中总是选择基于地区的函数集。
当你在段落的后面看到一个字符串散列的应用时,动机就很明显了。
6 不需要完整的拟态实现:不需要强制转换操作符。
7 这个语法将只在本节中使用,在这里没有混淆的可能。
8 另一个常见错误是论证交叉。假设类 C 有两个模板参数 T1 和 T2。如果你部分专门化了 C < T1,Y >和 C < X,T2 >对于某些固定的 X 和 Y,C < X,Y >是二义性的,所以也必须显式专门化。
9 这个问题其实属于不透明型原理。如果一个函数的返回类型是“复杂的”,你应该向用户发布一个方便的 typedef 或者允许他们通过忽略它的类型来使用这个对象(更多细节参见第九章)。
10 原则上,添加一个从 dynamic_hash_result 到 std::pair < size_t,iterator_t >的转换操作符是合理的。
11 因为类中只有一个函数,所以这个例子不是从 static_interface 派生的,而是复制了代码。
12 这是一个在线的问题。在离线问题中,所有的输入值同时给出。大卫·埃普斯坦(见www.ics.uci.edu/~eppstein/pubs/kbest.html
)提出了一种数据结构,它使用与 N 成比例的内存解决了在线问题,并展示了分摊常数时间操作。这个例子关注于如何改进一个简单的实现,而不是创建一个有效的算法。
13 在这里,更新及其辅助子程序都是全局函数。这使得演示更加容易,因为它允许您一次只关注一个特性。您可以安全地将所有这些函数声明为容器的私有静态成员。
14 一个包含 10.000.000 个插入和 N < 32 的基本压力测试显示了“正常”和“极端”发布版本之间非常大的运行时差异(30–40%)。贪婪算法和紧凑代码利用了技术因素,例如处理器缓存。
15 是的意思的来源。
16 所以你不能生成像 YYYYMMDDYY 这样的模式。
17 提示:使用长度为 SSTRING_BASE 的 char 的 const 数组并用{ 0,int 2 char<1>:::value,int 2 char<2>:::value 初始化...}.*
6# 八、函子
这一章重点介绍了几种在编写(或不编写)函子时有帮助的技术。
大多数 STL 算法需要编译时函数对象,这通常需要一些手动编码:
struct Person
{
unsigned int age;
std::string home_address;
double salary() const;
};
std::vector<Person> data;
std::sort(data.begin(), data.end(), /* by age */ );
std::partition(data.begin(), data.end(), /* by salary */ );
如果可以修改 Person,有时一个优雅而快捷的解决方案是编写一个公共静态成员函数 和一个成员仿函数。这同时实现了最高的效率和控制,因为您的代码可以访问私有成员:
struct Person
{
private:
unsigned int age;
public:
static bool less_by_age(const Person& a, const Person& b)
{
return a.age < b.age;
}
struct BY_AGE
{
bool operator()(const Person& a, const Person& b) const
{
return Person::less_by_age(a, b);
}
};
};
std::vector<Person> data;
std::sort(data.begin(), data.end(), Person::less_by_age); // suboptimal
std::sort(data.begin(), data.end(), Person::BY_AGE()); // good
静态成员函数可以访问私有数据。然而,编译器内联比较要困难得多,所以函子通常更好。
您甚至可以提取一些代码,将前者转换为后者:
template <typename T, bool (*LESS)(const T&, const T&)>
struct less_compare_t
{
typedef T first_argument_type;
typedef T second_argument_type;
typedef bool result_type;
bool operator()(const T& x, const T& y) const
{
return LESS(x, y);
}
};
struct Person
{
private:
unsigned int age;
public:
static bool less_by_age(const Person& a, const Person& b)
{
return a.age < b.age;
}
typedef less_compare_t<Person, Person::less_by_age> BY_AGE;
};
选择函数/仿函数的名称是为了使表达式在实例化时清晰,而不是在定义时清晰。
注意,非泛型函子(其参数具有固定类型)通常是该类的成员。
假设函子可以自由复制并通过值传递,这通常是合理的。如果一个函子需要许多数据成员,你最好把它们收集在一个单独的结构中,只存储一个引用。函子的调用方将负责保存额外的信息:
struct information_needed_to_sort_elements
{
// ...
};
class my_less
{
const information_needed_to_sort_elements& ref_;
public:
explicit functor(const information_needed_to_sort_elements& ref)
: ref_(ref)
{
}
bool operator()(const Person& p1, const Person& P2) const
{ ... }
};
int main()
{
information_needed_to_sort_elements i;
// build a suitable container data...
std::sort(data.begin(), data.end(), my_less(i));
}
STL 算法不提供任何关于函数对象副本数量的保证。
另一个有趣的特性是函子静态类型是不相关的,因为它总是被推导出来的。如果函子是从一个函数返回的,它将被立即使用(见 4.3.4 节);如果它被传递给一个函数模板,它将绑定到一个接受任何东西的参数。
这允许客户端在调用点生成复杂函数对象的匿名实例:
i = std::find_if(begin, end, std::bind2nd(std::less<double>(), 3.14));
// the exact type of the functor is irrelevant
// since find_if has an argument that binds to anything:
// template <typename I, typename F>
// I find_if(I begin, I end, F func)
注意 C++0x 包括对 lambda 对象创建的支持。
这是一种新的语法,可以传递花括号中的匿名“代码片段”,就像它们是函子一样。这减轻了名称污染的问题。换句话说,没有必要给一个不被重用的实体命名。
更多详情见第 12.4 节。
8.1.强函子和弱函子
有些函子是强类型的。这意味着用户在确定模板参数时固定函数调用的参数。所有标准泛函都是强类型的。
template <typename T>
struct less
{
bool operator()(const T& lhs, const T& rhs) const
{
return lhs < rhs;
}
};
std::sort(data.begin(), data.end(), less<Person>());
或者,您可以有一个弱 仿函数,它可以更自由地接受参数 1 :
struct weak_less
{
template <typename T>
bool operator()(const T& lhs, const T& rhs) const
{
return lhs < rhs;
}
};
std::sort(data.begin(), data.end(), weak_less());
强类型函子静态阻塞所有与 T 不兼容的类型,但由于这仅限于接口,它实际上可以与弱函子共享实现:
template <typename T>
struct less : private weak_less
{
bool operator()(const T& lhs, const T& rhs) const
{
return static_cast<const weak_less&>(*this)(lhs, rhs);
}
};
8.2.仿函数合成工具
STL 提供了组合函子和值的工具。例如,std::bind2nd 将一个二元运算和一个操作数转换为一元函数。通常,您需要执行相反操作的工具。
by age 中的前缀 by 实际上是一个二元关系与一个存取子的复合。年龄提取一个人的年龄,并比较两个年龄。这里有一个抽象这个组合概念的最小实现。
template <typename functor_t>
class by_t
{
functor_t f_;
public:
by_t(functor_t f)
: f_(f)
{}
template <typename argument_t>
bool operator()(const argument_t& a, const argument_t& b) const
{
return f_(a) < f_(b);
}
};
template <typename functor_t>
inline by_t<functor_t> by(const functor_t& f)
{
return f;
}
// see Section 1.1.4
template <typename R, typename A>
inline by_t<R (*)(A)> by(R (*f)(A))
{
return f;
}
struct age_t
{
unsigned int operator()(const Person& p) const
{
return p.age;
}
age_t(int = 0)
{
}
};
static const age_t AGE = 0<sup class="calibre7">2</sup>;
int main()
{
std::vector<Person> data;
std::sort(data.begin(), data.end(), by(AGE));
}
by 是一个仿函数合成工具。由于它对 functor_t 没有任何要求,所以它会接受合适的静态成员函数,如果 Person::age 是私有的,这就很方便了:
struct Person
{
private:
unsigned int age;
public:
static int AGE(const Person& p)
{
return p.age;
}
};
std::sort(data.begin(), data.end(), by(Person::AGE)); // ok!
函子/访问器可能被赋予强大的λ语义。
这里是第 9.2 节的另一个预览。在伪直观表示法中,比较器(A,S)是一个谓词,如果 A(O)比 S“小”,则它在对象 O 上返回 true。“小”是一个通用的二元谓词。
template
<
typename scalar_t,
typename accessor_t,
template <typename T> class less_t
>
class comparator
{
scalar_t x_;
accessor_t a_;
public:
comparator(scalar_t x, accessor_t a = accessor_t())
: x_(x), a_(a)
{
}
template <typename argument_t>
bool operator()(const argument_t& obj) const
{
less_t<scalar_t> less_;
return less_(a_(obj), x_);
}
};
使用模板-模板参数 代替普通的二元谓词,可以避免两次键入 scalar_t,并使匿名实例非常清晰易读:
comparator<double, SALARY, std::greater>(3.0)
另一个小问题是类的布局:x_ 是在 a_ 之前声明的,因为 a_ 通常是无状态的,因此是一个小对象。x_ 可能有更强的对齐约束。
现在,您可以向仿函数添加运算符,并将其提升为 lambda 谓词 3 :
struct age_t
{
int operator()(const Person& a) const
{
return a.age;
}
template <typename T>
comparator<T,age_t,std::less> operator<(const T& x) const
{
return comparator<T,age_t,std::less>(x, *this);
}
template <typename T>
comparator<T,age_t,std::equal_to> operator==(const T& x) const
{
return comparator<T,age_t,std::equal_to>(x, *this);
}
};
std::partition(data.begin(), data.end(), Person::AGE < 35);
std::partition(data.begin(), data.end(), Person::AGE == 18);
只需一点努力,您就可以向链接操作符添加更多的语法技巧:
const selector<true> INCREASING;
const selector<false> DECREASING;
template <typename T>
bool oriented_less(const T& x, const T& y, selector<true>)
{
return x<y;
}
template <typename T>
bool oriented_less(const T& x, const T& y, selector<false>)
{
return y<x;
}
oriented_less 可以翻转操作符< and simulate operator>。
template <typename functor_t, bool ASCENDING = true>
class by_t
{
functor_t f_;
public:
by_t(functor_t f) : f_(f) {}
template <typename argument_t>
bool operator()(const argument_t& a, const argument_t& b) const
{
return oriented_less(f_(a), f_(b), selector<ASCENDING>());
}
// inversion operators:
by_t<functor_t, true> operator+() const
{
return f_;
}
by_t<functor_t, false> operator-() const
{
return f_;
}
};
最后,还有另一个辅助函数 :
template <bool DIRECTION, typename functor_t>
by_t<functor_t, DIRECTION> by(selector<DIRECTION>, const functor_t& v)
{
return by_t<functor_t, DIRECTION>(v);
}
所有这些都允许写作:
std::sort(data.begin(), data.end(), +by(Person::AGE));
std::sort(data.begin(), data.end(), -by(Person::AGE));
std::sort(data.begin(), data.end(), by(DECREASING, Person::AGE));
注意我选择运算符+和运算符-是因为 by 处理的是数字属性;一元谓词的逻辑倒置最好用运算符来表达!
此外,第 2 行和第 3 行是相同的。挑最清晰的只是风格问题。
对 by_t 的最后一个改进是在 operator() 中执行严格的类型检查。
函数调用操作符几乎接受任何东西,所以更多的类型检查将会捕获那些仅仅是偶然编译的代码所产生的错误:
std::vector<Animal> data;
std::sort(data.begin(), data.end(), by(Person::AGE));
一种方便的方法是利用函子的合作。如果 functor_t 有一个成员 argument_type,它也将是一个强运算符()的参数。否则,使用弱函数调用操作符。
通常,您将决策隐藏在模板参数 中,并提供两个局部专门化。首先,一些特质:
template <typename T>
struct argument_type_of
{
typedef typename T::argument_type type;
};
template <typename A, typename R>
struct argument_type_of<R (*)(A)>
{
typedef A type;
};
template <typename A, typename R>
struct argument_type_of<R (*)(const A&)>
{
typedef A type;
};
template <typename T>
struct has_argument_type
: selector<[[ true if T::argument_type exists<sup class="calibre7">4</sup> ]]>
{
};
template <typename A, typename R>
struct has_argument_type<R (*)(A) >
: selector<true>
{
};
// ...
第一个专门化执行严格的类型检查。
template
<
typename functor_t,
bool ASCENDING = true,
bool STRICT_CHECK = has_argument_type<functor_t>::value
>
struct by_t;
template <typename functor_t, bool ASCENDING>
struct by_t<functor_t, ASCENDING, true>
{
// ...
typedef typename argument_type_of<functor_t>::type argument_type;
// note: strong argument type
bool operator()(const argument_type& a, const argument_type& b) const
{
return oriented_less(f_(a), f_(b), selector<ASCENDING>());
}
};
template <typename functor_t, bool ASCENDING>
struct by_t<functor_t, ASCENDING, false>
{
// ...
// note: weak argument type. This will accept anything
template <typename argument_t>
bool operator()(const argument_t& a, const argument_t& b) const
{
return oriented_less(f_(a), f_(b), selector<ASCENDING>());
}
};
为了最大限度地减少代码重复,您在模板库中提取出函数调用操作符,并使用 static_cast,如 CRTP 所示:
template <typename functor_t, bool ASCENDING = true>
struct by_t;
template <typename functor_t, bool ASCENDING, bool STRICT_CHECK>
struct by_base_t;
template <typename functor_t, bool ASCENDING>
struct by_base_t<functor_t, ASCENDING, true>
{
const functor_t& f() const
{
typedef by_t<functor_t, ASCENDING> real_type;
return static_cast<const real_type&>(*this).f_;
}
typedef typename argument_type_of<functor_t>::type argument_type;
bool operator()(const argument_type& a, const argument_type& b) const
{
return oriented_less(f()(a), f()(b), selector<ASCENDING>());
}
};
template <typename functor_t, bool ASCENDING>
struct by_base_t<functor_t, ASCENDING, false>
{
const functor_t& f() const
{
typedef by_t<functor_t, ASCENDING> real_type;
return static_cast<const real_type&>(*this).f_;
}
template <typename argument_t>
bool operator()(const argument_t& a, const argument_t& b) const
{
return oriented_less(f()(a), f()(b), selector<ASCENDING>());
}
};
template <typename functor_t, bool ASCENDING = true>
struct by_t
: by_base_t<functor_t,ASCENDING,has_argument_type<functor_t>::value>>
{
// ...
};
8.3.内部模板函子
仿函数包装器可以用作接口杠杆工具。
从语法上讲,您利用了内部类模板知道外部类的模板参数这一事实。
8.3.1.函数到函子的转换
为简单起见,假设您有一个函数集合 ,具有相似的签名 T f(T,T,...T),其中参数的数量是变化的。进一步假设要执行的函数列表在运行时是已知的,那么您需要一个带有虚拟调用的基类,它的唯一签名可以是(const T*,size_t)。 5
让我们寻找一种自动执行转换的方法:
template <typename T>
struct base
{
virtual T eval(const T*, size_t) const = 0;
virtual ~base() {}
};
给定一个函数,比如 double F(double,double),你可以把它嵌入到一个函子中,但是你必须同时推导出 T 和 F:
template <typename T, T (*F)(T,T)>
struct functor : public base<T>
{
// ...
};
实际上,在 F 之前需要 T,所以可以只在 T 上构建一个类模板,然后是一个内部模板类:
template <typename T>
struct outer
{
template <T (*F)(T,T)>
struct inner : public base<T>
{
首先你识别外部
template <typename T>
struct function_call_traits
{
template <T (*F)()>
struct eval_0 : public base<T>
{
virtual T eval(const T* , size_t) const { return F(); }
};
template <T (*F)(T)>
struct eval_1 : public base<T>
{
virtual T eval(const T* x, size_t) const { return F(x[0]); }
};
template <T (*F)(T, T)>
struct eval_2 : public base<T>
{
virtual T eval(const T* x, size_t) const { return F(x[0], x[1]); }
};
// ...
template <T (*F)()>
eval_0<F>* get_ptr() const
{
return new eval_0<F>;
}
template <T (*F)(T)>
eval_1<F>* get_ptr() const
{
return new eval_1<F>;
}
template <T (*F)(T, T)>
eval_2<F>* get_ptr() const
{
return new eval_2<F>;
}
// ...
};
template <typename T>
inline function_call_traits<T> get_function_call(T (*F)())
{
return function_call_traits<T>();
}
template <typename T>
inline function_call_traits<T> get_function_call(T (*F)(T))
{
return function_call_traits<T>();
}
template <typename T>
inline function_call_traits<T> get_function_call(T (*F)(T, T))
{
return function_call_traits<T>();
}
// ...
#define MXT_FUNCTION_CALL_PTR(F) get_function_call(F).get_ptr<F>()
请注意:
- f 使用了两次,第一次作为指针,第二次作为模板参数。
- get_ptr 函数不是静态的,看起来可能有点奇怪,这是一个实际上要实例化的 traits 类的例子(但是匿名使用)。
double add0()
{
return 6.28;
}
double add1(double x)
{
return x+3.14;
}
double add2(double x, double y)
{
return x+y;
}
int main()
{
double x[5] = {1,2,3,4,5};
base<double>* f[3] =
{
MXT_FUNCTION_CALL_PTR(add0),
MXT_FUNCTION_CALL_PTR(add1),
MXT_FUNCTION_CALL_PTR(add2)
};
for (int i=0; i<3; ++i)
std::cout << f[i]->eval(x, 5);
// normal destruction code has been omitted for brevity
}
前面的示例通过调用同一个接口来执行 add0()、add1(x[0])和 add2(x[0],x[1])。
8.3.2.成员 到函子的转换
上一节中看到的相同技术可以将指针转换成函子。 6
在 C++ 中,没有访问限制的简单结构通常用于传输小块数据。理想情况下,您会希望保持这种简单性,并且能够编写没有开销的代码:
struct Person
{
unsigned int age;
double salary() const;
};
std::vector<Person> data;
// warning: pseudo-c++
std::sort(data.begin(), data.end(), by(Person::age));
std::sort(data.begin(), data.end(), by(Person::salary));
因为可以使用指向成员的指针作为模板参数,所以编写一个辅助包装器并不太难。不幸的是,实例化太冗长,没有用。
template <typename from_t, typename to_t, to_t from_t::* POINTER>
struct data_member
{
const to_t& operator()(const from_t& x) const
{
return x.*POINTER;
}
};
template <typename from_t, typename to_t, to_t (from_t::*POINTER)() const>
struct property_member
{
to_t operator()(const from_t& x) const
{
return (x.*POINTER)();
}
};
struct TEST
{
int A;
int B() const { return -A; }
};
TEST data[3] = {2,1,3};
// very verbose...
std::sort(data, data+3, by(data_member<TEST, int, &TEST::A>()));
std::sort(data, data+3, by(property_member<TEST, int, &TEST::B>()));
然而,不可能编写一个泛型类指针作为唯一的模板参数:
template <typename A, typename B, B A::*POINTER>
struct wrapper<POINTER> // illegal: not c++
您必须再次求助于嵌套的类模板:
template <typename from_t, typename to_t>
struct wrapper
{
template <to_t from_t::*POINTER> // legal!
struct dataptr_t
{
const to_t& operator()(const from_t& x) const
{
return x.*POINTER;
}
};
template <to_t from_t::*POINTER>
dataptr_t<POINTER> get() const
{
return dataptr_t<POINTER>();
}
};
template <typename from_t, typename to_t>
wrapper<from_t, to_t> get_wrapper(to_t from_t::* pointer)
{
return wrapper<from_t, to_t>();
}
该示例包括一个函数,该函数使用指针执行第一次演绎,同样,您必须提供两次相同的指针,一次在运行时(其值基本上被忽略,但其类型用于演绎),另一次在编译时:
#define MEMBER(PTR) get_wrapper(PTR).get<PTR>()
- get_wrapper 自动从 PTR 中推导出参数 T1 和 T2,所以 get_wrapper(PTR)将返回包装器
。 - 然后您要求这个包装器在 PTR 上再次实例化它的成员函数 get,它返回正确的对象。
如果 PTR 有 int TEST:😗,这个宏将产生一个 dataptr_t
但是,任何其他重载都可以。这是一个扩展版本:
template <typename from_t, typename to_t>
struct wrapper
{
template <to_t from_t::* POINTER>
struct dataptr_t
{
// optional:
// typedef from_t argument_type;
const to_t& operator()(const from_t& x) const
{
return x.*POINTER;
}
};
template <to_t (from_t::*POINTER)() const>
struct propptr_t
{
// optional:
// typedef from_t argument_type;
to_t operator()(const from_t& x) const
{
return (x.*POINTER)();
}
};
template <to_t from_t::* POINTER>
dataptr_t<POINTER> get() const
{
return dataptr_t<POINTER>();
}
template <to_t (from_t::*POINTER)() const>
propptr_t<POINTER> get() const
{
return propptr_t<POINTER>();
}
};
template <typename from_t, typename to_t>
wrapper<from_t, to_t> get_wrapper(to_t from_t::* pointer)
{
return wrapper<from_t, to_t>();
}
template <typename from_t, typename to_t>
wrapper<from_t, to_t> get_wrapper(to_t (from_t::*pointer)() const)
{
return wrapper<from_t, to_t>();
}
#define mxt_create_accessor(PTR) get_wrapper(PTR).get<PTR>()
struct TEST
{
int A;
int B() const { return -A; }
};
TEST data[3] = {2,1,3};
std::sort(data, data+3, by(mxt_create_accessor(&TEST::A)));
std::sort(data, data+3, by(mxt_create_accessor(&TEST::B)));
和往常一样,如果类名中包含逗号(比如 std::map
&不是绝对必要的。可以将宏重新定义为 get_wrapper(PTR)。get ()以便在普通限定名上调用它。
根据标准,宏不能在模板中工作。编译器需要一个额外的 template 关键字来正确推断 get 是什么,所以最好的选择是定义第二个名为(比方说)的宏
mxt_create_accessor_template
get_wrapper(PTR).template get<&PTR>()
每当 PTR 依赖于对宏展开的行有影响的模板参数时,就需要使用这个版本。另一方面,当 PTR 不依赖于任何其他东西时,它是被禁止的。 7
8.3.3.关于双层包装技术的更多信息
在上一段中,您看到了一个类似如下的宏:
#define MEMBER(PTR) get_wrapper(PTR).get<PTR>()
参数 PTR 使用了两次——第一次作为模板函数的参数,忽略其值,只使用其类型,返回一个“中间函子”;第二次作为仿函数本身的模板参数,它产生你需要的最终对象。
让我们修改这个技巧,来面对一个明显不相关的问题。 8 在经典 C++ 中,枚举值自动衰减为整数。这可能会导致错误:
enum A { XA = 1 };
enum B { XB = 1 };
int main()
{
A a = XA;
B b = XB;
a == b; // compiles and returns true, even if enums are unrelated
}
让我们介绍一个简单的帮助函子:enum_const 类型的对象是一个静态值,它与来自相同(非匿名)枚举的一个值完全相等,但不能与整数或不同类型进行比较。
template <typename T, T VALUE>
struct enum_const
{
bool operator==(T that) const
{
return VALUE == that;
}
// Barton-Nackman, see section 6.6
friend inline bool operator==(T lhs, enum_const<T, VALUE> rhs)
{
return rhs == lhs;
}
};
template <typename T>
struct enum_const_helper
{
template <T VALUE>
enum_const<T, VALUE> get() const
{
return enum_const<T, VALUE>();
}
};
template <typename T>
inline enum_const_helper<T> wrap(T)
{
return enum_const_helper<T>();
}
所以你可以写这样的代码:
#define enum_static_const(X) wrap(X).get<X>()
int main()
{
A a = XA;
B b = XB;
a == b; // ok
b == enum_static_const(XA); // error
enum_static_const(XB) == a; // error
}
error: invalid operands to binary expression ('int' and 'enum_const<A, (A)1U>')
b == enum_static_const(XA); // fails
~ ^ ~~~~~~~~~~~~~~~~~~~~~
note: candidate template ignored: deduced conflicting types for parameter 'T' ('B' vs. 'A')
inline bool operator==(T lhs, enum_const<T, VALUE> rhs)
^
error: invalid operands to binary expression ('enum_const<B, (B)1U>' and 'int')
enum_static_const(XB) == a; // fails
~~~~~~~~~~~~~~~~~~~~~ ^ ~
note: candidate function not viable: no known conversion from 'A' to 'B' for 1st argument;
bool operator==(T that) const
编写的宏可以工作,但是它需要 X 是一个编译时常数:
#define enum_static_const(X) wrap(X).get<X>()
让我们寻找一个解决方法。第一个问题是,wrap 能检测出 X 是常数还是变量吗?它可以部分地—一个变量可以绑定到一个引用。 9
template <typename T>
inline enum_const_helper<T> wrap(T, ...)
{
return enum_const_helper<T>();
}
template <typename T>
inline enum_var_helper<T> wrap(T& x, int)
{
return enum_var_helper<T>(x);
}
注意包装的附加参数。假设 X 是一个变量,你写 wrap(X);wrap(T &)和 wrap(T)都是有效匹配,因此重载决策是不明确的。另一方面,表达式 wrap(X,0)在可能的情况下会倾向于匹配(T &,int),因为 0 正好有 int 类型(这比省略号好)。所以宏变成了:
#define enum_static_const(X) wrap(X, 0).get<X>()
第二个问题是,如果 X 是一个变量,能否给一个意义得到
同样,让我们引入一个 int 类型的伪参数:
template <typename T>
struct enum_const_helper
{
template <T VALUE>
enum_const<T, VALUE> get(int) const
{
return enum_const<T, VALUE>();
}
};
这是宏的最终版本:
#define enum_static_const(X) wrap(X, 0).get<X>(0)
现在语法不同了:get 可能是成员对象,get
下面是缺失的一段代码:
template <typename T>
struct enum_var
{
const T value_;
explicit enum_var(T val)
: value_(val) {}
bool operator==(T that) const
{
return value_ == that;
}
// Barton-Nackman again
friend inline bool operator==(T lhs, enum_var<T> rhs)
{
return rhs == lhs;
}
enum_var operator<(T) const // dummy operator<
{ return *this; }
enum_var operator>(int) const // dummy operator>
{ return *this; }
};
template <typename T>
struct enum_var_helper
{
enum_var<T> get; // surprise: data member called get
enum_var_helper(T& x)
: get(x) {}
};
enum_static_const(XB) == b; // picks enum_const<B,1>::operator==(b)
enum_static_const(b) == XB; // picks enum_var<B>(b).operator==(XB)
8.4.累积
累加器 是一个函子,它对一系列元素执行逻辑“传递”,并通过 operator+=或 operator+更新。这在 STL 算法 std::accumulate 中实现。
template <typename iterator_t, typename accumulator_t>
accumulator_t accumulate(iterator_t b, iterator_t e, accumulator_t x)
{
while (b != e)
x = x + *(b++);
return x;
}
如果 x 是 value_type(0),这实际上产生了范围上的和。
累加器可以分为在线的和离线的。脱机对象在一个范围内只能累积一次,不能再添加更多的值。另一方面,在线对象可以累积不相交的范围。(普通总和是一个在线累加过程,因为新的总和只取决于以前的总和以及新的值。精确的百分位数将是一个离线过程,因为两个不相交范围内的第 P 个百分位数同时取决于所有的值。 10 )
概括的第一步是累加 F(i),不一定是i. 11
template <typename T>
struct identity
{
T operator()(T x) const { return x; }
};
template <typename iter_t, typename accumulator_t, typename accessor_t>
accumulator_t accumulate(iter_t b, iter_t e, accumulator_t x, accessor_t F)
{
while (b != e)
x = x + F(*(b++));
return x;
}
template <typename iter_t, typename accumulator_t>
accumulator_t accumulate(iterator_t b, iterator_t e, accumulator_t x)
{
return accumulate(b, e, x,
identity<typename std::iterator_traits<iter_t>::reference>());
}
使用 TMP,可以动态构建多层累加器 :
- 识别一组相似的操作,这些操作通过同时执行而不是顺序执行来提高性能。 12
- 为实例化一个未命名的多重累加器定义一个合理的语法。
- 为提取结果定义一个合理的语法。
8.4.1.逐步实施
本节的其余部分将编写一个合适的名为 collect 的函数,该函数可以编写以下内容:
// collect F(*i) for each i in the range
// and produce sum, gcd and max
std::accumulate(begin, end, collect(F)*SUM*GCD*MAX)
您将利用 std::accumulate 返回累加器来一次转储一个或多个所需结果的事实:
int data[7] = { ... };
int S = std::accumulate(data, data+7, collect(identity<int>())*SUM).result(SUM);
int sum, gcd, max;
std::accumulate(begin, end, collect(F)*SUM*GCD*MAX)
.result(SUM >>sum, GCD >>gcd, MAX >>max);
让我们从头开始。
首先,确定基本操作并给每个操作分配一个代码:
enum
{
op_void, // null-operation
op_gcd,
op_max,
op_min,
op_sum
};
同样,您将使用模板旋转。主对象包含操作列表;它首先执行第一个,然后循环列表并调度执行。t 是访问器。
template <typename T, int O1 = op_void, int O2 = op_void,..., int On = op_void>
class accumulate_t
{
typedef accumulate_t<T, O2, O3, ..., On > next_t; // rotation
static const int OP_COUNT = 1+next_t::OP_COUNT;
scalar_t data_[OP_COUNT];
static void apply(/* ... */)
{
// perform operation O1 and store result in data_[0]
// then...
next_t::apply(...);
}
};
然后实现二元运算(为简洁起见,省略了一些代码):
template <int N>
struct op_t;
template <>
struct op_t<op_void>
{
private:
explicit op_t(int = 0) {}
};
template <>
struct op_t<op_sum>
{
explicit op_t(int = 0) {}
template <typename scalar_t>
scalar_t operator()(const scalar_t a, const scalar_t b) const
{
return a+b;
}
};
你创建一些全局常量对象;显式构造函数正是出于这个目的。
const op_t< op_gcd > GCD(0);
const op_t< op_sum > SUM(0);
const op_t< op_max > MAX(0);
const op_t< op_min > MIN(0);
注意没人能构造 op_t <op_void>。</op_void>
因为您可以执行四个不同的操作,所以您将四个作为模板参数的限制:
template
<
typename accessor_t,
int O1 = op_void, int O2 = op_void, int O3 = op_void, int O4 = op_void
>
class accumulate_t
{
typedef typename accessor_t::value_type scalar_t;
typedef accumulate_t<accessor_t,O2,O3,O4> next_t;
template <typename T, int I1, int I2, int I3, int I4>
friend class accumulate_t;
static const int OP_COUNT = 1 + next_t::OP_COUNT;
scalar_t data_[OP_COUNT];
size_t count_;
accessor_t accessor_;
每个对象都是通过访问器的一个实例构造的:
public:
accumulate_t(const accessor_t& v = accessor_t())
: accessor_(v), count_(0), data_()
{
}
// more below...
};
您有一个名为 data_ 的结果数组。第 I 个操作会将其结果存储在 data_[i]中。
递归计算部分确实很简单。有一个公共操作符+ =调用私有静态成员函数:
template <typename object_t>
accumulate_t& operator+=(const object_t& t)
{
apply(data_, accessor_(t), count_); // <-- static
return *this;
}
和一个全局运算符+:
template <typename accessor_t, int N1, ..., int N4, typename scalar_t>
accumulate_t<accessor_t,N1,N2,N3,N4>
operator+(accumulate_t<accessor_t,N1,N2,N3,N4> s, const scalar_t x)
{
return s += x;
}
accessor_(t)得出要在存储单元*data 上累加的值。如果 count 为 0,这意味着单元格是“空的”,只需写入值。否则,调用合并前一个单元格值和新值的第一个二元运算。然后,将指针前进到下一个单元格,并将调用转发到 next_t:
static void apply(scalar_t* const data, const scalar_t x, size_t& count)
{
*data = (count>0) ? op_t<O1>()(*data, x) : x;
next_t::apply(data+1, x, count);
}
当所有操作都是 op_void 时,递归停止。此时,您更新了计数器。
template <typename accessor_t>
class accumulate_t <accessor_t, op_void, op_void, op_void, op_void>
{
/* ... */
static const int OP_COUNT = 0;
static void apply(scalar_t* const, const scalar_t, size_t& count)
{
++count;
}
您需要另一个静态递归来检索结果:
private:
template <int N>
static scalar_t get(const scalar_t* const data, op_t<N>)
{
return O1==N ? data[0] : next_t::get(data+1, op_t<N>());
}
public:
template <int N>
scalar_t result(op_t<N>) const
{
return get(data_, op_t<N>());
}
递归停止器不应被调用。然而,这是必要的,因为 next_t::get 被提到了(因此,无论如何都是完全编译的)。只有当请求类型为 accumulate_t <k1...kn>的对象的结果(op_t
在这种情况下,您可以引发任何合适的运行时错误:
template <typename accessor_t>
class accumulate_t <accessor_t, op_void, op_void, op_void, op_void>
{
private:
template <int N>
static scalar_t get(const scalar_t* const, op_t<N>)
{
// if you prefer,
// throw std::runtime_error("invalid result request");
return std::numeric_limits<scalar_t>::quiet_NaN();
}
public:
/* nothing here */
};
因为 SUM 是一个正确类型的全局常量,所以您最终将调用 std::accumulate(begin,end,[...]).结果(总和)。
此时,您可以编写计算结果的代码和检索结果的代码,但是您仍然缺少累加器工厂。正如基于模板旋转的所有对象经常发生的那样,您给用户一个助手函数,该函数最初产生一个“空累加器”(即 accumulate_t
如果选择运算符* (二进制乘法)进行链接,函数如下所示:
template <int N, int N1, ... int Nk>
accumulate_t<T, N, N1, N2,..,Nk-1> operator*(accumulate_t<T, N1,..,Nk-1, Nk>, op_t<N>)
这个链接操作符将包含一个静态断言,以确保“丢弃的术语”Nk 是 op_void。
下面是全局助手函数:
template <typename accessor_t>
inline accumulate_t<accessor_t> collect(const accessor_t& v)
{
return v;
}
最后,这里是整个类的列表,与递归停止特殊化并列:
| 模板<类型名称 accessor_t,int O1 = op_void,int O2 = op_void,int O3 = op_void,int O4 = op_void>班级积聚{typedef typename accessor _ t::value _ type 标量 _ t;模板
| typedef(类型定义)accumulate _ t<accessor_t>next _ t;</accessor_t> | |
| 静态 const int OP _ COUNT = 1+next _ t::OP _ COUNT; | 静态常量 int OP _ COUNT = 0; |
| scalar _ t data _[OP _ COUNT];size _ t count _accessor _ t accessor _ | accessor _ t accessor _ |
| 静态 void 应用(scalar_t* const 数据,常量标量 x,大小 t&计数){data = (count>0)?op_t
| 模板 static scalar _ t get(const scalar _ t * const data,op_t{return O1==N?data[0] : next_t::get(data+1,op _ t
| 公共: | 公共: |
| accumulate_t(常量访问器 _t& v =访问器 t()):访问器 (v),count(0),data(){} | accumulate_t(常量访问器 _t& v =访问器 _t()):访问器 _(v){} |
| 模板积累 _t 运算符* (op_t
| 模板
| 模板 scalar_t 结果(op_t
| size_t size()常量{返回计数 _;} | |
| }; | }; |
最后一个特性提供了一次检索更多结果的能力。这是非常重要的,因为它避免了存储累积的结果。
您只需引入一个将引用绑定到每个 op_t 的操作符(本例使用操作符>>,因为它类似于一个箭头)。另一个可能的选择是运算符<=, since <= can be seen as ←) and builds a reference wrapper of unique type. From this temporary, an overloaded accumulator::result 将提取两个操作数并执行赋值。
RESULT1 r1;
RESULT2 r2;
accumulator.result(SUM >> r1, MAX >> r2);
实现如下:
template <typename scalar_t, int N>
struct op_result_t
{
scalar_t& value;
op_result_t(scalar_t& x)
: value(x)
{
}
};
template <typename scalar_t, int N>
inline op_result_t<scalar_t, N> operator>> (const op_t<N>, scalar_t& x)
{
return op_result_t<scalar_t, N>(x);
}
然后将这些方法添加到通用模板中(宏只是为了简洁):
#define ARG(J) const op_result_t<scalar_t, N##J> o##J
// ARG(1) expands to "const op_result_t<scalar_t, N1> o1"
template <int N1>
const accumulate_t& result(ARG(1)) const
{
o1.value = result(op_t<N1>());
return *this;
}
template <int N1, int N2>
const accumulate_t& result(ARG(1), ARG(2)) const
{
result(o2);
return result(o1);
}
template <int N1, int N2, int N3>
const accumulate_t& result(ARG(1), ARG(2), ARG(3)) const
{
result(o3);
return result(o1, o2);
}
template <int N1, int N2, int N3, int N4>
const accumulate_t& result(ARG(1), ARG(2), ARG(3), ARG(4)) const
{
result(o4);
return result(o1, o2, o3);
}
#undef ARG
表达式 MAX>>x 默默返回 op_result_t (x)。
如果 x 与累积结果的类型不同,它将不会编译。
几个额外的增强将节省一些打字。您只需添加第一个结果,并通过操作符()链接后续调用,而不是有许多结果。 十三
template <int N1>
const accumulate_t& result(const op_result_t<scalar_t,N1> o1) const
{
o1.value = result(op_t<N1>());
return *this;
}
template <int N1>
const accumulate_t& operator()(const op_result_t<scalar_t,N1> o1) const
{
return result(o1);
}
所以与其说:
int q_sum, q_gcd, q_max;
std::accumulate(...).result(SUM >> q_sum, GCD >> q_gcd, MAX >> q_max);
新语法是:
std::accumulate(...).result(SUM >> q_sum)(GCD >> q_gcd)(MAX >> q_max);
或者甚至:
std::accumulate(...)(SUM >> q_sum)(GCD >> q_gcd)(MAX >> q_max);
其次,添加一个重载,该重载返回累加单个量的函数的第一个结果:
scalar_t result() const
{
// MXT_ASSERT(O2 == op_void);
return result(op_t<O1>());
}
// now .result(SUM) is equivalent to .result()
int S = std::accumulate(data, data+7, collect(...)*SUM).result();
8.5.驱动程序
一个写得好的算法避免了不必要的代码乘法。要重写一个现有的算法以获得更大的通用性,您必须从其中删除一些“固定的”逻辑,并通过一个模板参数(通常是一个仿函数)再次插入它:
template <typename iterator_t>
void sort(iterator_t begin, iterator_t end)
{
for (...)
{
// ...
if (a<b) // operator< is a good candidate for becoming a functor
{}
}
}
所以你把它改写成:
template <typename iterator_t, typename less_t>
void sort(iterator_t begin, iterator_t end, less_t less)
{
for (...)
{
// now we ask the functor to "plug" its code in the algorithm
if (less(a,b))
{}
}
}
一个驱动是一个可以一路引导算法的对象。
函子和驱动的主要区别在于前者有一个通用的类似函数的接口(至少是 operator()),这个接口是开放给用户定制的。另一方面,驱动程序是一个具有详细接口的低级对象,它不应该被定制(除了它的名字,它甚至可能没有被记录,就像它是一个标记类型)。框架本身将提供一个小的固定驱动程序集。
考虑下面的例子。您需要一个 sq 函数,它可以选择在 std::cerr 上记录结果。因为如果您接收一个通用的 logger 对象,您就不能强制这样的约束,所以您切换到驱动程序,然后提供一些:
struct dont_log_at_all
{
bool may_I_log() const { return false; }
};
struct log_everything
{
bool may_I_log() const { return true; }
};
struct log_ask_once
{
bool may_I_log() const
{
static bool RESULT = AskUsingMessageBox("Should I log?", MSG_YN);
return RESULT;
}
};
template <typename scalar_t, typename driver_t>
inline scalar_t sq(const scalar_t& x, driver_t driver)
{
const scalar_t result = (x*x);
if (driver.may_I_log())
std::cerr << result << std::endl;
return result;
}
template <typename scalar_t>
inline scalar_t sq(const scalar_t& x)
{
return sq(x, dont_log_at_all());
}
注意,driver_t::may_I_log()既不包含关于平方的代码,也不包含关于日志记录的代码。它只是做出一个决定,驱动算法的流程。
驱动程序的最大优势是减少调试时间,因为主算法是一个单一的函数。通常驱动程序对运行时的影响很小。然而,没有什么可以阻止一个驱动程序执行长时间复杂的计算。
通常,您总是通过实例调用驱动程序。一个界面,如
template <typename driver_t>
void explore(maze_t& maze, driver_t driver)
{
while (!driver.may_I_stop())
{ ... }
}
比它的无状态对应物 更一般 14 :
template <typename driver_t>
void explore(maze_t& maze)
{
while (driver_t::may_I_stop())
{ ... }
}
驱动程序在某种程度上类似于“公共非虚拟/受保护虚拟”的经典 C++ 习语(见 6.3 节)。关键的相似之处在于算法的结构是固定的。希望用户只定制特定的部分,这些部分只在基础设施需要时运行。 十五
8.6.阿尔戈斯
一个 algor ,或者算法函子,是一个嵌入算法的对象,或者简单地说是一个带状态的算法。
标准 C++ 库提供了一个
algor 对象实现了一个简单的类似函数的接口—通常是 operator()—用于算法的执行,但是它的状态允许更快的重复执行。
algor 有用的最简单的情况是缓冲内存分配。std::stable_sort 可能需要分配一个临时缓冲区,这个缓冲区在函数返回时必须释放。通常这不是一个问题,因为花费在(单个)内存分配上的时间是由算法本身的执行决定的。一个小的输入将导致一个小的内存请求,这是“快速的”(操作系统倾向于支持小的分配)。大量的输入将导致“缓慢”的内存请求,但这一额外的时间将被忽略,因为算法将需要更多的时间来运行。
然而,在有些情况下,单个缓冲区足以满足许多请求。对许多长度相似的向量进行稳定排序时,如果在对象中维护缓冲区,可以节省分配/释放时间:
template <typename T>
class stable_sort_algor
{
buffer_type buffer_;
public:
template <RandomAccessIterator>
void operator()(RandomAccessIterator begin, RandomAccessIterator end)
{
// ensure that buffer_ is large enough
// if not, reallocate
// then perform the stable sort
}
~stable_sort_algor()
{
// release buffer_
}
};
综上所述,最简单的 algor 只是一种带状态的函子(在最后一种情况下,是一个临时缓冲区),但 algor 可能有更丰富的接口,超越了函子。
一般来说,算法不会被复制或赋值。它们被构造和重用(比如在一个循环中)或者在一次执行中用作未命名的临时变量。因此,你不需要担心效率,只需要担心安全。如果 buffer_type 不能被安全复制(如果是指针的话),就显式禁用所有危险的成员函数,使它们成为私有或公共的无操作。如果 buffer_type
另一种有用的 algor 是一个 self- 累加器 ,它可以一次保存多个结果。不涉及缓冲(见第 8.4 节)。
template <typename T>
class accumulator
{
T max_;
T min_;
T sum_;
// ...
public:
accumulator()
: sum_(0) // ...
{
}
template <typename iterator_t>
accumulator<T>& operator()(iterator_t begin, iterator_t end)
{
for (;begin != end; ++begin)
{
sum_ += *begin;
// ...
}
return *this;
}
T max() const { return max_; }
T min() const { return min_; }
T sum() const { return sum_; }
// ...
};
int main()
{
double data[] = {3,4,5 };
// single invocation
double SUM = accumulator<double>()(data, data+3).sum();
// multiple results are needed
accumulator<double> A;
A(data, data+3);
std::cout << "Range: " << A.max()-A.min();
}
一个交互式算法 有一个允许呼叫者一步一步运行算法的界面。例如,假设您必须计算平方根,达到一定的精度:
template <typename scalar_t>
class interactive_square_root
{
scalar_t x_;
scalar_t y_;
scalar_t error_;
public:
interactive_square_root(scalar_t x)
: x_(x)
{
iterate();
}
void iterate()
{
// precondition:
// y_ is some kind of approximate solution for y2=x
// error_ is |y2-x|
// now compute a better approximation
}
scalar_t error() const
{
return error_;
}
operator scalar_t() const
{
return y_;
}
};
驱动算法的是用户:
int main()
{
interactive_square_root<double> ISR(3.14);
while (ISR.error()>0.00001)
{
ISR.iterate();
}
double result = ISR;
}
这种算法通常从构造函数中获取所有参数。
一个常见的用例是产生一组解决方案的算法。执行后,成员函数允许用户以某种顺序“访问”所有解。 16 这些算法可能会做构造函数中的所有工作:
template <typename string_t>
class search_a_substring
{
const string_t& text_;
std::vector<size_t> position_;
public:
search_a_substring(const string_t& TEXT, const string_t& PATTERN)
: text_(TEXT)
{
// search immediately every occurrence of PATTERN in TEXT
// store all the positions in position_
}
bool no_match() const { return position_.empty(); }
// the simplest visitation technique
// is... exposing iterators
typedef std::vector<size_t>::const_iterator position_iterator;
position_iterator begin() const
{
return position_.begin();
}
position_iterator end() const
{
return position_.end();
}
};
在子串匹配的情况下,迭代器可能会从第一个匹配到最后一个匹配。在数值最小化问题中,解可能是 N 个点,在这些点上函数具有目前为止找到的最小值。
一个更复杂的访问者接受接口可以接受两个输出 迭代器,algor 将在其中编写它的解决方案。您可以根据迭代器 value_type 在解决方案上构建一个“自定义视图”。例如,内部计算对(Xj,Yj)的 algor 可能只发出第一个分量或整个对(下面是一个简化的例子):
class numerical_minimizer
{
std::function<double (double)> F;
std::vector<double> X_; // all the points where F has minima
public:
// ...
template <typename out_t>
out_t visit(out_t beg, out_t end) const
{
typedef typename std::iterator_traits<out_t>::value_type> val_t;
int i=0;
while (beg != end)
*beg++ = build_result(i++, instance_of<val_t>());
return beg;
}
private:
template <typename T>
double build_result(int i, instance_of<T>) const
{
return X_[i];
}
using std::pair;
template <typename T>
pair<double,double> build_result(int i, instance_of<pair<T,T>>) const
{
return std::make_pair(X_[i], F(X_[i]));
}
};
8.7.转发和引用包装器
对于一个类模板来说,保存一个泛型类型的成员是一种常见的习惯用法,类会将执行调度给这个成员。
template <typename T>
class test
{
T functor_;
public:
typename T::value_type operator()(double x) const
{
return functor_(x); // call forward
}
};
由于成员的确切类型未知,您可能必须实现 test::operator()的几个重载。因为这是一个模板,所以这不是一个问题,因为实际需要的将被实例化,其余的将被忽略。
template <typename T>
class test
{
T functor_;
public:
/* we don't know how many arguments functor_ needs */
template <typename T1>
typename T::value_type operator()(T1 x) const
{
return functor_(x); // call forwarding
}
template <typename T1, typename T2>
typename T::value_type operator()(T1 x, T2 y) const
{
return functor_(x, y); // call forwarding
}
// more...
};
调用错误的重载(即提供太多或不受支持的参数)将导致编译器错误。但是,请注意,参数是通过值转发的,因此您可以修改原型:
template <typename T1>
typename T::value_type operator()(const T1& x) const
{
return functor_(x); // call forwarding
}
但是如果 T 需要一个非常数引用的参数,代码就不会编译。
为了理解问题的严重性,考虑一个稍微不同的例子,其中您用未指定数量的参数构造了一个成员。
STL 指南建议为类测试编写一个单独的构造函数,它(可能)接受一个先前构造的 T 类型的对象:
test(const T& data = T())
: member_(data)
{
}
这种策略并不总是可行的。特别是,T 可能有一个不可访问的复制构造函数,或者它可能是非常量引用。
事实上,让我们暂时忘记 STL 风格,采用与前面相同的 operator()习惯用法。
template <typename T>
class bad_test
{
T member_;
public:
template <typename X1>
bad_test(X1 arg1)
: member_(arg1)
{
}
template <typename X1, typename X2>
bad_test(X1 arg1, X2 arg2)
: member_(arg1, arg2)
{
}
};
正如所写的,bad_test
int main(int argc, char* argv[])
{
double x = 3.14;
bad_test<double&> urgh(x); // unfortunately, it compiles
urgh.member_ = 6.28; // bang!
int i = 0;
assert(x == 6.28); // assertion failed!
// ...
}
urgh 的构造函数是在 double 类型上实例化的,而不是 double&,所以 urgh.member_ 是指其构造函数堆栈中的一个临时位置(即 arg1 占用的存储空间),其内容是 x 的临时副本。
因此,您修改 bad_test 来通过常量引用转发参数。至少 good_test
template <typename T>
class good_test
{
T member_;
public:
template <typename X1>
good_test(const X1& arg1)
: member_(arg1)
{
}
};
然而,额外的包裹层可以解决这两个问题:
template <typename T>
class reference_wrapper
{
T& ref_;
public:
explicit reference_wrapper(T& r)
: ref_(r)
{
}
operator T& () const
{
return ref_;
}
T* operator& () const
{
return &ref_;
}
};
template <typename T>
inline reference_wrapper<T> by_ref(T& x)
{
return reference_wrapper<T>(x);
}
int main()
{
double x = 3.14;
good_test<double> y0(x); // ok: x is copied into y0.member_
good_test<double&> y1(x); // compiler error!
y1.member_ = 6.28; // would be dangerous, but does not compile
good_test<double&> y2(by_ref(x));
y2.member_ = 6.28; // ok, now x == 6.28
}
使用 by_ref,good_test
注同样,参数转发问题在 C++0x 中用 R 值引用解决了。
1 它们被称为“透明的”。据作者所知,这本书是这个想法第一次公开出现的地方。详见www . open-STD . org/JT C1/sc22/wg21/docs/papers/2012/n 3421 . htm
。
2 由于大多数函子是无状态的,因此不受初始化问题的影响,全局常量可以在头文件中创建。
作为一个规则,为了表达,最好写一个完全合格的人::年龄而不仅仅是年龄,所以你必须假设 Person 里面有一个 age_t 类型的静态常量。这也允许 age_t 成为人的朋友。还可以考虑 Person::AGE(),其中 AGE 要么是 age_t 的成员 typedef,要么是返回 age_t 的默认实例的静态成员函数。
4 详细描述见 4.2.1 节。
5 细心的读者会注意到,下面的例子确实传递了数组的长度,即使它总是被忽略。
6 类似的 STL 结构反而仅仅是在函子中嵌入一个指针。
7 有些编译器,包括 VC,不会注意到区别;但是,海合会确实在意。
8 本段仅作为教学示例,不作为生产代码的解决方案。在实践中,这个问题可以通过提升编译器对错误的警告,或者通过使用现代 C++ 强类型枚举来解决。然而,作为一个(元)程序员如何变通 C++ 语法来解决小问题的例子,这是很有启发性的。
9 如果 X 有类型 const A,wrap 会推导出 T=const A 并挑选第二个重载,前提是你仔细实现 enum_var_helper。
然而,有一些在线累加器可以很准确地估计个百分点。
11 你可能想再回头看看第五章。
12 例如,maxmin 算法的复杂度比分两步计算 max 和 min 低 25%。
13 关于这一点详见第 9.3 节。
**14 特质在某种程度上等同于无国籍司机。
15 参见 http://www.gotw.ca/publications/mill18.htm。
16 这和 std::regex 的工作方式有些相似。
17 这个例子写得好像所有成员都是公共的。**
九、不透明类型原则
模板类型名称可能太复杂,用户无法直接使用,因为它们可能很冗长,或者可能需要非常复杂的语法。所以你要么发布一个方便的 typedef,要么允许用户完全忽略他们的类型。
平原 C 充满了不透明的类型。
在 C # 中,文件流是通过指向驻留在系统内存中的未知文件结构的指针来处理的(C # 运行时会预分配一小部分这样的结构)。要在打开的文件中检索当前位置,调用 fgetpos (FILE,fpos_t),传递文件指针和另一个充当书签的不透明类型。您不能知道或修改当前位置,但可以通过调用 fsetpos (FILE,fpos_t)来恢复它。从用户的角度来看,fpos_t 的实例是完全不透明的。因为只有名称是已知的,所以除了默认构造函数、复制构造函数和赋值之外,该类型没有接口。
在不透明类型原则中,不透明性只与类型名有关,与接口无关。换句话说,对象有一个未指定的类型和一个已知的接口——它可能是迭代器或函子。
作为“难以编写”的类型,您不希望存储对象,而是应该在创建站点上立即使用它。
9.1.多态结果
假设一个函数执行一次产生几个结果的计算。
你可以将它们打包在一个多态结果中,并允许用户选择需要的内容。
让我们举一个简化的例子:
template <typename iterator_t >
[[???]] average(iterator_t beg, iterator_t end)
{
typename std::iterator_traits<iterator_t>::value_type total = 0;
total = std::accumulate(beg, end, total);
size_t count = std::distance(beg, end);
return total/count;
}
固定的返回类型将破坏部分结果,这可能是有用的。因此,您可以延迟子项的聚合,并像这样更改代码:
template <typename T, typename I>
class opaque_average_result_t
{
T total_;
I count_;
public:
opaque_average_result_t(T total, I count)
: total_(total), count_(count)
{
}
// default result is the average
operator T () const
{
return total_/count_;
}
T get_total() const
{
return total_;
}
I get_count() const
{
return count_;
}
};
template <typename VALUE_TYPE, typename iterator_t >
opaque_average_result_t<VALUE_TYPE, size_t> average(iterator_t beg, iterator_t end)
{
VALUE_TYPE total = 0;
total = std::accumulate(beg, end, total);
size_t count = std::distance(beg, end);
return opaque_average_result_t<VALUE_TYPE, size_t>(total, count);
}
现在,客户端可以以更多方式使用原始算法:
std::vector<double> v;
double avg = average<double>(v.begin(), v.end());
double sum = average<double>(v.begin(), v.end()).get_total();
由于返回类型是不透明的,所以不方便存储结果,但是如果需要的话,很容易将其传递给函数模板: 1
<???> x = average<double>(v.begin(), v.end());
template <typename T>
void receive(T res, double& avg, double& sum)
{
avg = res;
sum = res.get_total();
}
std::vector<double> v;
double avg, sum;
receive(average<double>(v.begin(), v.end()), avg, sum);
9.2.经典 Lambda 表达式
Lambda 表达式是在调用点创建的不透明函数对象。它们将一些基本的片段与有意义的操作符结合起来。产生的函子稍后将在其参数上重放操作符序列。例如,给定两个合适的“lambda 变量”类型的对象 X 和 Y,那么(X+Y)*(X-Y)将是一个接受两个参数并返回它们的和乘以它们的差的函子。
构建一个简化的实现并理解底层的模板技术是一个很好的练习。这些最初是由 Todd Veldhuizen 在他的开创性文章“表达式模板”中提出的。
可以这样写代码:cos(X+2.0) 是一个表达式,返回一个函子,给定一个双 X,其运算符()计算 cos(X+2.0)。
lambda_reference<const double> X;
std::find_if(..., X<5.0 && X>3.14);
std::transform(..., cos(X+2.0));
lambda_reference<double> Y;
std::for_each(..., Y+=3.14);
lambda_reference<const double, 0> ARG1;
lambda_reference<const double, 1> ARG2;
std::sort(..., (ARG1<ARG2));
可以做如下假设。有些将在以后删除,有些有望在您继续操作时变得更加清晰:
- 为了清楚起见,T 将是一个友好的标量类型,double 或 float,所以所有的操作符都是定义良好的。
- 一个 lambda 表达式将最多接收 K=4 个参数,所有的都是相同的类型 T &。特别是:
- lambda_reference
和 lambda_reference 是不同的,后者是“lambda-const 引用到 double”。 - 表达式必须包含对相同类型对象的引用。
- 为了简单起见,所有的常量最初都是 T 类型,X+2 被认为是无效的语法,因为 X 指的是 double,而 2 是 int。因此,你必须编写 X+2.0(你将在本章后面学习如何去除这个限制)。
- 我们显式地尝试编写看起来相似的函数,因此它们可以很容易地用预处理宏生成,即使它们没有在这里列出。
9.2.1.基本 Lambda 对象
让我们在这里改写一下基本定义:lambda 对象是一个用特殊语法生成的函子(即,用运算符组装一些占位符)。函子的作用是在它的实际参数上重放相同的运算符。例如,如果 X 是一个这样的占位符,那么表达式 X+2 产生一个接受一个参数并返回其参数加 2 的函子。
首先,定义一个空的静态接口。注意 T 目前没有被使用,但是你很快就会意识到为什么它是必要的。
template <typename true_t, typename T>
class lambda
{
protected:
~lambda()
{
}
public:
const true_t& true_this() const
{
return static_cast<const true_t&>(*this);
}
};
第一个(普通的)对象是一个λ常数。这是一个函子,不管参数是什么,它都返回常数结果。因为它是一个 lambda 表达式,所以您可以从接口中得到它:
template <typename T>
class lambda_const : public lambda<lambda_const<T>, T>
{
typedef const T& R;
T c_;
public:
typedef T result_type;
lambda_const(R c)
: c_(c)
{
}
result_type operator()(R = T(), R = T(), R = T(), R = T()) const
{
return c_;
}
};
请注意,lambda 常量可以接受零个或多个参数,但它是一个函数对象,因此调用必须使用某种形式的运算符()。
第二个对象是 lambda_reference
lambda_reference<double> X1;
lambda_reference<const double> Y1;
X1(3.14); // error: needs double&
Y1(3.14); // ok: takes and returns const double&
变量的选择并不简单。通常,参数轮换是首选的技术。此外,由于引用是廉价的,这个例子引入了一种称为参数的复制的技术,以减少重载的数量。operator()的最后一个参数是“克隆”的,因此它总是传递四个项目。
template <typename T, size_t N = 0>
class lambda_reference: public lambda<lambda_reference<T, N>, T>
{
static T& apply_k(static_value<size_t,0>, T& x1, T&, T&, T&)
{
return x1;
}
template <size_t K>
static T& apply_k(static_value<size_t,K>, T& x1, T& x2, T& x3, T& x4)
{
return apply_k(static_value<size_t,K-1>(), x2, x3, x4, x1);
}
public:
typedef T& result_type;
result_type operator()(T& x1, T& x2, T& x3, T& x4) const
{
MXT_STATIC_ASSERT(N<4);
return apply_k(static_value<size_t,N>(), x1, x2, x3, x4);
}
result_type operator()(T& x1, T& x2, T& x3) const
{
MXT_STATIC_ASSERT(N<3);
return apply_k(static_value<size_t,N>(), x1, x2, x3, x3);
}
result_type operator()(T& x1, T& x2) const
{
MXT_STATIC_ASSERT(N<2);
return apply_k(static_value<size_t,N>(), x1, x2, x2, x2);
}
result_type operator()(T& x1) const
{
MXT_STATIC_ASSERT(N<1);
return apply_k(static_value<size_t,N>(), x1, x1, x1, x1);
}
};
9.2.2.λ函数和运算符
应用于 lambda 表达式的一元函数 F 是返回应用于 lambda 结果的 F 的函子。 2
由于有了静态接口,实现可以立刻处理任何λ表达式。
还有,lambda
template <typename F, typename X, typename T>
class lambda_unary : public lambda<lambda_unary<F,X,T>, T>
{
X x_;
F f_;
public:
lambda_unary(const lambda<X,T>& that)
: x_(that.true_this())
{
}
typedef typename F::result_type result_type;
result_type operator()() const
{
return f_(x_());
}
result_type operator()(T& x1) const
{
return f_(x_(x1));
}
result_type operator()(T& x1, T& x2) const
{
return f_(x_(x1, x2));
}
result_type operator()(T& x1, T& x2, T& x3) const
{
return f_(x_(x1, x2, x3));
}
// ...
};
前面的代码构建了一个仿函数 f_,它的运算符()被调用,但是你还需要插入全局/静态成员函数。因此,需要一个小的适配器:
template <typename T, T (*F)(T)>
struct unary_f_wrapper
{
typedef T result_type;
T operator()(const T& x) const { return F(x); }
};
接下来,收集 traits 类中的所有全局函数:
template <typename T>
struct unary_f_library
{
static T L_abs(T x) { return abs(x); }
static T L_cos(T x) { return cos(x); }
// ...
};
最终你开始在 lambda 对象上定义函数:
#define LAMBDA_ABS_TYPE \
lambda_unary<unary_f_wrapper<T, &unary_f_library<T>::L_abs>, X, T>
template <typename X, typename T>
LAMBDA_ABS_TYPE abs(const lambda<X, T>& x)
{
return LAMBDA_ABS_TYPE(x);
}
#define LAMBDA_COS_TYPE \
lambda_unary<unary_f_wrapper<T, &unary_f_library<T>::L_cos>, X, T>
template <typename X, typename T>
LAMBDA_COS_TYPE cos(const lambda<X, T>& x)
{
return LAMBDA_COS_TYPE(x);
}
...
这个方案也适用于一元运算符,只需使用不同的仿函数。
template <typename T>
struct lambda_unary_minus
{
typedef T result_type;
result_type operator()(const T& x) const { return -x; }
};
#define LAMBDA_U_MINUS_TYPE lambda_unary<lambda_unary_minus<T>, X, T>
template <typename X, typename T>
LAMBDA_U_MINUS_TYPE operator-(const lambda<X, T>& x)
{
return LAMBDA_U_MINUS_TYPE(x);
}
添加的特性越多,返回类型就变得越复杂,但是这些对用户来说是完全隐藏的。
二元运算,比方说+,可以类似地定义:(lambda < X1,T > + lambda < X2,T >)是一个函子,它把它的参数分配给它的两个加数。 3 所以,类比一元的情况,你会定义一个具体的对象来处理二元运算符,即 lambda_binary < X1,F,X2,T >。特别是混合二元运算,比如 lambda < X1,T > + T,是一种特殊情况,通过将 T 提升为 lambda_const < T >来处理。
template <typename X1, typename F, typename X2, typename T>
class lambda_binary : public lambda< lambda_binary<X1,F,X2,T>, T >
{
X1 x1_;
X2 x2_;
F f_;
public:
lambda_binary(const lambda<X1,T>& x1, const lambda<X2,T>& x2)
: x1_(x1.true_this()), x2_(x2.true_this())
{
}
typedef typename F::result_type result_type;
result_type operator()() const
{
return f_(x1_(), x2_());
}
result_type operator()(T& x1) const
{
return f_(x1_(x1), x2_(x1));
}
result_type operator()(T& x1, T& x2) const
{
return f_(x1_(x1, x2), x2_(x1, x2));
}
result_type operator()(T& x1, T& x2, T& x3) const
{
return f_(x1_(x1, x2, x3), x2_(x1, x2, x3));
}
// ...
};
在此实现中,逻辑运算符不会使用短路。如果 T 是 int,lambda 对象 X > 0 & & (1/X) < 5 将在被零除时崩溃,而类似的 C++ 语句返回 false。
像+这样的算术运算符可以写成 f_(x1_(...),x2_(...))和前面一样,但这对于& &和||(其工作流程更复杂)是不正确的:
b1 := x1_(...);
if (f_(b1, true) == f_(b1, false))
return f_(b1, true);
else
return f_(b1, x2_(...))
在接下来的讨论中,为了清晰起见,我们牺牲了一些正确性,将所有操作符视为普通的二元谓词,我们将上面伪代码中为逻辑操作符编写 lambda_binary 的部分专门化作为练习。
现在你定义“具体的”二元函数:
template <typename T, T (*f)(T, T)>
struct binary_f_wrapper
{
typedef T result_type;
T operator()(const T& x, const T& y) const { return f(x,y); }
};
template <typename T>
struct binary_f_library
{
static T L_atan2(T x, T y) { return atan2(x, y); }
// ...
};
#define ATAN2_T(X1, X2) \
lambda_binary<X1, \
binary_f_wrapper<T, &binary_f_library<T>::L_atan2>, \
X2, T>
template <typename X1, typename X2, typename T>
ATAN2_T(X1, X2) atan2(const lambda<X1,T>& L, const lambda<X2,T>& R)
{
return ATAN2_T(X1, X2) (L, R);
}
template <typename X1, typename T>
ATAN2_T(X1, lambda_const<T>) atan2(const lambda<X1,T>& L, const T& R)
{
return atan2(L, lambda_const<T>(R));
}
template <typename T, typename X2>
ATAN2_T(lambda_const<T>, X2) atan2(const T& L, const lambda<X2,T>& R)
{
return atan2(lambda_const<T>(L), R);
}
最后,您需要另一个扩展。有三种类型的运算符
- 二元谓词,带有签名布尔值 F(常数 T &,常数 T &)
- 二元运算符,带签名 T F(常数 T &,常数 T &)
- 赋值,签名 T & F(T &,常量 T &
这转化为以下 C++ 代码:
enum lambda_tag
{
LAMBDA_LOGIC_TAG,
LAMBDA_ASSIGNMENT_TAG,
LAMBDA_OPERATOR_TAG
};
template <typename T, lambda_tag TAG>
struct lambda_result_traits;
template <typename T>
struct lambda_result_traits<T, LAMBDA_ASSIGNMENT_TAG>
{
typedef T& result_type;
typedef T& first_argument_type;
typedef const T& second_argument_type;
};
template <typename T>
struct lambda_result_traits<T, LAMBDA_OPERATOR_TAG>
{
typedef T result_type;
typedef const T& first_argument_type;
typedef const T& second_argument_type;
};
template <typename T>
struct lambda_result_traits<T, LAMBDA_LOGIC_TAG>
{
typedef bool result_type;
typedef const T& first_argument_type;
typedef const T& second_argument_type;
};
所以你可以写:
template <typename T>
struct lambda_less
{
typedef lambda_result_traits<T, LAMBDA_LOGIC_TAG> traits_t;
typedef typename traits_t::result_type result_type;
typedef typename traits_t::first_argument_type arg1_t;
typedef typename traits_t::second_argument_type arg2_t;
result_type operator()(arg1_t x, arg2_t y) const
{
return x < y;
}
};
template <typename T>
struct lambda_plus
{
typedef lambda_result_traits<T, LAMBDA_OPERATOR_TAG> traits_t;
typedef typename traits_t::result_type result_type;
typedef typename traits_t::first_argument_type arg1_t;
typedef typename traits_t::second_argument_type arg2_t;
result_type operator()(arg1_t x, arg2_t y) const
{
return x + y;
}
};
template <typename T>
struct lambda_plus_eq
{
typedef lambda_result_traits<T, LAMBDA_ASSIGNMENT_TAG> traits_t;
typedef typename traits_t::result_type result_type;
typedef typename traits_t::first_argument_type arg1_t;
typedef typename traits_t::second_argument_type arg2_t;
result_type operator()(arg1_t x, arg2_t y) const
{
return x += y;
}
};
这些对象的差异很小。
逻辑和标准操作符与任何其他二元函数相同,除了返回类型(与 atan2 比较)。下面是 lambda 的运算符< : 4 的实现
#define LSS_T(X1,X2) lambda_binary<X1, lambda_less<T>, X2, T>
template <typename X1, typename X2, typename T>
LSS_T(X1,X2) operator<(const lambda<X1,T>& L, const lambda<X2,T>& R)
{
return LSS_T(X1, X2) (L, R);
}
template <typename X1, typename T>
LSS_T(X1, lambda_const<T>) operator<(const lambda<X1,T>& L, const T& R)
{
return L < lambda_const<T>(R);
}
template <typename T, typename X2>
LSS_T(lambda_const<T>, X2) operator<(const T& L, const lambda<X2,T>& R)
{
return lambda_const<T>(L) < R;
}
赋值运算符不允许第三个重载,这将对应于 lambda 表达式,如(2.0 += X),这是一个有点可疑的 C++ 语句:
#define PEQ_T(X1,X2) lambda_binary<X1, lambda_plus_eq<T>, X2, T>
template <typename X1, typename X2, typename T>
PEQ_T(X1,X2) operator+=(const lambda<X1,T>& L, const lambda<X2,T>& R)
{
return PEQ_T(X1,X2) (L,R);
}
template <typename X1, typename T>
PEQ_T(X1,lambda_const<T>) operator+=(const lambda<X1,T>& L, const T&R)
{
return L += lambda_const<T>(R);
}
下面是一个使用所有先前代码的示例:
lambda_reference<double, 0> VX1;
lambda_reference<double, 1> VX2;
double data[] = {5,6,4,2,-1};
std::sort(data, data+5, (VX1<VX2));
std::for_each(data,data+5, VX1 += 3.14);
std::transform(data,data+5, data, VX1 + 3.14);
std::transform(data,data+5, data, 1.0 + VX1);
std::for_each(data,data+5, VX1 += cos(VX1));
这里有一个故意产生错误的例子,它仍然是人类可读的:
const double cdata[] = {5,6,4,2,-1};
// add 3.14 to all the elements of a constant array...
std::for_each(cdata, cdata+5, VX1 += 3.14);
error C2664: 'double &lambda_binary<X1,F,X2,T>::operator ()(T &) const' :
cannot convert parameter 1 from 'const double' to 'double &'
with
[
X1=lambda_reference<double,0>,
F=lambda_plus_eq<double>,
X2=lambda_const<double>,
T=double
]
Conversion loses qualifiers
see reference to function template instantiation being compiled
'_Fn1 std::for_each<const double*,lambda_binary<X1,F,X2,T>>(_InIt,_InIt,_Fn1)'
with
[
_Fn1=lambda_binary<lambda_reference<double,0x00>,lambda_plus_eq<double>,lambda_const<double>,double>,
X1=lambda_reference<double,0>,
F=lambda_plus_eq<double>,
X2=lambda_const<double>,
T=double,
_InIt=const double *
]
您可能希望下面的代码能够正常工作;相反,它不会编译。错误日志可能又长又吵,但都指向 operator+。这里已经分离出精确的错误:
double data[] = {5,6,4,2,-1};
const double cdata[] = {5,6,4,2,-1};
lambda_reference<const double> C1;
std::transform(cdata,cdata+5, data, C1 + 1.0);
error: 'lambda_binary<X1,lambda_plus<T>,lambda_const<T>,T> operator +(const lambda<true_t,T> &,const T &)' :
template parameter 'T' is ambiguous
could be 'double'
or 'const double'
这个问题相当于:
template <typename T>
struct A
{
};
template <typename T>
void F(A<T>, T)
{
}
A<const double> x;
double i=0;
F(x, i); // error: ambiguous call.
// deduce T=const double from x, but T=double from i
这就是类型特征出现的地方。您只从 lambda 表达式中获取参数 T,并让常量的类型是依赖的。更准确地说,所有带有 const T &类型参数的混合操作符都应该改为接受 typename lambda _ constant _ arg
template <typename T>
struct lambda_constant_arg
{
typedef const T& type;
};
template <typename T>
struct lambda_constant_arg<const T>
{
typedef const T& type;
};
C++ 标准规定,如果一个参数可以从一个实参中推导出来,那么它就被推导出来,然后替换到其余的实参中。如果结果是可行的,那么这个推论就被接受为有效的,所以特别是在这样一个签名中:
template <typename T>
void F(A<T> x, typename lambda_constant_arg<T>::type i);
唯一能推导出 T 的上下文是 x 的类型,所以不会再有歧义了。特别是,现在可以添加任何类型的可转换为 T:
std::transform(cdata, cdata+5, data, C1 + 1);
// don't need to write C1 + 1.0
最后注意,这些 lambda 表达式对于参数的数量并不太严格。唯一的显式检查是 lambda_reference 中的静态断言。 5
lambda_reference<const double, 0> C1;
lambda_reference<const double, 1> C2;
double t1 = ((C1<C2)+(-C1))(3.14); // error: C2 requires 2 args
double t2 = ((C1<C2)+(-C1))(3.14, 6.28); // ok
double t3 = ((C1<C2)+(-C1))(3.14, 6.28, 22/7); // ok, "22/7" ignored
9.2.3.改进
请注意,一元和二元运算确实包含表示运算的仿函数的副本,但仿函数始终是默认构造的。您可以添加一个包装器,在 lambda 表达式中嵌入任何用户仿函数。只需修改构造函数,如下所示:
public:
lambda_unary(const lambda<X,T>& that, F f = F())
: x_(that.true_this()), f_(f)
{
}
这个例子立即使用这个特性来创建一个仿函数,它接受一个仿函数-on-T 并返回一个仿函数-on-lambda:
int main()
{
MyFunctor F;
lambda_reference<double> X;
std::transform(data, data+n, data, lambda_wrapF); // = F(3*X+14)
}
lambda_wrap 是 lambda_wrap_t < void >的全局实例,它的运算符[]吸收了一个合适的用户仿函数。选择[]而不是()给了额外的视觉清晰度,因为它避免了与函数参数的混淆。
template <typename F = void>
class lambda_wrap_t
{
F f_;
public:
lambda_wrap_t(F f)
: f_(f)
{
}
template <typename X, typename T>
lambda_unary<F, X, T> operator()(const lambda<X, T>& x) const
{
return lambda_unary<F, X, T>(x, f_);
}
};
template <>
class lambda_wrap_t<void>
{
public:
lambda_wrap_t(int = 0)
{
}
template <typename F>
lambda_wrap_t<F> operator[](F f) const
{
return f;
}
};
const lambda_wrap_t<void> lambda_wrap = 0;
这在以下情况下使用:
struct MyF
{
typedef double result_type;
result_type operator()(const double& x) const
{
return 7*x - 2;
}
};
lambda_reference<double> V;
std::for_each(begin, end, lambda_wrapMyF()); // will execute MyF(V+2)
同样的技术可以进一步扩展以实现三元运算符(不能重载),假设的语法可以是:
if_[CONDITION].then_[X1].else_[X2]
将语句链接在一起的点清楚地表明 if_[C]的返回类型是一个对象,其成员 then_ 有另一个运算符[],依此类推。
9.2.4.论证和结果演绎
不严格地说,复合 lambda 对象 G:=F(λ)带一个参数 x,返回 F(λ(x))。G 的参数类型是λ的参数类型,G 的结果类型是 f 的结果类型
到目前为止,我们避免了定义这些类型的问题,因为它们要么是固定的,要么是显式给出的。
- lambda 接口中的标量类型 T 充当其运算符()的参数。每当一个函数被应用到 lambda
时,T 被借用并插入结果,也就是说 lambda 。 - lambda 运算符()的返回类型可能会有所不同,所以它被发布为 result_type。例如,lambda_unary
从外部获取 T& x,并返回 F 从调用 F(X(x))返回的任何内容。f 可能返回对 T 或 bool 的引用。
但是,在这个过程中,可能会发生从 bool 到 T 的静默强制转换。
例如,函数对象 abs(C1
一般来说,这是期望的行为:
(C1<C2); // returns bool
((C1<C2)+2); // operator+ will promote "bool" to "double"
运算符&&可以作为运算符
(C1<C2) && (C2>C1); // operator&& will promote two bools to double, then return bool
你应该只指定 lambda_reference 的参数,让每个 lambda 对象正确地借用参数和结果。lambda_reference 实际上是唯一用户可见的对象,它的类型参数足以确定整个仿函数。
这个改变也允许你从 lambda 接口中移除 T:6
template <typename X>
class lambda
{
protected:
~lambda()
{
}
public:
const X& true_this() const
{
return static_cast<const X&>(*this);
}
};
template <typename T, size_t N = 0>
class lambda_reference : public lambda< lambda_reference<T, N> >
{
public:
typedef T& result_type;
typedef T& argument_type;
result_type operator()(argument_type x1) const
{
MXT_STATIC_ASSERT(N<1);
return apply_k(static_value<size_t, N>(), x1, x1, x1, x1);
}
// ...
};
你要用内部对象的 result_type 的(元)函数替换每个“包装”lambda 类中 T 的用法:
template <typename F, typename X>
class lambda_unary : public lambda< lambda_unary<F,X> >
{
X x_;
F f_;
public:
typedef typename F::result_type result_type;
typedef typename X::argument_type argument_type;
// ...
};
然而,虽然 T 是一个普通类型(可能是 const 限定的,但从来不是引用),但 argument_type 通常是一个引用。因此,您需要一个元函数来删除任何限定符:
template <typename T>
struct plain
{
typedef T type;
};
template <typename T>
struct plain<T&> : plain<T>
{
};
template <typename T>
struct plain<const T> : plain<T>
{
};
template <typename T>
class lambda_const : public lambda< lambda_const<T> >
{
typedef typename plain<T>::type P;
P c_;
public:
typedef P result_type;
typedef const P& argument_type;
// ...
};
棘手的问题在于二元运算符,这里有两个 lambdas,X1 和 X2。
应该借用谁的 argument_type?很容易看出两种类型都必须检查,因为必须进行一些推断。
例如,如果 X 是一个 lambda 非常数引用,它需要 T&。λ常数需要常数 T & 1。表达式(X+1.0)是一个函子,它接受一个参数并将其传递给一个 lambda 引用和一个 lambda 常量,因此它应该是 T & amp。一般来说,你需要一个交换元函数,它能够“推导”出一个可行的公共参数类型。
template <typename X1, typename F, typename X2>
class lambda_binary : public lambda< lambda_binary<X1,F,X2> >
{
X1 x1_;
X2 x2_;
F f_;
public:
typedef typename F::result_type result_type;
typedef typename
deduce_argument<typename X1::argument_type, typename X2::argument_type>::type
argument_type;
// ...
};
结合两个任意泛函的问题甚至更深。首先,T 的消除使得所有的返回类型更加复杂。例如,现在 lambda_plus 对象将不得不处理来自任何不同 lambda 的两个不同结果的相加,而不是两个 Ts 的相加:
// before
lambda_binary<X1, lambda_plus<T>, X2, T>
// after
lambda_binary<X1, lambda_plus<typename X1::result_type, typename X2::result_type>, X2>
此外,“泛型加法”的返回类型未知: 7
template <typename T1, typename T2>
struct lambda_plus
{
typedef const typename plain<T1>::type& arg1_t; // not a problem
typedef const typename plain<T2>::type& arg2_t; // not a problem
typedef [[???]] result_type;
result_type operator()(arg1_t x, arg2_t y) const
{
return x + y;
}
};
因此,您需要另一个元函数“推断结果”,它接受 arg1_t 和 arg2_t 并返回一个合适的类型。
幸运的是,在合理的假设下,这个问题可以通过 TMP 技术解决,因为您只有几个自由度。涉及的类型有 T(从 lambda_reference 推导出来的,在整个模板表达式中是唯一的)、T&、const T&、bool。
9.2.5.推导参数类型
现在你将寻找一个元函数 F,它推导出了普通的参数类型。f 应满足:
- 对称性:F
:= F - 最强的需求占上风:F <t ...="">= T&
- const T&和 T 的行为相同:F <const t="" ...="">= F
F 的元参数是其他 lambda 对象的参数类型:
F<typename X1::argument_type, typename X2::argument_type>
最终,F 返回 T&或常数 T&就足够了。最简单的实现是将两个参数都减少到引用。如果它们具有相同的基础类型,您应该选择最强的类型;否则,编译器会给出一个错误:
template <typename T>
struct as_reference
{
typedef const T& type;
};
template <typename T>
struct as_reference<T&>
{
typedef T& type;
};
template <typename T>
struct as_reference<const T&> : as_reference<T>
{
};
template <typename T1, typename T2>
struct deduce_argument
: deduce_argument<typename as_reference<T1>::type, typename as_reference<T2>::type>
{
};
template <typename T>
struct deduce_argument<T&, T&>
{
typedef T& type;
};
template <typename T>
struct deduce_argument<T&, const T&>
{
typedef T& type;
};
template <typename T>
struct deduce_argument<const T&, T&>
{
typedef T& type;
};
注意,当 T 是一个 const 类型时,还将使用专门化 deduct _ argument
9.2.6.推断结果类型
您可以使用类似的方法来编写推导结果类型的代码。也就是说,您将分解您想要覆盖的案例列表,并根据需要实现附加的元功能。首先,请注意函数调用的预期结果是而不是引用,因此您必须开始确保在调用位置没有引用被传递:
template <typename T1, typename T2>
struct lambda_plus
{
typedef const typename plain<T1>::type& arg1_t;
typedef const typename plain<T2>::type& arg2_t;
typedef
typename deduce_result<typename plain<T1>::type, typename plain<T2>::type>::type
result_type;
result_type operator()(arg1_t x, arg2_t y) const
{
return x + y;
}
};
这一次您需要四种专业化:
template <typename T1, typename T2>
struct deduce_result;
template <typename T>
struct deduce_result<T, bool>
{
typedef T type;
};
template <typename T>
struct deduce_result<bool, T>
{
typedef T type;
};
template <typename T>
struct deduce_result<T, T>
{
typedef T type;
};
template <>
struct deduce_result<bool, bool>
{
typedef bool type;
};
最后的专门化是必要的;否则,
9.2.7.静态造型
结果/论证演绎的局限性可能会导致一些不一致。传统加法 bool+bool 的类型为 int,而布尔 lambda 对象的加法返回 bool:
lambda_reference<const double,0> C1;
lambda_reference<const double,1> C2;
((C1<C2) + (C2<C1))(x, y); // it returns bool
两者(C1
解决类似问题的唯一方法是 lambda-cast 运算符。幸运的是,很容易使用不可推导的模板参数来重现 static_cast 的语法:
template <typename T1, typename T2>
struct lambda_cast_t
{
typedef T2 result_type;
result_type operator()(const T1& x) const
{
return x;
}
};
#define LAMBDA_CAST_T(T,X) \
lambda_unary<lambda_cast_t<typename X::result_type, T>, X>
template <typename T, typename X>
LAMBDA_CAST_T(T,X) lambda_cast(const lambda<X>& x)
{
return x;
}
(lambda_cast<double>(C1<C2)+lambda_cast<double>(C1<C2))(3.14, 6.28);
// now returns 2.0
9.2.8.数组
Todd Veldhuizen 率先将“模板表达式”应用于数组的快速运算,以尽量减少临时变量的使用。 8
valarray<double> A1 = ...;
valarray<double> A2 = ...;
valarray<double> A3 = 7*A1-4*A2+1;
一般来说,简单的操作符会产生比需要的更多的对象“副本”。子表达式 7A1 将返回一个临时数组,其中每个元素是 A1 中相应条目的七倍;4A2 将返回另一个临时,以此类推。
相反,您可以使用类似 lambda 的表达式:
template <typename X, typename T>
class valarray_interface
{
// X is the true valarray and T is the scalar
// ...
public:
// interface to get the i-th component
T get(size_t i) const
{
return true_this().get(i);
}
size_t size() const
{
return true_this().size();
}
operator valarray<T>() const
{
valarray<T> result(size());
for (size_t i=0; i<size(); ++i)
result[i] = get(i);
return result;
}
};
该接口可以转换为真实的 valarray。这种类型转换触发了一个临时对象的创建,这个临时对象是按组件填充的(这是最有效的方式)。
乘积 valarray
template <typename VA1, typename F, typename VA2, typename T>
class valarray_binary_op
: public valarray_interface< valarray_binary_op<VA1,F,VA2,T> >
{
const VA1& va1_;
const VA2& va2_;
F op_;
public:
// ...
T get(size_t i) const
{
return op_(va1_.get(i), va2_.get(i));
}
};
对复杂对象(如数组)成功使用表达式模板的关键优化是小心使用常量引用:
const VA1& va1_;
const VA2& va2_;
const 引用通常是好的,因为它绑定到临时变量,但是它不会阻止被引用对象的死亡。
例如,(A7)+B 将产生一个临时(A7),和另一个具有对它的常量引用的对象,以及对 B 的常量引用。由于 A*7“只在那行代码中”是有效的,如果可以存储表达式并在以后对其求值,它将使程序崩溃。
实际上,您可能希望使用特征来确定合适的存储类型。如果 VA1 是 valarray
总而言之,行
valarray<double> A3 = A1*7;
将神奇地触发右边模板表达式的组件式评估,使用接口中的 cast 操作符,或者更好地使用 valarray
铸造操作者不容易去除。因为 A17 应该是一个 valarray,所以它甚至可能被用作 valarray,比如编写(A17)[3]或 even (A1*7)。调整大小(n)。这意味着在可行的情况下,valarray 和 valarray_interface 应该非常相似。
静态接口方法的另一个优点是许多不同的对象可以表现为一个伪 valarray。作为 lambda_const 的等价体,可以让标量 c 充当数组[c,c,...,c]:
template <typename T>
class scalar_wrapper
: public valarray_interface< scalar_wrapper<T> >
{
T c_;
size_t size_;
public:
scalar_wrapper(T c, size_t size)
: c_(c), size_(size)
{
}
T get(size_t i) const
{
return c_;
}
};
9.3.创造性句法
这一节专门讨论如何利用模板语法技巧,比如操作符重载,来表达与标准含义不同的概念。
一些运算符传递一种自然的结合性;最简单的例子是用+,<连接的序列:
std:string s = "hello";
std:string r = s + ' ' + "world" + '!';
std::ofstrean o("hello.txt");
o << s << ' ' << "world" << '!';
int a = 1,2,3,4,5,6,7;
用户期望这些操作符能够形成任意长度的链。此外,运算符[]和运算符()有时可能有相似的含义;特别是,当链条长度固定时,应使用前者:
array a;
a[2]; // ok: the user expects a single subscript
matrix m;
m[2][3]; // ok: a matrix is expected to have 2 coordinates
SomeObject x;
x[2][3][1][4][5]; // bad style, here the meaning is obscure
Tensor<double,5> t;
t[2][3][1][4][5]; // good style: the user intuitively expects 5 "dimensions"
您可以通过编写使用第一个参数并返回能够处理剩余链的东西的操作符来利用这种语法。想想这条线:
std::cout << a << b << c;
这个表达式的形式是:F(F(F(cout,a),b),c),所以 F(cout,a)应该返回一个对象 X,这样就存在一个接受 X 和 b 的 F 的重载,依此类推。最简单的情况,F(cout,a)只是返回 cout。
你现在要详细阐述这个论点。
9.3.1.带有()和[]的参数链
有时运算符()用于形成链,从一个函数对象开始。
让我们分析一些假设的代码:
double f(int, double, char, const char*);
double r1 = bind_to(f)(argument<2>('p')) (37, 3.14, "hello");
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// produces a new function:
//
// double f1(int a, double b, const char* c)
// { return f(a, b, 'p', c);
double r2
= bind_to(f)(argument<0>(17))(argument<2>('p')) (3.14, "hello");
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// produces a new function:
// double f2(double b, const char* c)
// { return f(17, b, 'p', c);
假设 f 是一个带 N 个参数的函数,您可以猜测以下事实:
-
bind_to(f)用两个不同的操作符()返回一个对象。
-
第一种形式采用一个语法为 argument
(x)的表达式,并返回一个将 x 固定为 f 的第 k 个参数的函子。可以重复调用第一种形式来固定同一语句中的几个参数。 -
The second operator() takes all the remaining arguments at a time and evaluates the function.
另一个典型的例子是一个需要几个对象的函数(通常是仿函数或访问器,但是没有形式上的要求),您不希望将它与其他参数混淆,因为:
- 太多了:F(...、x1、x2、x3、x4...).
- 它们不能按照“改变默认值的概率递减”来排序,调用者可能不得不将任意值放在未指定的位置。f(...,X1 x1 = X1(),X2 x2 = X2()...)可能需要作为 F(...,X1(),X2(),...,x7,X8(),...).
- 每个对象都与一个不同的模板参数相关联,比如 X1,X2...,所以错误地交换了两个参数的函数调用可能会编译。10
为了说明这种情况,让我们挑选一个需要三个对象的算法:一个 less-comparator、一个一元谓词和一个 logger:
template <typename iterator_t, typename less_t, typename pred_t, typename logger_t>
void MyFunc(iterator_t b, iterator_t e, less_t less, pred_t p, logger_t& out)
{
std::sort(b, e, less);
iterator_t i = std::find_if(b, e, p);
if (i != e)
out << *i;
}
但是,所有这些参数都有一个默认类型,即 std::ostream 作为 logger(STD::cout 作为 out 的默认值)和以下两种类型:
struct basic_comparator
{
template <typename T>
bool operator()(const T& lhs, const T& rhs) const
{ return lhs < rhs; }
};
struct accept_first
{
template <typename T>
bool operator()(const T&) const { return true; }
};
你可能经常想改变其中的一个,也许是最后一个。但是,很难提供重载,因为参数的类型无法区分:
template <typename iterator_t, typename less_t >
void MyFunc(iterator_t b, iterator_t e, less_t less)
{ ... }
template <typename iterator_t, typename logger_t>
void MyFunc(iterator_t b, iterator_t e, logger_t& out)
{ ... }
// ambiguous: these functions will generate errors, if given a named variable as 3rd argument
所以你使用参数包技术。首先,您标记参数。
enum { LESS, UNARY_P, LOGGER };
template <size_t CODE, typename T = void>
struct argument
{
T arg;
argument(const T& that)
: arg(that)
{
}
};
template <size_t CODE>
struct argument<CODE, void>
{
argument(int = 0)
{
}
template <typename T>
argument<CODE, T> operator=(const T& that) const
{
return that;
}
argument<CODE, std::ostream&> operator=(std::ostream& that) const
{
return that;
}
};
然后,您提供命名的全局常量:
const argument<LESS> comparator = 0;
const argument<UNARY_P> acceptance = 0;
const argument<LOGGER> logger = 0;
template <typename T1, typename T2, typename T3>
struct argument_pack
{
T1 first;
T2 second;
T3 third;
argument_pack(int = 0)
{
}
argument_pack(T1 a1, T2 a2, T3 a3)
: first(a1), second(a2), third(a3)
{
}
argument_pack::operator[] 接受一个参数< N,T >并用 T 替换它的第 N 个模板参数:
template <typename T>
argument_pack<T, T2, T3> operator[](const argument<0, T>& x) const
{
return argument_pack<T, T2, T3>(x.arg, second, third);
}
template <typename T>
argument_pack<T1, T, T3> operator[](const argument<1, T>& x) const
{
return argument_pack<T1, T, T3>(first, x.arg, third);
}
template <typename T>
argument_pack<T1, T2, T> operator[](const argument<2, T>& x) const
{
return argument_pack<T1, T2, T>(first, second, x.arg);
}
};
这段代码引入了一个名为 where 的全局常量,并重载了原始函数两次(不考虑参数的实际数量):
typedef argument_pack<basic_comparator, accept_first, std::ostream&> pack_t;
// note: a global variable called "where"
static const pack_t where(basic_comparator(), accept_first(), std::cout);
template <typename iterator_t, typename T1, typename T2, typename T3>
void MyFunc(iterator_t b, iterator_t e, const argument_pack<T1,T2,T3> a)
{
return MyFunc(b, e, a.first, a.second, a.third);
}
template <typename iterator_t >
void MyFunc(iterator_t b, iterator_t e)
{
return MyFunc(b, e, where);
}
所以现在可以写:
MyFunc(v.begin(), v.end(), where[logger=std::clog]);
MyFunc(v.begin(), v.end(), where[logger=std::cerr][comparator=greater<int>()]);
记录器是一个类型参数< 2,void >的常量,它被升级到参数< 2,STD::ostream&T5。这个实例用 std::ostream &替换 pack_t 的第三个模板参数,用对 std::cerr 的引用替换 pack_t::third 的值。
注意,本节中显示的代码不是通用的,但是它与特定的函数调用紧密相关。然而,需要参数包的复杂函数通常在每个项目中只需要几个。
9.4.成长物体概念
先说个例子。字符串 sum 具有内存重新分配的预期成本 11 :
template <typename T>
std::string operator+(std::string s, const T& x)
{
// estimate the length of x when converted to string;
// ensure s.capacity() is large enough;
// append a representation of x to the end of s;
return s;
}
显然,如果同一行上有多个 sum,编译器知道参数的顺序:
std::string s = "hello";
std::string r = s + ' ' + "world!";
// repeated invocation of operator+ with arguments: char, const char*
// may cause multiple memory allocations
所以你会想:
- 一次收集所有的论点,并对它们的长度求和
- 执行一次内存分配
- 再次遍历参数序列并将它们连接起来
增长对象是一种允许在执行前遍历 C++ 表达式的模式。该技术的思想是在表达式中注入一个带有特殊操作符的代理,该操作符“吸收”所有后续参数。
代理 是一个临时的聚集对象,它的操作者让它“增长”,包括对它们的参数的引用。最后,当增长完成时,对象可以一次处理所有的参数,并将它们转换成想要的结果。
因此,在前面的示例中,s+' '不是一个字符串,而是一个包含对 s 的引用和一个字符的代理。当添加“world”时,该对象会增长,因此 s+“+”world 也包含一个 const char*。
非正式地说,一个正在增长的对象被实现为一对,包含该对象以前的状态和一些新的微小数据(比如一个引用)。此外,“pair”还有三种可能的变体:
- 一个有两个成员的类:对前一个增长对象和一个小对象的引用
- 有两个成员的类:前一个增长对象的副本和一个小对象
- 从以前的增长对象派生的类,以一个小对象作为唯一成员
在伪模板符号中,三种不同的模型可以写成:
template <...>
class G1<N>
{
const G1<N-1>& prev_;
T& data_;
};
template <...>
class G2<N>
{
G2<N-1> prev_;
T& data_;
};
template <...>
class G3<N> : public G3<N-1>
{
T& data_;
};
第一种是构建速度最快的,因为用新数据扩充临时对象 G1 不涉及复制,但是 G1 的生存期是最短的。其他类型具有相似的复杂性,因为它们的构造无论如何都要复制 Gj
G1 的最大优点是构造函数和析构函数都只按顺序运行一次。相反,要创建一个 G2
这一点尤其重要,因为当增长完成时,你可能需要 G
*这些 Gj 中的任何一个都包含引用,所以举例来说,没有增长的对象可以被抛出。
此外,在计算结果时有一些已知的递归模式:
-
向内链接 : G < N >要么直接计算结果,要么委托 G < N-1 >,向内传递信息:
private: result do_it_myself() { // ... } result do_it(arguments) { if (condition) return do_it_myself(); else return prev_.do_it(arguments); } public: result do_it() { return do_it(default); }
-
外向链接 : G < N >递归地从 G < N-1 >中求一个结果,并对其进行后处理。
result do_it() { result temp = prev_.do_it(); return modify(temp); }
-
直接访问 : G < N >计算 J 并向 G < J >请求一个结果。这种模式对于基于继承的增长对象有不同的实现。
template <...> class G1<N> { result do_it_myself(static_value<int, 0>) { // really do it } template <int K> result do_it_myself(static_value<int, K>) { return prev_.do_it_myself(static_value<int, K-1>()); } public: result do_it() { static const int J = [...]; return do_it_myself(static_value<int, J>()); } }; template <...> class G3<N> : G3<N-1> { result do_it_myself() { // ... } public: result do_it() { static const int J = ...; return static_cast<growing<J>&>(*this).do_it_myself(); } };
9.4.1.字符串串联
使用一系列聚集体实现第一个生长对象(参见第 3.6.8 节)。
因为单个语句中涉及的对象至少存在到表达式的末尾,所以您可以认为是一个 const 引用的集合。表达式(string+T1)+T2 不应返回一个字符串,而应返回一个包含参数引用的结构(或副本,如果它们很小)。 12
template <typename T1, typename T2>
class agglomerate;
template <typename T>
agglomerate<string, const T&> operator+(const string&, const T&);<sup class="calibre7">13</sup>
template <typename T1, typename T2, typename T>
agglomerate<agglomerate<T1, T2>, const T&>
operator+(const agglomerate<T1, T2>, const T&);
因此,下面的原型示例中的 sum 将返回 aggregate< agglomerate
std::string s = "hello";
std::string r = s + ' ' + "world!";
最终,所有的工作都由一个 cast 操作符完成,它将 agglomerate 转换为 string:
- 将 this->first 和 this->second 的长度相加(first 是另一个凝聚或字符串,所以两者都有 size()函数;第二个是对新参数的引用)。
- 分配大小合适的字符串。
- 将所有对象附加到字符串的末尾,知道内部不会发生重新分配。
请注意,关于参数,聚集是以相反的顺序构建的;也就是说,执行转换的对象持有最后一个参数。因此,它必须在转储参数成员之前转储其聚集成员。
// using namespace std;
template <typename T, bool SMALL = (sizeof(T)<=sizeof(void*))>
struct storage_traits;
template <typename T>
struct storage_traits<T, true>
{
typedef const T type;
};
template <typename T>
struct storage_traits<T, false>
{
typedef const T& type;
};
// assume that T1 is string or another agglomerate
// and T2 is one of: char, const char*, std::string
template <typename T1, typename T2>
class agglomerate
{
T1 first;
typename storage_traits<T2>::type second;
void write(string& result) const
{
// member selection based on the type of 'first'
write(result, &first);
}
template <typename T>
void write(string& result, const T*) const
{
// if we get here, T is an agglomerate, so write recursively:
// mind the order of functions
first.write(result);
result += this->second;
}
void write(string& result, const string*) const
{
// recursion terminator:
// 'first' is a string, the head of the chain of arguments
result = first;
}
size_t size()
{
return first.size() + estimate_length(this->second);
}
static size_t estimate_length(char)
{
return 1;
}
static size_t estimate_length(const char* const x)
{
return strlen(x);
}
static size_t estimate_length(const string& s)
{
return s.size();
}
public:
operator string() const
{
string result;
result.reserve(size());
write(result);
return result; // NVRO
}
};
第一个增强功能允许通过链在单遍中累积信息:
void write(string& result, size_t length = 0) const
{
write(result, &first, length + estimate_length(this->second));
}
template <typename T>
void write(string& result, const T*, size_t length) const
{
first.write(result, length);
result += this->second;
}
void write(string& result, const string*, size_t length) const
{
result.reserve(length);
result = first;
}
operator string() const
{
string result;
write(result);
return result;
}
std::string s = "hello";
std::string r = s + ' ' + "world!";
在经典 C++ 中,每次调用 string::operator+都会返回一个不同的临时对象,只是简单的复制。所以最初的例子产生了两个中间字符串:即 t 1 ="hello "和 t 2 ="hello world!"。由于每个临时变量都包含一个副本,因此这具有二次复杂度。
对于 C++0x 语言扩展,std::string 是一个可移动的对象。换句话说,当一个参数是临时对象时,它的操作符会检测到,并允许您窃取或重用它的资源。所以前面的代码实际上可能调用了两个不同的求和运算符。第一个无论如何都会产生一个临时变量(因为不允许从局部变量中偷取数据);第二个检测临时的并重用它的内存。
从概念上讲,实现可能如下所示:
string operator+(const string& s, char c)
{
string result(s);
return result += c;
}
字符串运算符+(string&&****tmp, const char* c)
{
string result;
result.swap(tmp);
return result += c;
}
其中符号字符串&&表示对临时的引用。甚至更简单:
string operator+(string s, char c)
{
return s += c;
}
string operator+(string&& tmp, const char* c)
{
return tmp += c;
}
换句话说,C++0x 字符串和在概念上类似于: 14
std::string s = "hello";
std::string r = s;
r += ' ';
r += "world!";
但是一个不断增长的对象表现得更好,相当于:
std::string s = "hello";
std::string r;
r.reserve(s.size()+1+strlen("world!");
r += s;
r += ' ';
r += "world!";
因此,C++0x 扩展本身不会比增长的对象获得更好的性能。
9.4.2.可变增长对象
一个成长的对象可以用来提供增强的断言: 15
std::string s1, s2;
...
SMART_ASSERT(s1.empty() && s2.empty())(s1)(s2);
Assertion failed in matrix.cpp: 879412:
Expression: 's1.empty() && s2.empty()'
Values: s1 = "Wake up, Neo"
s2 = "It's time to reload."
这段代码可以用一个简单的可链接操作符()来实现:
class console_assert
{
std::ostream& out_;
public:
console_assert(const char*, std::ostream& out);
console_assert& operator()(const std::string& s) const
{
out_ << "Value = " << s << std::endl;
return *this;
}
console_assert& operator()(int i) const;
console_assert& operator()(double x) const;
// ...
};
#define SMART_ASSERT(expr) \
if (expr) {} else console_assert(#expr, std::cerr)
这个宏使用 operator()开始一个参数链,因为它不是一个增长对象,所以必须立即使用参数。但是你可以有一个更复杂的“懒惰”方法: 16
template <typename T1, typename T2>
class console_assert
{
const T1& ref_;
const T2& next_;
mutable bool run_;
public:
console_assert(const T1& r, const T2& n)
: ref_(r), next_(n), run_(false)
{}
std::ostream& print() const
{
std::ostream& out = next_.print();
out << "Value = " << ref_ << std::endl;
run_ = true;
return out;
}
template <typename X>
console_assert<X, console_assert<T1, T2> >
operator()(const X& x) const
{
return console_assert<X, console_assert<T1, T2> >(x, *this);
}
~console_assert()
{
if (!run_)
print();
}
};
template < >
class console_assert<void, void>
{
std::ostream& out_;
public:
console_assert(const char* msg, std::ostream& out)
: out_(out << "Assertion failed: " << msg << std::endl)
{
}
std::ostream& print() const
{
return out_;
}
template <typename X>
console_assert<X, console_assert<void, void> >
operator()(const X& x) const
{
return console_assert<X, console_assert<void, void> >(x, *this);
}
};
#define SMART_ASSERT(expr) \
if (expr) {} else console_assert<void, void>(#expr, std::cerr)
前面的例子表明,可以从内部修改增长的对象,通过成员窃取或传递资源。
特别是,一步一步的代码扩展会产生以下结果:
SMART_ASSERT(s1.empty() && s2.empty())(s1)(s2);
if (s1.empty() && s2.empty())
{}
else
console_assert<void, void>("s1.empty() && s2.empty()", std::cerr)(s1)(s2);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// constructor of console_assert<void, void>
如果断言为假,则创建三个嵌套的临时变量:
| ?? | 控制台 _ 断言 |
| 一种网络的名称(传输率可达 1.54mbps) | 控制台 _ 断言
| ?? | 控制台 _ 断言
- ?? 被创造出来,然后立即被摧毁。因为 run_ 为 false,所以它调用 print。
- 打印通话 next_.print。
- ?? 将其流传递给 ??。
- ?? 打印它的消息,设置 run_=true,并把数据流向上传递给 ??。
- ?? 印刷了它的信息,然后死去。
- ?? 被销毁,但由于 run_ 为真,它保持沉默。
- ?? 被破坏。
控制台 _ 断言、??【void】和 void >等专门化称为链式启动器 。它的界面可能与通用模板有很大不同。
增长对象的接口通常不依赖于粘合在一起的参数的数量。 17
9.4.3.更多生长物体
概括这个模式,给定一个类型容器 C,实现一个通用的聚集链
为简单起见,关于如何在链中存储参数(通过复制还是通过引用)的决定由一个全局策略给出: 18
template <typename T>
struct storage_traits
{
typedef const T& type;
};
template <typename T>
struct storage_traits<T*>
{
typedef T* type;
};
template <typename T>
struct storage_traits<const T*>
{
typedef const T* type;
};
template <>
struct storage_traits<char>
{
typedef char type;
};
在“凝聚”期间,长度为 N 的链和一个新的自变量生成长度为 N+1 的链。新的链存储这两者并组合一些新的信息(例如,它将旧链的估计长度与新参数的预期长度相加)。
最终,这条信息被发送到“目标对象”,然后该对象以某种顺序接收所有参数。
因为所有这些动作都是参数化的,所以您可以将它们组合在一个 traits 类中:
- update 从参数中收集信息,一次一个。
- dispatch 将累积信息发送给目标对象。
- transmit 将实际参数发送给目标对象。
struct chain_traits
{
static const bool FORWARD = true;
struct information_type
{
// ...
};
typedef information_type& reference;
typedef ... target_type;
template <typename ARGUMENT_T>
static void update(const ARGUMENT_T&, information_type&);
static void dispatch(target_type&, const information_type&);
template <typename ARGUMENT_T>
static void transmit(target_type&, const ARGUMENT_T&);
};
对象增长期间将自动调用 Update 如果链被强制转换为 target_type 或注入 target _ type,将延迟调用 dispatch 和 transmit。
首先实现空链。
类似于上面 9.4.2 节中的流引用,这个类将只存储公共信息。增长对象的附加层将使用 traits::reference 引用它。
template <typename traits_t, typename C = empty>
class chain;
template <typename traits_t>
class chain<traits_t, empty>
{
template <typename ANY1, typename ANY2>
friend class chain;
typedef typename traits_t::information_type information_type;
typedef typename traits_t::target_type target_type;
information_type info_;
void dump(target_type&) const
{
}
public:
explicit chain(const information_type& i = information_type())
: info_(i)
{
}
#define PLUS_T \
chain<traits_t, typename push_front<empty, T>::type>
template <typename T>
PLUS_T operator+(const T& x) const
{
return PLUS_T(x, *this);
}
const chain& operator >> (target_type& x) const
{
x = target_type();
return *this;
}
operator target_type() const
{
return target_type();
}
};
非空链包含:
- 类型为 front
的数据成员,存储为 storage_traits <local_t>::type。</local_t> - 由常量引用存储的类型为 chain <pop_front>>,的链。因为 C 不是空的,你可以安全地弹出它。</pop_front>
- 对信息对象的引用。信息类型的存储依赖于特征。可能是副本(当 traits_t::reference 和 traits_t::information_type 相同时)也可能是真引用。
operator+调用的私有构造函数首先复制尾链携带的信息,然后用新参数更新它。
template <typename traits_t, typename C>
class chain
{
template <typename ANY1, typename ANY2>
friend class chain;
typedef typename traits_t::target_type target_type;
typedef typename front<C>::type local_t;
typedef chain<traits_t, typename pop_front<C>::type> tail_t;
typename storage_traits<local_t>::type obj_;
typename traits_t::reference info_;
const tail_t& tail_;
void dump(target_type& x) const
{
tail_.dump(x);
traits_t::transmit(x, obj_);
}
chain(const local_t& x, const tail_t& t)
: obj_(x), tail_(t), info_(t.info_)
{
traits_t::update(x, info_);
}
public:
template <typename T>
chain<traits_t,typename push_front<C,T>::type> operator+(const T& x) const
{
typedef
chain<traits_t, typename push_front<C, T>::type> result_t;
return result_t(x, *this);
}
const chain& operator >> (target_type& x) const
{
traits_t::dispatch(x, info_);
dump(x);
return *this;
}
operator target_type() const
{
target_type x;
*this >> x;
return x;
}
};
私有转储成员函数负责将所有参数递归传输到目标。请注意,您可以使遍历参数化,并用一个简单的布尔值来反转它:
void dump(target_type& x) const
{
if (traits_t::FORWARD)
{
tail_.dump(x);
traits_t::transmit(x, obj_);
}
else
{
traits_t::transmit(x, obj_);
tail_.dump(x);
}
}
最后,您展示了用于字符串连接的 traits 类的概要:
struct string_chain_traits
{
static const bool FORWARD = true;
typedef size_t information_type;
typedef size_t reference;
typedef std::string target_type;
template <typename ARGUMENT_T>
static void update(const ARGUMENT_T& x, information_type& s)
{
s += estimate_length(x);
}
static void dispatch(target_type& x, const information_type s)
{
x.reserve(x.size()+s);
}
template <typename ARGUMENT_T>
static void transmit(target_type& x, const ARGUMENT_T& y)
{
x += y;
}
};
typedef chain<string_chain_traits> begin_chain;
std::string q = "lo ";
std::string s = (begin_chain() + "hel" + q + 'w' + "orld!");
图 9-1。链图。对象是自下而上构造的
- 因为不允许修改 std::string,所以必须用默认构造的对象显式地开始这个链。
- 在链开始之前只运行一次的代码可以放在 information_type 构造函数中。然后可以用 begin_chain(参数)开始这个链。
- 存储策略是一个可以透明地插入定制代码来执行转换的地方。例如,为了加速 int 到 string 的转换,您可以编写:
template <>
struct storage_traits<int>
{
class type
{
char data_[2+sizeof(int)*5/2];<sup class="calibre7">19</sup>
public:
type(const int i)
{
// perform the conversion here
_itoa(i, data_, 10);
}
operator const char* () const
{
return data_;
}
};
};
9.4.4.连锁破坏
可以在链析构函数 中编写自定义代码。
由于每个链只有一个副本(它们由常量引用链接),所以链段是按照从第一个参数到最后一个参数的顺序构造的。它们将以相反的顺序被销毁。您有机会在语句结束时执行一些终结操作。
~chain()
{
traits_t::finalize(obj_, info_);
}
std::string s;
std::string q = "lo ";
(begin_chain() + "hel" + q + 'w' + "orld!") >> s;
然后最左边的对象会追加“hello world!”至多进行一次重新分配。最后,析构函数将以相反的顺序(从左到右)运行 finalize。
如果链是按值存储的,那么破坏的顺序是固定的(首先是对象,然后是其成员)。但是每个子链会有多个副本(即 operator+返回的所有 temporaries)。显然,如果 C1 持有 C0 的副本,C2 持有 C1 的副本,那么就有 C0 的三个副本,因此,不需要一些额外的工作,你就不会知道哪个子链正在被销毁。
9.4.5.生长物体的变化
如果您必须将增长的对象添加到一个只读类中(如 std::string 所示),而不是手动插入一个链起始符,您可以:
- 用处理第一个参数的全局函数替换链起始符(这相当于将空链的运算符+提升为一个函数)。
- 切换到运算符()进行连接(这使得括号语法一致)。
template <typename traits_t, typename T>
chain<traits_t,typename push_front<empty,T>::type> concatenate(const T& x)
{
typedef chain<traits_t, typename push_front<empty, T>::type> result_t;
return result_t(x, chain<traits_t>());
}
std::string s = concatenate("hello")(' ')("world");
另一个变化涉及结果的提取。有时,强制转换运算符并不可取。您可能决定用流插入语法替换=和+,所以您应该写:
std::string s;
s << begin_chain() << "hello" << ' ' << "world";
这是可行的,但需要一些技巧来打破这种结合性,因为语言规则会使编译器执行:
(s << begin_chain()) << "hello" << ' ' << "world";
^^^^^^^^^^^^^^^^^^^
虽然您更喜欢:
s << (begin_chain() << "hello" << ' ' << "world");
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
在旧的方法中,结果是最后一条信息;现在是第一。所以你得改装链条,随身携带。您在空链中存储一个指向结果的指针,这样它只能被读取一次。非寻常运算符<< fills this pointer and then returns its 第二个自变量,不是第一个;这是结合性断路器。
本节仅简要介绍了与之前实现的不同之处:
template <typename traits_t, typename C = empty>
class chain;
template <typename traits_t>
class chain<traits_t, empty>
{
// ...
mutable target_type* result_;
public:
// ...
const chain& bind_to(target_type& x) const
{
result_ = &x;
return *this;
}
target_type* release_target() const
{
target_type* const t = result_;
result_ = 0;
return t;
}
};
template <typename traits_t>
const chain<traits_t>& operator<<(typename traits_t::target_type& x,
const chain<traits_t>& c)
{
return c.bind_to(&x);
}
template <typename traits_t, typename C>
class chain
{
// ...
target_type* release_target() const
{
return tail_.release_target();
}
public:
template <typename T>
chain<traits_t, typename push_front<C,T>::type> operator<<(const T& x) const
{
typedef chain<traits_t, typename push_front<C, T>::type> result_t;
return result_t(x, *this);
}
~chain()
{
if (target_type* t = release_target())
dump(*t);
}
};
链中的最后一个对象会首先被销毁,它将是 release_target 中唯一成功的对象。
9.5.流
正如上一节所介绍的,流插入语法是最统一的语法之一,因此它在视觉上清晰而灵活,并对定制开放。
9.5.1.自定义操纵器和流插入
假设您想通过流插入以 C++ 方式打印一个位串(见 5.2.3 节)。
一个 bitstring 同时实现很多静态接口。
class bitstring
: public pseudo_array<bitstring, bit_tag>
, public pseudo_array<bitstring, nibble_tag>
, public pseudo_array<bitstring, byte_tag>
{ ... };
如何决定哪个接口应该将其数据发送给流?换句话说,如何优雅地在逐位、逐字节和半字节打印之间进行选择?
回想一下,操纵器是在流中流动的对象,获取流对象,并修改其状态: 20
using namespace std;
ostream& flush(ostream& o)
{
// flush the stream, then...
return o;
}
// a manipulator is a function pointer that takes and returns a stream by reference
typedef ostream& (*manip_t)(ostream&);
ostream& operator<<(ostream& o, manip_t manip)
{
manip(o);
return o;
}
cout << flush << "Hello World!";
请注意,虽然有些对象会永久修改流的状态,但通常在下一次插入后,操纵器插入的效果会消失。在前面的代码中,cout 将需要在插入字符串后重新刷新。
然而,没有什么能阻止操纵器返回一个完全不同的流。作为子表达式的一部分,原始流肯定是活动的,因此可以将其封装在一个 shell 中,该 shell 拦截对 operator <
class autoflush_t
{
ostream& ref;
public:
autoflush_t(ostream& r)
: ref(r)
{}
template <typename T>
autoflush_t& operator<<(const T& x)
{
ref << x << flush;
return *this;
}
operator ostream& () const
{
return ref;
}
};
autoflush_t* autoflush() { return 0; }
inline autoflush_t operator<<(ostream& out, autoflush_t* (*)())
{
return autoflush_t(out);
}
cout << autoflush << "Hello" << ' ' << "World";
autoflush 之后的所有插入实际上都是对 autoflush_t::operator <
还要注意,代码使用代理本身为操纵器生成一个唯一的签名。
一个流代理不必是持久的。它可能会实现自己的特殊插入和一个通用操作符,如果下一个对象不是预期的,它会再次“打开”流。
假设您有一个特殊的 double 格式化程序:
class proxy
{
ostream& os_;
public:
explicit proxy(ostream& os)
: os_(os)
{
}
ostream& operator<<(const double x) const
{
// do the actual work here, finally clear the effect of the
// manipulator, unwrapping the stream
return os_;
}
// the default insertion simply reveals the enclosed stream
template <typename T>
ostream& operator<<(const T& x) const
{
return os_ << x;
}
};
proxy* special_numeric() { return 0; }
inline proxy operator<<(ostream& os, proxy* (*)())
{
return proxy(os);
}
cout
<< special_numeric << 3.14 // ok, will format a double
<< special_numeric << "hello"; // ok, the manipulator has no effect
如果不是模板操作符<< is omitted, a double will be 要求在机械手之后。
总而言之,通过改变运算符<
- 只要插入了 X 的一个实例,就只影响下一次插入;否则,它们将被忽略。
- 仅影响下一次插入,并要求紧接其后插入 X;否则,就会出现编译错误。
- 影响所有后续插入,直到子表达式结束。
- 影响所有后续插入,直到插入 X:
template <typename any_t>
proxy_dumper& operator<<(const any_t& x) const
{
os_ << x;
return *this;
}
这正是你需要的 bitstring 的解决方案;将静态接口类型标签视为操纵器。Insertion 返回一个模板代理,它使用静态接口本身的合适函数,根据(静态已知的)类型标签格式化下一个位串。
using std::ostream;
template <typename digit_t>
class bistring_stream_proxy
{
ostream& os_;
public:
bistring_stream_proxy(ostream& os)
: os_(os)
{
}
ostream& operator<<(const pseudo_array<bitstring, digit_t>& b) const
{
b.dump(os_);
return os_;
}
template <typename any_t>
ostream& operator<<(const any_t& x) const
{
return os_ << x;
}
};
inline bistring_stream_proxy<bit_t> operator<<(ostream& o, bit_t)
{
return bistring_stream_proxy<bit_t>(o);
}
inline bistring_stream_proxy<octet_t> operator<<(ostream& o, octet_t)
{
return bistring_stream_proxy<octet_t>(o);
}
inline bistring_stream_proxy<nibble_t> operator<<(ostream& o, nibble_t)
{
return bistring_stream_proxy<nibble_t>(o);
}
9.5.2.带有增长对象的范围插入
另一个练习是在流中插入一个范围。您需要一个自定义项目来开始一个链:
cout << range << begin << end;
第一个代理(由 std::cout << range 返回)接受一个迭代器并增长(参见上一节)。插入第二个同类迭代器会触发完全转储:
template <typename iterator_t = void*>
class range_t
{
std::ostream& ref_;
iterator_t begin_;
public:
explicit range_t(std::ostream& ref)
: ref_(ref), begin_()
{
}
range_t(range_t<> r, iterator_t i)
: ref_(r.ref_), begin_(i)
{
}
std::ostream& operator<<(iterator_t end)
{
while (begin_ != end)
ref_ << *(begin_++);
return ref_;
}
std::ostream& operator<<(size_t count)
{
while (count--)
ref_ << *(begin_++);
return ref_;
}
};
range_t<>* range() { return 0; }
inline range_t<> operator<<(std::ostream& os, range<>* (*)())
{
return range_t<>(os);
}
template <typename iterator_t>
inline range_t<iterator_t> operator<<(range_t<> r, iterator_t begin)
{
return range_t<iterator_t>(r, begin);
}
范围代理接受由[begin...end)或由[begin,N]决定。从理论上讲,可以更加专业化:
template <typename iterator_t = void*>
class range_t
{
private:
// ...
void insert(iterator_t end, std::random_access_iterator_tag)
{
// faster algorithm here
}
public:
// ...
std::ostream& operator<<(iterator_t end)
{
insert(end, typename iterator_traits<iterator_t>::iterator_category());
return ref_;
}
};
9.6.逗号链
逗号运算符 有时会与赋值一起重载,以获得某种形式的惰性/迭代初始化。这模仿了常见的 C 数组初始化语法:
int data[] = { 1,2,3 };
// equivalent to:
// data[0] = 1; data[1] = 2; data[2] = 3
因为标准的结合性规则,不管它的意义如何,这样一个表达式:
A = x, y, z;
被编译为
(((A = x), y), z);
每个逗号实际上是一个二元运算符,所以实际上
((A.operator=(x)).operator,(y)).operator,(z)
注意这个语法和增长对象之间的区别。后者将赋值右侧的所有项目从左到右关联起来:
A = ((x+y)+z);
在这里,您有机会迭代地修改 A,因为表达式中包含 A 的部分是最先计算的部分:
- 定义一个代理对象 P
- 定义 P
- 定义 A::operator=(x)为返回 P
假设您有一个 C 数组的包装器:
template <typename T, size_t N>
struct array
{
T data[N];
};
作为具有公共成员的结构,这样的对象可以用花括号语法初始化:
array<double, 4> a = { 1,2,3,4 };
但是,您不能在现有对象上执行相同的操作: 21
array<double, 4> a = { 1,2,3,4 };
// ok, but now assign {5,6,7,8} to a...
const array<double, 4> b = { 5,6,7,8 };
a = b;
// is there anything better?
让赋值返回一个带有特殊逗号运算符的代理:
template <typename T, size_t N>
struct array
{
T data[N];
private:
template <size_t J>
class array_initializer
{
array<T, N>* const pointer_;
friend struct array<T, N>;
template <size_t K>
friend class array_initializer;
array_initializer(array<T, N>* const p, const T& x)
: pointer_(p)
{
MXT_ASSERT(J<N);
pointer_->data[J] = x;
}
作为 operator=的结果,代理在概念上等同于引用,所以通过声明成员常量(如本例)或引用来禁止复制和赋值是很自然的。
为了方便起见,代理是数组的内部类,它的构造函数是私有的;数组本身和所有代理都是朋友。请注意,构造函数执行(安全)赋值。
该代理有一个公共逗号操作符,它构造了另一个代理,将索引移动到下一个位置。因为用户希望表达式 A = x 返回对 A 的引用,所以您还可以添加一个转换运算符:
class array_initializer
{
// ...
public:
array_initializer<J+1> operator, (const T& x)
{
return array_initializer<J+1>(pointer_, x);
}
operator array<T, N>& ()
{
return *pointer_;
}
}; // end of nested class
最后,数组赋值只是构造了第一个代理:
public:
array_initializer<0> operator=(const T& x)
{
return array_initializer<0>(this, x);
}
};
请注意,该片段:
array<int,4> A;
A = 15,25,35,45;
大致相当于:
((((A = 15),25),35),45);
其中,如前所述,每个逗号都是一个运算符。这个表达式在 array::operator=之后,在编译时扩展为:
(((array_initializer<0>(A, 15), 25), 35), 45);
array_initializer <0>的构造设置一个[0]=15,然后 array_initializer 逗号运算符构造另一个赋值为[1]的初始值设定项,以此类推。
为了构建一个临时的 array_initializer ,你必须将一个 const 指针存储在一个临时的堆栈上,所以整个过程在某种程度上等价于:
array<int,4>* const P1 = &A;
P1->data[0] = 15;
array<int,4>* const P2 = P1;
P2->data[1] = 25;
array<int,4>* const P3 = P2;
P3->data[2] = 35;
array<int,4>* const P4 = P3;
P4->data[3] = 45;
如果编译器可以传播所有赋值都涉及的信息,那么代码就相当于手写的初始化。所有的 const 修饰符只是给编译器的提示,使其分析更容易。
逗号链经常利用另一种语言属性:临时代理对象的销毁。一般来说,问题可以表述为:代理怎么知道自己是不是最后一个?
在上例中,您可能会喜欢:
array<int,4> A;
A = 15,25; // equivalent to {15,25,0,0}
并且
array<int,4> A;
A = 15; // equivalent to {15,15,15,15} not to {15,0,0,0}
表达式编译为 array_initializer <0> (&A,15)。操作员,(25);这将返回 array_initializer <1> (&A,25)。
代理可以向下一个代理传输信息的唯一方式是通过逗号操作符。对象可以跟踪调用,其析构函数可以执行相应的操作:
template <size_t J>
class array_initializer
{
array<T, N>* pointer_; // <-- non-const
public:
array_initializer(array<T, N>* const p, const T& x)
: pointer_(p)
{
MXT_ASSERT(J<N);
p->data[J] = x;
}
array_initializer<J+1> operator, (const T& x)
{
array<T, N>* const p = pointer_;
pointer_ = 0; // <-- prevent method re-execution
return array_initializer<J+1>(p, x);
}
~array_initializer()
{
// if operator, has not been invoked
// then this is the last proxy in chain
if (pointer_)
{
if (J == 0)
std::fill_n(pointer_->data+1, N-1, pointer_->data[0]);
else
std::fill_n(pointer_->data+(J+1), N-(J+1), T());
}
}
};
一般来说,改变析构函数的语义是有风险的。然而,在这里,您可以假设这些对象不应该被存储或复制,并且实现强制这样做,以便(非恶意)用户不能人为地延长这些代理的寿命。 22
- 将代理放在数组的私有部分,这样用户就无法访问它的名称。
- 将所有危险的操作符声明为非 const,这样,如果通过 const 引用将代理传递给函数,就不能调用它们。非常数引用必须引用非临时变量,这不太可能。
- 禁止复制建设。
虽然有可能执行非法操作,但它确实需要恶意代码:
template <typename T>
T& tamper(const T& x)
{
T& r = const_cast<T&>(x);
r, 6.28;
return r;
}
array<double, 10> A;
array<double, 10> B = tamper(A = 3.14);
- tamper 的参数是 const T&,可以绑定到任何临时。因此,它击败了名称隐藏保护。
- const_cast 移除 const 保护,并使逗号运算符可调用。
- r.operator,(6.28)作为副作用设置 r.pointer_ = 0。
- 当编译器将要构造 B 时,返回的引用仍然有效,但是转换运算符取消了对空指针的引用。
注意,像 tamper 这样的函数看起来无害,可以为每个测试编译
9.7.模拟中缀
让我们来分析下面的片段:
double pi = compute_PI();
assert(pi IS_ABOUT 3.14);
我们不会解决比较浮点数的问题,但是这一段会给你一个模拟新中缀的思路。如果一个表达式包含不同优先级的运算符,您可以在执行左边部分之前控制右边部分(反之亦然)。例如,IS_ABOUT 可以是扩展到以下内容的宏:
assert(pi == SOMETHING() + 3.14);
SOMETHING::operator+先运行,所以你马上捕获 3.14。然后一个合适的操作员==负责左侧。
下面是一些代码:
template <typename float_t>
class about_t
{
float_t value_;
public:
about_t(const float_t value)
: value_(value)
{
}
bool operator==(const float_t x) const
{
const float_t delta = std::abs(value_ - x);
return delta < std::numeric_limits<float_t>::epsilon();
}
};
template <typename float_t>
inline bool operator==(const float_t x, const about_t<float_t> a)
{
return a == x;
}
struct about_creator_t
{
template <typename float_t>
inline about_t<float_t> operator+(const float_t f) const
{
return about_t<float_t>(f);
}
};
#define IS_ABOUT == about_creator_t() +
显然+和==的作用是可以颠倒的,这样才能先读左边。
还要注意,如果所有这些对象都属于一个名称空间,那么宏应该完全限定 about_creator_t。
好奇的读者可能希望研究下面的算法,它没有给出任何解释。
给出了两个数字 X 和 Y。
- 如果 X==Y,则返回 true。
- 当一个或两个数字都是无穷大或 NAN 时,检查无关紧要的情况,并相应地返回。
- 从 std::numeric_limits 中选择ε。
- 设 D := |X-Y|。
- 设 R := max(|X|,|Y|)。
- 返回 R
该算法的一个变体也测试 D<ε。
1 参见第 12.3 节。
2 在符号中,(F(λ))(x) := F(λ(x)),其中 x 可能是一个元组。
3 再次在符号中,(λ1+λ2)(x) := λ1(x)+λ2(x),其中 x 可能是一个元组。
4
这可以通过在每个 lambda 实现中存储一个名为 min_number_of_arguments 的静态常量来解决。原子 lambda(如 lambda_reference)将直接定义它,派生的 lambda 将从它们的嵌套类型中取最大值。最后,这个常量可以用于静态断言。我们将此作为练习。
6
7 另一方面,赋值和逻辑运算符有一个可推导的返回类型。前者返回它的第一个参数(非常数引用);后者返回 bool。C++0x 的新关键字 decltype 将允许自动推导这种类型。
8 故意使用 valarray 这个名称,是为了说明这些技术适合 std::valarray。
9 换句话说,一个取 const valarray_interface < X,T > &的构造函数。细节应该很容易理解,留给读者。如果运算符返回不能直接修改的类型(如 std::string),则需要 cast 运算符。
10 参见 6.2.1 节背包例子中的“价格”和“质量”。
11 注意 s 是按值传递的。根据 NVRO(命名值返回优化),如果只有一个返回语句,并且结果是一个命名变量,编译器通常可以省略副本,直接在调用方的堆栈上构造结果。
12 参见 9.2.8 节。
13 这个例子显然是虚构的,因为您无法真正将操作符添加到 std::string 中。
14
15 参见 Alexandrescu 和 Torjo 关于断言的文章,也是本段第一个样本的来源:。
16 为了更加清晰,我们从这个例子中省略了信息收集阶段(字符串长度的估计)。事实上,std::ostream 不需要管理。
17 这一点都不明显。在 9.3 节的最后一个例子中,我们考虑了一个凝聚,即 bind_to(f)(argument)...(参数),其语法取决于链的长度。事实上,绑定四个参数中的一个会产生一个接受 4-1=3 个自由参数的函子,依此类推。
18 这允许你呈现简化的代码。您可以轻松地将存储策略添加为模板参数。
19 如果 n 是 int_t 类型的整数,则 n 的十进制位数为 ceil(log10(n+1))。假设一个字节包含 8 位,sizeof(int_t)是偶数,最大的整数是 256^sizeof(int_t)-1.当您用它代替 n 时,您将获得最大数量的 ceil(log10(256)* sizeof(int _ t))~(5/2)* sizeof(int _ t)位数。您应该为符号添加 1,为终止符添加 1。
20 参见 1.4.7 节关于机械手的内容。
21 C++0x 语言扩展允许你用花括号里的列表初始化一些对象(包括 std::array)。更多详情请参考 http://en.cppreference.com/w/cpp/utility/initializer_list 的。
22 异常安全可能是一个依赖问题。如果代理的析构函数执行重要的工作,它可能会抛出。*
十、重构
模板可以被认为是普通类和函数的一般化。通常,由于新的软件需求,已经测试过的预先存在的函数或类被提升为模板;这通常会节省调试时间。
然而,在添加对应于实现细节的模板参数之前要小心,因为它们将成为类型的一部分。差别不大的对象可能无法互操作。再次考虑 1.4.9 节中的例子,这是一个违反此规则的容器:
template <typename T, size_t INITIAL_CAPACITY = 0>
class special_vector;
让操作符测试任意两个 special_vector
一般来说,所有与额外模板参数正交的成员函数要么需要提升为模板,要么需要移动到基类。 1
事实上,有两种实现是可能的:
- 一个模板函数 special_vector
::operator==对于任意 K 取常量 special_vector &
template <typename T, size_t N>
class special_vector
{
public:
template <size_t K>
bool operator==(const special_vector<T, K>&);
// ...
};
- special_vector
继承自公共 special_vector_base 。这个基类有一个受保护的析构函数和运算符= =(const special _ vector _ base &):
template <typename T>
class special_vector_base
{
public:
bool operator==(const special_vector_base<T>&);
// ...
};
template <typename T, size_t N>
class special_vector : public special_vector_base<T>
{
// ...
};
后一个例子允许更多的灵活性。不应该直接使用基类,但是可以将包装器公开为智能指针/引用,以允许特殊向量(具有相同的 T)的任意集合,而没有意外删除的风险。为了说明这一点,假设您要按如下方式稍微更改代码:
template <typename T>
class pointer_to_special_vector;
template <typename T, size_t N>
class special_vector : private special_vector_base<T>
{
// thanks to private inheritance,
// only the friend class will be able to cast special_vector to
// its base class
friend class pointer_to_special_vector<T>;
};
template <typename T>
class pointer_to_special_vector // <-- visible to users
{
special_vector_base<T>* ptr_; // <-- wrapped type
public:
template <size_t K>
pointer_to_special_vector(special_vector<T,K>* b = 0)
: ptr_(b)
{}
// fictitious code...
T at(size_t i) const { return (*ptr_)[i]; }
};
int main()
{
std::list< pointer_to_special_vector<double> > lp;
special_vector<double, 10> sv1;
special_vector<double, 20> sv2;
lp.push_back(&sv1);
lp.push_back(&sv2); // ok, even if sv1 and sv2 have different static types
}
10.1.向后兼容性
一个典型的重构问题包括修改一个现有的例程,这样任何调用者都可以选择原始的行为或变体。
从一个非常简单的例子开始,假设您想要(可选地)记录每个数字的平方,并且您不想重复代码。所以,你可以修改经典函数模板 sq :
template <typename scalar_t>
inline scalar_t sq(const scalar_t& x)
{
return x*x;
}
template <typename scalar_t, typename logger_t>
inline scalar_t sq(const scalar_t& x, logger_t logger)
{
// we shall find an implementation for this...
}
struct log_to_cout
{
template <typename scalar_t>
void operator()(scalar_t x, scalar_t xsq) const
{
std::cout << "the square of " << x << " is " << xsq;
}
};
double x = sq(3.14); // not logged
double y = sq(6.28, log_to_cout()); // logged
用户将打开日志,向 sq 的两个参数版本传递一个定制的仿函数。但是在旧功能上实现新功能有不同的方法:
- 封装 :在 sq(scalar_t,logger_t)内部调用 sq(scalar_t)。此解决方案的实施风险极小。
template <typename scalar_t>
inline scalar_t sq(const scalar_t& x)
{
return x*x;
}
template <typename scalar_t, typename logger_t>
inline scalar_t sq(const scalar_t& x, logger_t logger)
{
const scalar_t result = sq(x);
logger(x, result);
return result;
}
- 接口适配 :转换 sq(scalar_t)以便用无操作记录器秘密调用 sq(scalar_t,logger_t)。这是最灵活的解决方案。 2
struct dont_log_at_all
{
template <typename scalar_t>
void operator()(scalar_t, scalar_t) const
{
}
}
template <typename scalar_t, typename logger_t>
inline scalar_t sq(const scalar_t& x, logger_t logger)
{
const scalar_t result = x*x; // the computation is performed here
logger(x, result);
return result;
}
template <typename scalar_t>
inline scalar_t sq(const scalar_t& x)
{
return sq(x, dont_log_at_all());
}
- 内核宏 :在算法核心极其简单,需要在静态和动态代码之间共享时工作。
#define MXT_M_SQ(x) ((x)*(x))
template <typename scalar_t>
inline scalar_t sq(const scalar_t& x)
{
return MXT_M_SQ(x);
}
template <typename int_t, int_t VALUE>
struct static_sq
{
static const int_t result = MXT_M_SQ(VALUE);
};
注意内核宏的使用将被 C++0x 关键字 constexpr 取代。
square/logging 的例子很简单,但是令人遗憾的是,代码重复很常见。在许多 STL 实现中,std::sort 被写了两次:
template <typename RandomAccessIter>
void sort(RandomAccessIter __first, RandomAccessIter __last);
template <class RandomAccessIter, typename Compare>
void sort(RandomAccessIter __first, RandomAccessIter __last, Compare less);
使用接口适配,第一个版本是第二个版本的特例:
struct weak_less_compare
{
template <typename T1, typename T2>
bool operator()(const T1& lhs, const T2& rhs) const
{
return lhs < rhs;
}
};
template <typename RandomAccessIter>
void sort(RandomAccessIter __first, RandomAccessIter __last)
{
return sort(__first, __last, weak_less_compare());
}
10.2.重构策略
本节考虑一个示例问题,并揭示一些不同的技术。
10.2.1.用接口重构
预先存在的 private_ptr 类在 void*中保存 malloc 的结果,并在析构函数中释放内存块:
class private_ptr
{
void* mem_;
public:
~private_ptr() { free(mem_); }
private_ptr() : mem_(0)
{ }
explicit private_ptr(size_t size) : mem_(malloc(size))
{ }
void* c_ptr() { return mem_; }
//...
};
现在,您需要扩展该类,以便它可以保存一个指针,指向一个 malloc 块或一个 t 类型的新对象。
由于 private_ptr 负责分配,您可以引入一个具有合适虚函数的私有接口,创建一个单独的派生(模板)类,并让 private_ptr 进行正确的调用:
class private_ptr_interface
{
public:
virtual void* c_ptr() = 0;
virtual ~private_ptr_interface() = 0;
};
template <typename T>
class private_ptr_object : public private_ptr_interface
{
T member_;
public:
private_ptr_object(const T& x)
: member_(x)
{
}
virtual void* c_ptr()
{
return &member_;
}
virtual ~private_ptr_object()
{
}
};
template < >
class private_ptr_object<void*> : public private_ptr_interface
{
void* member_;
public:
private_ptr_object(void* x)
: member_(x)
{
}
virtual void* c_ptr()
{
return member_;
}
virtual ~private_ptr_object()
{
free(member_);
}
};
class private_ptr
{
private_ptr_interface* mem_;
public:
~private_ptr()
{
delete mem_;
}
private_ptr()
: mem_(0)
{
}
explicit private_ptr(size_t size)
: mem_(new private_ptr_object<void*>(malloc(size)))
{
}
template <typename T>
explicit private_ptr(const T& x)
: mem_(new private_ptr_object<T>(x))
{
}
void* c_ptr()
{
return mem_->c_ptr();
}
//...
};
注意虚函数调用在 private_ptr 之外是不可见的。 3
10.2.2.用蹦床重构
前一种方法使用两个分配来存储 void*:一个用于内存块,一个用于辅助 private_ptr_object。蹦床可以做得更好:
template <typename T>
struct private_ptr_traits
{
static void del(void* ptr)
{
delete static_cast<T*>(ptr);
}
};
template <typename T>
struct private_ptr_traits<T []>
{
static void del(void* ptr)
{
delete [] static_cast<T*>(ptr);
}
};
template < >
struct private_ptr_traits<void*>
{
static void del(void* ptr)
{
free(ptr);
}
};
template < >
struct private_ptr_traits<void>
{
static void del(void*)
{
}
};
class private_ptr
{
typedef void (*delete_t)(void*);
delete_t del_;
void* mem_;
public:
~private_ptr()
{
del_(mem_);
}
private_ptr()
: mem_(0), del_(&private_ptr_traits<void>::del)
{
}
explicit private_ptr(size_t size)
{
mem_ = malloc(size);
del_ = &private_ptr_traits<void*>::del;
}
template <typename T>
explicit private_ptr(const T& x)
{
mem_ = new T(x);
del_ = &private_ptr_traits<T>::del;
}
template <typename T>
explicit private_ptr(const T* x, size_t n)
{
mem_ = x;
del_ = &private_ptr_traits<T []>::del;
}
void* c_ptr()
{
return mem_;
}
//...
};
10.2.3.用访问器重构
假设您有处理一系列简单对象的算法:
struct stock_price
{
double price;
time_t date;
};
template <typename iterator_t>
double computePriceIncrease(iterator_t begin, iterator_t end)
{
return ((end-1)->price - begin->price)
/ std::difftime(begin->date, (end-1)->date) * (24*60*60);
}
可能需要重构来处理来自两个独立容器的数据:
std::vector<double> prices;
std::vector<time_t> dates;
// problem: we cannot call computePriceIncrease
对于新的算法 I/O,您有几种选择:
- 假设迭代器指向 pair,其中第一个是价格,第二个是日期(换句话说,写 end->first - begin->first...).如前所述,这通常不是一个好的风格选择。
- 明确提及开始->价格和开始->日期(如前所示)。算法不依赖于迭代器,但是底层类型被约束在 stock_price 的接口上。
- 传递两个不相交的范围。该解决方案的复杂性可能会有所不同。
template <typename I1, typename I2>
double computePriceIncrease(I1 price_begin, I1 price_end, I2 date_begin, I2 date_end)
{
// the code must be robust and handle ranges of different length, etc.
}
- 传递一个范围和两个访问器。
template <typename I, typename price_t, typename date_t>
double computePriceIncrease(I begin, I end, price_t PRICE, date_t DATE)
{
double p = PRICE(*begin);
time_t t = DATE(*begin);
//...
}
struct price_accessor
{
double operator()(const stock_price& x) const
{
return x.price;
}
};
struct date_accessor
{
time_t operator()(const stock_price& x) const
{
return x.date;
}
};
computePriceIncrease(begin, end, price_accessor(), date_accessor());
请注意,您可以欺骗访问器查看其他地方,例如在成员变量中:
struct price_accessor_ex
{
const std::vector<double>& v_;
double operator()(const int x) const
{
return v_[x];
}
};
struct date_accessor_ex
{
const std::vector<time_t>& v_;
time_t operator()(const int x) const
{
return v_[x];
}
};
int main()
{
std::vector<double> prices;
std::vector<time_t> dates;
// ...
assert(prices.size() == dates.size());
std::vector<int> index(prices.size());
for (int i=0; i<prices.size(); ++i)
index[i] = i;
price_accessor_ex PRICE = { prices };
date_accessor_ex DATE = { dates };
computePriceIncrease(index.begin(), index.end(), PRICE, DATE);
}
访问器可能携带对外部容器的引用,所以它们选择从实际参数中推导出的元素。在某些特殊情况下,可以使用指针来避免创建索引容器。但是,使用这种方法时应该非常小心。
// warning: this code is fragile:
// changing a reference to a copy may introduce subtle bugs
struct price_accessor_ex
{
double operator()(const double& x) const
{
return x;
}
};
struct date_accessor_ex
{
const double* first_price_;
size_t length_;
const time_t* first_date_;
time_t operator()(const double& x) const
{
if ((&x >= first_price_) && (&x < first_price_+length_))
return first_date_[&x - first_price_];
else
throw std::runtime_error("invalid reference");
}
};
int main()
{
price_accessor_ex PRICE;
date_accessor_ex DATE = { &prices.front(), prices.size(), &dates.front() };
computePriceIncrease(prices.begin(), prices.end(), PRICE, DATE);
}
该算法引用一个价格,并相应地推导出相应的日期。
10.3.占位符
每个 C++ 对象都可以执行一些动作。空对象,比如 instance_of,可以执行元操作,比如声明它们的类型并将它们的类型“绑定”到模板参数或特定的函数重载。
有时,TMP 的工作是通过用一个相似的空对象替换一个对象,用一个相应的元动作替换一个动作,来阻止工作被完成。
如果 P < T >是一个类,它的公共接口满足与 T 相同的前置和后置条件,但运行时开销最小,则 P
10.3.1.关闭
关闭是一种算法重构技术,它允许你有选择地“关闭”一些特性,而不需要重写或复制函数。这个名字来源于这样一种典型情况:函数通过引用获取一个对象,在执行过程中被“触发”,最终返回一个独立的结果,这是执行的副产品。该对象可以是在执行期间接收信息的容器或同步对象。
void say_hello_world_in(std::ostream& out)
{
out << "hello world";
}
double read_from_database(mutex& s)
{
// acquire the mutex, return a value from the DB, and release the mutex
}
以最少的代码返工获得不同结果的一种快速而优雅的方法是提供一个具有简化接口的空心对象,特别是不需要任何动态存储。循序渐进:
- 重命名原始函数并将参数升级为模板类型:
template <typename T>
void basic_say_hello_world_in(T& o)
- 添加一个恢复原始行为的霸王:
inline void say_hello_world_in(std::stream& o)
{
return basic_say_hello_world_in(o);
}
- 最后,提供一个“中和”大部分努力的对象:
struct null_ostream
{
template <typename T>
null_ostream& operator<<(const T&)
{
return *this;
}
};
inline void say_hello_world_in()
{
null_stream ns;
basic_say_hello_world_in(ns);
}
关闭习惯用法需要准确了解主算法中使用的对象接口(的子集)。
当你设计一个定制容器时,偶尔添加一个额外的模板参数来启用一个空心模式可能是有用的。您获取原始类并将其提升为模板:
| 类自旋锁{S7-1200 可编程控制器:ptr_t vptr_t 可变类型定义:公共:spinlock(vptr_tconst):bool try _ acquire();bool acquire();/ ... →。}; | 模板类自旋锁;模板< >类旋转锁{S7-1200 可编程控制器:ptr_t vptr_t 可变类型定义:公共:spinlock(vptr_t const):bool try _ acquire();bool acquire();// ...};模板< >类旋转锁
如果该类是一个模板,您将需要再添加一个布尔参数。
当然,接口复制的关键点是空心类的一组谨慎但有意义的默认答案,前提是这种复制是可能的(见下面的反例)。这也允许您识别被认为“有效”的对象的最小接口。对象的接口是由它的用途定义的。
最后,您可以将程序限制为自旋锁(可能是“开”或“关”):
template <typename ..., bool IS_LOCKING_REQURED>
void run_simulation(..., spinlock<IS_LOCKING_REQURED>& spin)
{
if (spin.acquire())
{
//...
}
}
或未指定类型的对象,其接口被隐式假定为与自旋锁兼容:
template <typename ..., typename lock_t>
void run_simulation(..., lock_t& lock)
{
if (lock.acquire())
{
//...
}
}
两种选择都是有效的,但在某些情况下,更倾向于选择其中一种(详见第 5.2 节)。
另一个应用是双联减速。有些算法一次处理一个或两个项目,同时对两个项目执行相同的操作。为了避免重复,您需要一个接受一个或两个参数的算法实现。
原型例子是排序两个“同步”数组和矩阵行缩减。由于高斯的原因,该算法对矩阵 M 执行一系列初等运算,并将其转变为对角(或三角形)形式。如果在单位矩阵上并行应用相同的运算,它也获得 M. 4 的逆
因此,您可以编写一个通用函数,使总是接受两个不同静态类型的矩阵,并将它们视为相同:
template <typename matrix1_t, typename matrix2_t>
void row_reduction(matrix1_t& matr, matrix2_t& twin)
{
// ...
for (size_t k=i+1; k<ncols && pivot!=0; ++k)
{
matr(j, k) -= pivot*matr(i, k);
twin(j, k) -= pivot*twin(i, k);
}
// ...
}
假设你已经有一个矩阵类: 5
template <typename scalar_t>
class matrix
{
public:
typedef scalar_t value_type;
size_t rows() const;
size_t cols() const;
void swap_rows(const size_t i, const size_t j);
value_type& operator()(size_t i, size_t j);
value_type operator()(size_t i, size_t j) const;
};
按照中空模式的习惯用法来扩展它是不可能的,因为对于返回引用的函数没有满意的默认答案: 6
template <typename scalar_t, bool NO_STORAGE = false>
class matrix;
template <typename scalar_t>
class matrix<scalar_t, false>
{
/* put the usual implementation here */
};
template <typename scalar_t>
class matrix<scalar_t, true>
{
public:
value_type& operator()(size_t i, size_t j)
{
return /* what? */
}
//...
};
因此,您完全删除引用,并向下移动一级。你中和了容器和被包含的物体。孪生矩阵是在幻影标量上定义的容器;操作符什么也不做的类:
template <typename T>
struct ghost
{
// all operators return *this
ghost& operator-=(ghost)
{
return *this;
}
//...
};
template <typename T>
inline ghost operator*(T, ghost g) { return g; }
template <typename T>
inline ghost operator*(ghost g, T) { return g; }
template <typename scalar_t>
class matrix<scalar_t, true>
{
size_t r_;
size_t c_;
public:
typedef ghost<scalar_t> value_type;
size_t rows() const { return r_; }
size_t cols() const { return c_; }
void swap_rows(const size_t, const size_t) {}
value_type operator()(size_t i, size_t j)
{
return value_type();
}
const value_type operator()(size_t i, size_t j) const
{
return value_type();
}
};
ghost
在这一点上需要更多的细节。
10.3.2.鬼魂
没有真正令人满意的方法来编写虚标量。大多数实现都是半正确的,但是它们可能会有令人讨厌的副作用:
- 如果不对名称空间进行适当的约束,幽灵很可能会在名称空间中出没。因为它们的接口应该支持几乎所有的 C++ 操作符,所以您可能需要编写一些全局操作符,并且您希望确保这些操作符仅在必要时出现。
- 鬼的主要目的是阻止工作被做。如果 G 是鬼,那么 G*3+7 应该编译,什么都不做。很容易获得编译的实现,但是错误地做了一些工作——比如说,因为 G 被转换为整数 0。
ghost 应该是一个模仿其模板参数 T 的类模板,它驻留在一个不同的名称空间中。为了简单起见,可以假设 T 是一个内置的数值类型,这样就可以实现所有可能的操作符。
template <typename T>
struct ghost
{
ghost(T) {}
ghost() {}
//...
};
对于 coherence,比较运算符返回的结果与 ghost 是单态(所有 ghost 都是等效的)这一事实相符,因此运算符
通常,大多数算术运算符都可以用合适的宏来定义: 7
#define mxt_GHOST_ASSIGNMENT(OP) \
ghost& operator OP##= (const ghost) { return *this; }
#define mxt_GHOST_UNARY(OP) \
ghost operator OP() const { return *this; }
#define mxt_GHOST_INCREMENT(OP) \
ghost& operator OP () { return *this; } \
const ghost operator OP (int) { return *this; }
template <typename T>
struct ghost
{
ghost(const T&){}
ghost() {}
mxt_GHOST_INCREMENT(++); // defines pre- and post-increment
mxt_GHOST_INCREMENT(--);
mxt_GHOST_ASSIGNMENT(+); // defines operator+=
mxt_GHOST_ASSIGNMENT(-);
// ...
mxt_GHOST_UNARY(+);
mxt_GHOST_UNARY(-);
//...
};
对于算术/比较运算符,您需要研究这些可能性:
- 带参数 ghost 的成员运算符
。 - 带参数 t 的成员运算符。
- 带有参数 const X&的模板成员运算符,其中 X 是一个独立的模板参数。
- 非成员运算符,如
template <typename T>
ghost<T> operator+(ghost<T>, ghost<T>) // variant #1
template <typename T>
ghost<T> operator+(T, ghost<T>) // variant #2
template <typename T1, typename T2>
<???> operator+(ghost<T1>, ghost<T2>) // variant #3
template <typename T1, typename T2>
<???> operator+(T1, ghost<T2>) // variant #4
每个选择都有一些问题。
- 成员运算符将在右侧执行参数提升,但模板全局运算符要求参数推导完全匹配。 8 使用成员操作符 ghost
:·operator+(ghost )const,ghost < T > + X 形式的任何和都将成功,只要有可能从 X 构建临时 ghost < T >(因为 ghost 构造函数不是显式的)。但是 X + ghost < T >不会编译。 - 当 T 是一个数字类型(比如 double)并且 X 是一个文字零时,这个问题最明显。成员运算符+将处理 ghost
+ 0,因为 0 (int) → 0.0 (double) → ghost ,但是 0 + ghost 必须由一个签名不能太严格的全局运算符处理,因为 0 不是 double。 - 这意味着在这种情况下,只有变量#4 是可行的,因为没有其他操作符会完全匹配(int,ghost
)。 - 但是,您希望运算符匹配尽可能多的类型,而不是更多。虽然您应该能够编写 int + ghost
,但是您不想接受任何东西。
ghost<double> g;
g + 0; // should work
0 + g; // should work
std::cout + g; // should not work!
g + std::cout; // should not work!
通常,全局操作符应该将执行委托给成员函数:
template <typename T1, typename T2>
inline ghost<T2> operator+ (T1 x, const ghost<T2> y)
{
return y + x;
}
y + x 确实是对任何成员 operator+的调用,所以你可以把接受 T1 作为参数的责任传递给 ghost 自己的接口(编译器会尝试任何重载的 operator+)。
要使赋值合法,转换运算符是必需的:
operator T() const
{
return T();
}
ghost<double> g = 3.14;
double x = g; // error: cannot convert from ghost to double
相反,使用转换运算符和糟糕的运算符实现,无害的代码会突然变得模糊不清:
ghost<double> g;
g + 3.14;
例如,以下各项之间可能存在歧义:
- 3.14 晋升为幽灵
,之后是幽灵 :::操作员+(幽灵 )。 - 将 g 转换为 double,然后进行普通求和。
由于两条路径的等级相等,编译器将会放弃。
在不同的情况下,转换会被意外地调用:
ghost<double> g = 3.14;
double x = 3*g + 7;
编译器应将该代码翻译成以下序列:
double x = (double)(operator*(3, g).operator+(ghost<double>(7)));
如果全局操作符*由于某种原因不能被调用(比方说,它期望 double,ghost
double x = 3*(double)(g) + 7;
这需要在运行时进行两次浮点运算,因此它违背了 ghost 的目的。 9
总而言之,在最佳实现中:
- ghost 构造函数是强类型的,所以它需要一个可转换为 t 的参数。
- 您需要成员和非成员操作员:
- 成员操作符将接受任何参数(任何类型 X)并用静态断言(使用构造函数本身)检查 X。
- 非成员操作符会盲目地将任何事情委托给成员函数。
这里描述的是一个不使用宏的实现。无论如何,由同一个预处理器指令生成的函数已经被分组:
#define mxt_GHOST_GUARD(x) sizeof(ghost<T>(x))
template <typename T>
struct ghost
{
ghost(const T&) {}
ghost() {}
operator T() const
{
return T();
}
ghost& operator++ () { return *this; }
const ghost operator++ (int) { return *this; }
ghost& operator-- () { return *this; }
const ghost operator-- (int) { return *this; }
template <typename X> ghost& operator+= (const X& x)
{ mxt_GHOST_GUARD(x); return *this; }
template <typename X> ghost& operator-= (const X& x)
{ mxt_GHOST_GUARD(x); return *this; }
template <typename X> ghost operator+ (const X& x) const
{ mxt_GHOST_GUARD(x); return *this; }
template <typename X> ghost operator- (const X& x) const
{ mxt_GHOST_GUARD(x); return *this; }
template <typename X> bool operator== (const X& x) const
{ mxt_GHOST_GUARD(x); return true; }
template <typename X> bool operator!= (const X& x) const
{ mxt_GHOST_GUARD(x); return false; }
ghost operator+() const { return *this; }
ghost operator-() const { return *this; }
};
template <typename X, typename Y>
ghost<Y> operator+ (const X& x, const ghost<Y> y) { return y + x; }
template <typename X, typename Y>
ghost<Y> operator- (const X& x, const ghost<Y> y) { return -(y - x); }
template <typename X, typename Y>
bool operator== (const X& x, const ghost<Y> y) { return y == x; }
template <typename X, typename Y>
bool operator!= (const X& x, const ghost<Y> y) { return y != x; }
关于 STL 分配器,也有类似的争论。“两个同类容器相等”的概念显然要求元素序列相等,但不清楚这是否也足够。
2 虽然封装向用户传达了一种“开销感”,但接口适配表明新的 sq 要好得多,可以自由使用。
3 换句话说,代码的调用方不用担心继承问题。它们可以通过任何 T,类会自动地自动包装它。这个想法在肖恩·帕伦特的一次演讲中得到了进一步的发展,可以从这个链接免费下载:channel 9 . msdn . com/Events/going native/2013/Inheritance-Is-The-Base-Class-of-Evil
。
一个对数学不感兴趣的读者可能想考虑一个类似的情况:软件执行一系列动作,同时记录一系列“撤销”步骤。
5 为了便于算法,数据结构的接口经常被改造。这一课是 STL 设计的里程碑之一。
6 一般来说,空心容器本身没有记忆。您可能会反对,在这里您可以使用单个 scalar_t 数据成员,并为任何一对索引返回对同一对象的引用,但是这种策略会消耗大量 CPU 运行时间,毫无意义地覆盖相同的内存位置。
7 介意使用令牌串联#。您可能想编写 operator ## OP 来连接 operator and +,但这是非法的,因为在 C++ 中,operator 和+是两个不同的标记。另一方面,在+和=之间需要# 来生成运算符+=,所以需要写运算符 OP ## =。
8 将 T 转换为 ghost < T >的自定义构造函数只有在模板实参推演后才被考虑。注意,这里的构造函数甚至不是显式的。参见[2]第 B.2 节。
9 提示:一定要在转换运算符中留一个断点。
十一、调试模板
由于 TMP 代码诱导编译器执行计算,所以实际上不可能一步一步地遵循它。然而,有一些技巧可以帮助你。这一章实际上包含了一些建议和调试策略。
11.1.识别类型
现代的调试器总是会在程序停止时显示变量的确切类型。此外,在调用堆栈中可以看到许多关于类型的信息,其中(成员)函数通常显示有它们的模板参数的完整列表。但是,您经常需要检查中间结果和返回类型。
以下函数有助于:
template <typename T>
void identify(T, const char* msg = 0)
{
std::cout << (msg ? msg : "") << typeid(T).name() << std::endl;
}
记住 type_info:: name 对返回字符串的可读性没有任何保证。 1 使用自由函数返回 void 可以很容易地在调试和优化版本之间切换,因为代码可以简单地使用预处理器指令来替换函数,比如用一个空宏。然而,当你需要识别一个类成员时,这种方法不起作用,比如当你调试 lambda 表达式时。(参见第 9.2 节)。您可能希望检查返回类型是否被正确地推导出来;最佳解决方案是添加一个小型公共数据成员:
template <typename X1, typename F, typename X2>
class lambda_binary : public lambda< lambda_binary<X1,F,X2> >
{
// ...
typedef typename
deduce_argument
<
typename X1::argument_type,
typename X2::argument_type
>::type
argument_type;
#ifdef MXT_DEBUG
instance_of<result_type> RESULT_;
#endif
result_type operator()(argument_type x1, argument_type x2) const
{
identify(RESULT_);
return f_(x1_(x1, x2), x2_(x1, x2));
}
};
添加数据成员特别有用,因为交互式调试器允许您检查内存中的对象并显示它们的确切类型。
一般来说,每当元函数编译但给出错误结果时,添加 instance_of 和 static_value 类型的成员来检查计算的中间步骤,然后在堆栈上创建元函数的本地实例。
template <size_t N>
struct fibonacci
{
static const size_t value = fibonacci<N-1>::value + fibonacci<N-2>::value;
static_value<size_t, value> value_;
fibonacci<N-1> prev1_;
fibonacci<N-2> prev2_;
};
int main()
{
fibonacci<12> F;
}
然后在调试器里看 F。您可以从常量的类型来检查它们。 2
11.1.1.陷印类型
有时在大型项目中,会检测到错误的模式。当这种情况发生时,您需要列出所有使用坏模式的代码行。您可以使用模板创建不编译的函数陷阱,并将它们注入到错误模式中,这样编译器日志将指向您正在寻找的所有行。
假设您发现一个 std::string 被传递给了 printf,并且您怀疑这在项目中发生了几次。
std::string name = "John Wayne";
printf("Hello %s", name); // should be: name.c_str()
class Foo{};
printf("I am %s", Foo());
遍历 printf 的所有实例会花费太多时间,所以您可以在一个公共的包含文件中添加一些陷阱代码。请注意,您必须编写一个始终为假的静态断言,但它依赖于一个未指定的参数 t,在下面的代码中,MXT_ASSERT 是一个静态断言:
template <typename T>
void validate(T, void*)
{
}
template <typename T>
void validate(T, std::string*)
{
MXT_ASSERT(sizeof(T)==0); // if this triggers, someone is passing
// std::string to printf!
}
template <typename T>
void validate(T x)
{
validate(x, &x);
}
template <typename T1>
void printf_trap(const char* s, T1 a)
{
validate(a);
}
template <typename T1, typename T2>
void printf_trap(const char* s, T1 a, T2 b)
{
validate(b);
printf_trap(s, a);
}
template <typename T1, typename T2, typename T3>
void printf_trap(const char* s, T1 a, T2 b, T3 c)
{
validate(c);
printf_trap(s, a, b);
}
// ...
#define printf printf_trap
每次将字符串传递给 printf 时,这个陷阱代码都会导致编译器错误。
能够提到 std::string(在 validate 中)很重要,所以前面的文件必须包含
template <typename T>
void validate(T, void*)
{
MXT_ASSERT(!is_class<T>::value); // don't pass classes to printf;
}
11.1.2.不完整的类型
类模板可能不要求 T 是完整的类型。这个需求通常是不明确的,它依赖于内部模板实现的细节。
STL 容器,比如 vector、list 和 set,可以被实现为接受不完整的类型,因为它们动态地分配存储。判定 T 是否不完全的一个充要条件是把它自身的一个容器放入一个类中。
struct S1
{
double x;
std::vector<S1> v;
};
struct S2
{
double x;
std::list<S2> l;
};
特别是,分配器不应该假设 T 已经完成;否则,它可能与标准容器不兼容。
只需向编译器询问类型的大小,就可以轻松获得静态断言:
template <typename T>
struct must_be_complete
{
static const size_t value = sizeof(T);
};
struct S3
{
double x;
must_be_complete<S3> m;
};
test.cpp: error C2027: use of undefined type 'S3'
该技术用于实现安全删除。指向不完整类型的指针可能会被删除,但这会导致未定义的行为(在最好的情况下,T 的析构函数不会被执行)。
template <typename T>
void safe_delete(T* p)
{
typedef T must_be_complete;
sizeof(must_be_complete);
delete x;
}
确定一个模板是否会得到一个完整的类型作为参数可能并不容易。
标准分配器有一个 rebind 成员,允许任何分配器
template <typename T>
class allocator
{
typedef T* pointer;
template <typename other_t>
struct rebind
{
typedef allocator<other_t> other;
};
// ...
};
template <typename T, typename allocator_t>
struct list
{
struct node;
friend struct node;
typedef typename allocator_t::template rebind<node>::other::pointer node_pointer;
// the line above uses allocator<node> when node is still incomplete
struct node
{
node(node_pointer ptr)
{
}
};
// ...
};
要编译节点构造函数,需要 node_pointer。所以编译器查看 allocator ::rebind
假设您现在有一个高效的类来管理固定长度 N 的内存块:
template <size_t N>
class pool;
为了在一般的无状态分配器中正确地包装它,您可能想写:
template <typename T>
class pool_allocator
{
static pool<sizeof(T)>& get_storage();
// ...
};
但是在这种情况下,类级别的 sizeof(T)的存在要求 T 是完整的。相反,您可以切换到带有模板成员函数的惰性实例化方案:
template <typename T>
class pool_allocator
{
template <typename X>
static pool<sizeof(X)>& get_storage()
{
static pool<sizeof(X)>* p = new pool<sizeof(X)>;
return *p;
}
// ...
void deallocate(pointer ptr, size_type)
{
get_storage<T>().release(ptr);
}
};
现在在类的层面上,sizeof(T)是从来不提的。
注如[7]第 10.14 节所述,堆栈和堆分配是有区别的:
static T& get1()
{
static T x;
return x;
}
static T& get2()
{
static T& x = *new T;
return x;
}
前者会在程序结束的某个未指明的时刻销毁 x,而后者永远不会销毁 x。
所以,如果 T::~T()释放了一个资源,比如说一个互斥体,那么第一个版本就是正确的。但是,如果另一个全局对象的析构函数调用 get1(),可能是 x 已经被销毁了(这个问题被称为“静态初始化顺序惨败”)。
11.1.3.标记全局变量
非类型模板参数可以是指向具有外部链接的对象的任意指针。限制是这个指针不能在编译时被解引用:
template <int* P>
struct arg
{
arg()
{
myMember = *P; // dereference at runtime
}
int myMember;
};
extern int I;
int I = 9;
arg<&I> A;
相反,写下以下内容是非法的:
template <int* P>
struct arg : static_value<int, *P> // dereference at compile time
您可以使用指针将一些元数据与全局常量相关联:
// metadata.hpp
template <typename T, T* global>
struct metadata
{
static const char* name;
};
#define DECLARE_CPP_GLOBAL(TYPE, NAME) \
TYPE NAME; \
template <> const char* metadata<TYPE, &NAME>::name = #NAME
// main.cpp
#include "metadata.hpp"
DECLARE_CPP_GLOBAL(double, xyz);
int main()
{
printf(metadata<double, &xyz>::name); // prints "xyz"
}
11.2.整数计算
本节快速回顾静态整数计算可能导致的一些问题。
11.2.1.有符号和无符号类型
当 T 是整数类型时,T(-1)、-T(1)、T()-1 和~T()之间的差异可能会导致常见问题。
- 如果 T 是无符号的和大的,它们都是相同的。
- 如果 T 是有符号的,前三个是相同的。
- 如果 T 是无符号的并且很小,第二个和第三个表达式可能会给出意外的结果。
我们借用 is_signed_integer 的实现中的一个函数(见 4.3.2 节)。
template <typename T>
static selector<(T(0) > T(-1))> decide_signed(static_value<T, 0>*);
用-T(1)替换 T(-1),突然两个回归测试失败。(但是哪些呢?)
bool t01 = (!is_signed_integer<unsigned char>::value);
bool t02 = (!is_signed_integer<unsigned int>::value);
bool t03 = (!is_signed_integer<unsigned long long>::value);
bool t04 = (!is_signed_integer<unsigned long>::value);
bool t05 = (!is_signed_integer<unsigned short>::value);
bool t11 = (is_signed_integer<char>::value);
bool t12 = (is_signed_integer<int>::value);
bool t13 = (is_signed_integer<long long>::value);
bool t14 = (is_signed_integer<long>::value);
bool t15 = (is_signed_integer<short>::value);
失败的原因是“一元减”运算符将小的无符号整数提升为 int,所以-T(1)是 int,整个比较转移到 int 域,其中 0 > -1 为真。要查看这一点,请执行以下命令:
unsigned short u = 1;
identify(-u);
11.2.2.对数字常数的引用
通常,不要将静态常量直接传递给函数:
struct MyStruct
{
static const int value = 314;
}
int main()
{
double myarray[MyStruct::value];
std::fill_n(myarray, MyStruct::value, 3.14); // not recommended
}
如果 fill_n 通过 const 引用接受第二个参数,那么这段代码可能会导致链接失败。获取常数的地址需要在。cpp 文件(和其他静态成员一样)。在 TMP 中,很少出现这种情况。
作为一种廉价的解决方法,您可以构建一个临时整数并用常量初始化它:
// not guaranteed by the standard, but usually ok
std::fill_n(myarray, int(MyStruct::value), 3.14);
对于极端的可移植性,特别是对于枚举和 bool,您可以动态构建一个函数:
template <bool B> struct converter;
template <> struct converter<true>
{ static bool get() { return true; } };
template <> struct converter<false>
{ static bool get() { return false; } };
// instead of: DoSomethingIf(MyStruct::value);
DoSomethingIf(converter<MyStruct::value>::get());
11.3.常见解决方法
11.3.1 .调试 SFINAE
一个常见的“剪切和粘贴”错误是在函数中添加了一个无用的不可推导的模板参数。有时候,编译器会抱怨,但如果函数重载,SFINAE 原理 会默默将其排除在重载解析之外,一般会导致细微的错误:
template <typename X, size_t N>
static YES<[condition on X]> test(X*);
static NO test(...);
在这个片段中,不能推导出 N,因此总是选择第二个测试函数。
11.3.2.蹦床
编译器限制可能会影响蹦床。在经典 C++ 中,局部类有一些限制(它们不能绑定到模板参数)。它们可能会导致虚假的编译器和链接器错误:
template <typename T>
struct MyStruct
{
template <typename X>
void doSomething(const X& m)
{
struct local
{
static T* myFunc(const void* p)
{
// compilers may have problems here using template parameter X
}
};
// call local::myFunc(&m);
}
};
解决方法是将大部分模板代码移到本地类之外:
template <typename T>
struct MyStruct
{
template <typename X>
static T* MyFunc(const X& m)
{
// do the work here
}
template <typename X>
void DoSomething(const X& m)
{
struct local
{
static T* MyFunc(const void* p)
{
// put nothing here, just a cast
return MyStruct<T>::MyFunc(*static_cast<const X*>(p));
}
};
// ...
}
};
11.3.3.编译器错误
编译器错误很少见,但确实会发生,尤其是在模板元编程中。它们通常产生模糊的诊断结果。3
error C2365: 'function-parameter' : redefinition; previous definition was a 'template parameter'. see declaration of 'function-parameter'
在以下情况下,编译器会对模板感到困惑:
- 他们不能推断出表达式是一种类型。
- 它们没有正确地或以正确的顺序执行自动转换,因此会发出不正确的诊断。
- 一些语言关键字在静态上下文中可能无法正常工作。
这是最后一种说法的一个例子。如果表达式无效,sizeof 通常会抱怨。当您尝试取消引用 double 时,会发生以下情况:
int main()
{
sizeof(**static_cast<double*>(0));
}
error: illegal indirection
相同的测试可能无法正确触发 SFINAE。下面的代码用来用一个老版本的流行编译器打印“Hello”:4
template <size_t N>
struct dummy
{
};
template <typename X>
dummy<sizeof(**static_cast<X*>(0))>* test(X*)
{
printf("Hello");
return 0;
}
char test(...)
{
return 0;
}
int main()
{
double x;
test(&x);
}
下一个例子是由于隐式转换:
double a[1];
double b[1];
double (&c)[1] = true ? a : b;
error: 'initializing' : cannot convert from 'double *' to 'double (&)[1]'
A reference that is not to 'const' cannot be bound to a non-lvalue
因此,您可以看到编译器在三元运算符中将数组错误地转换为指针。然而,错误可能不会在模板函数中触发:
template <typename T>
void f()
{
T a;
T b;
T& c = true ? a : b;
}
f<double [1]>();
确保可移植性是一项重要的开发工作。可移植性的一个非正式定义是,“在多个平台上工作的代码,潜在地适应平台本身(带有预处理器指令,等等)”。符合标准的代码可以在任何地方工作,无需修改(假设编译器没有错误)。实际上,可移植性是符合标准的代码和解决特定编译器限制/错误的代码的结合。有些编译器有微妙的非标准行为;它们可能有扩展(例如,它们可能允许在堆栈上创建可变长度的数组),它们可能容忍较小的语法错误(例如 this- >或::template 的使用),甚至一些模糊性(例如,具有多个基的对象的静态转换)。然而,以标准一致性为目标是非常重要的,因为它保证了如果一段(元编程)代码工作了,它将继续工作,即使是在同一编译器的未来版本中。
如果看起来正确的代码无法编译,这可能有助于:
- 简化引入额外类型定义的复杂类型,反之亦然。
- 将功能提升为模板,反之亦然。
- 如果代码无法进一步更改,请测试不同的编译器。
见【http://en . cppreference . com/w/CPP/types/type _ info/name】。
2 另外,还存在交互式元调试器。元调试器在幕后使用他们自己的编译器,所以他们的输出可能与实际二进制文件中观察到的不同,但是当研究一个不编译的元函数时,他们是非常有价值的。这里可以找到一个:metashell.readthedocs.org/en/latest/
3 然而,它们是可能出错的好例子。
4 decltype 可能会遇到类似的问题。
十二、C++0x
“我注意到 C++0x 的每个特性都是由某个人在某个地方实现的。”
bjarne stroustup
在 2003 年的最终修订版中,我们习惯上称之为“经典 C++”,而不是 2011 年推出、随后在 2014 年完善的“现代 C++”(也非正式地称为 C++0x)。这一系列的改变是巨大的,但是新的规则总的来说是为了减轻 TMP 并使代码不那么冗长。此外,编译器提供了一系列新的标准类、容器、语言工具(如 std::bind)和特征,这些特征揭示了以前只有编译器知道的元信息。 1
最简单的例子就是元函数 std::has_trivial_destructor
仅仅通过语言来检测一个类型是否有一个简单的析构函数是不可能的。经典 C++ 中的最佳默认实现是“除非 T 是本机类型,否则返回 false”。 2
本章简要地触及了一个巨大主题的表面,所以不要认为这是一个完整的参考。为了更加清晰,一些描述被稍微简化了。
12.1.类型特征
编译器已经提供了一套完整的元函数:
#include <type_traits>
这将在名称空间 std 或 std::tr1 中带来一些元函数(取决于编译器和标准库)。 3
特别是,本书中描述的一些元函数出现在 C++0x 中,只是名称不同。下表列出了一些示例。4
|
本书
|
相当于 C++0x
|
| --- | --- |
| 静态值 | std::积分常数 |
| 仅 _ 如果 | 标准::使能 _if |
| 键入 if | 标准::有条件 |
| 散列 _ 转换 | STD::is _ 可兑换 |
12.2. Decltype
与 sizeof 类似,decltype 解析为括号中给定的 C++ 表达式的类型(在运行时不计算它),您可以将它放在任何需要类型的地方:
int a;
double b;
decltype(a+b) x = 0; // x is double
decltype 可以对 SFINAE 产生积极的影响。以下元函数正确检测交换成员函数,测试表达式 x.swap(x ),其中 x 是对 x 的非常数引用。
由于 swap 通常返回 void,所以对通过测试的类型使用指向 decltype 的指针,对其余类型使用非指针类。然后,您像往常一样将它转换为是/否:
#define REF_TO_X (*static_cast<X*>(0))
struct dummy {};
template <typename T>
struct has_swap
{
template <typename X>
static decltype(REF_TO_X.swap(REF_TO_X))* test(X*);
static dummy test(...);
template <typename X>
static yes_type cast(X*);
static no_type cast(dummy);
static const bool value = sizeof( cast(test((T*)0)) )==sizeof(yes_type);
};
另外,C++11 头文件
在 SFINAE-expression 中,您可能会提到成员函数调用(前面的示例是“REF_TO_X.swap(REF_TO_X)的结果”),因此您需要 T 的一个实例。但是,您不能简单地调用构造函数,比如 T(),因为 T 可能没有公共的默认构造函数。一种解决方法是产生一个假引用,比如 REF_TO_X,因为表达式无论如何都不会被求值。但是在 C++11 中你可以只使用表达式 std::declval
12.3.汽车
从 C++11 开始,关键字 auto 有了新的含义。它用于声明一个需要立即初始化的局部变量。初始化对象用于推断变量的实际类型,就像模板参数一样:
auto i = 0;
I 的实际类型与从调用 f(0)推导出的模板相同,其中 f 将是(伪代码):
template <typename auto>
void f(auto i);
auto 将总是解析为值类型。事实上,它的预期用途是存储来自一个函数的结果,而不明确提及它们的类型(想想 auto i = myMap.begin())。如果用户确实需要引用,可以显式限定 auto(作为任何模板参数):
const auto& i = cos(0.0);
auto 将解析为 double,因为调用 g(cos(0.0))时会发生这种情况,用
template <typename auto>
void g(const auto& i);
请记住,泛型模板参数不会与引用匹配:
int& get_ref();
template <typename T>
void f(T x);
f(get_ref()); // T = int, not reference-to-int
另一方面,decltype 返回表达式的精确静态类型,定义如下: 5
int i = 0;
decltype(get_ref()) j = i; // j is reference-to-int
decltype 有一些处理引用的规则:
- decltype(变量)或 decltype(类成员)导致与操作数相同的声明类型;如果 x 在当前范围内是 double,则 decltype(x)被推导为 double,而不是 double&。
- decltype(函数调用)是函数返回的结果类型。 6
- 如果前面的规则都不为真,并且表达式是 T 类型的左值,则结果为 T &;否则就是 t。
特别是一些“看起来很奇怪”的表达式,如 decltype(*&x)、decltype((x))或 decltype(true?x : x)将得到 double&因为没有一个操作数是普通变量,所以遵循第三条规则。
12.4.匿名函数
Lambda 表达式 (简称“lambdas”)提供了一种快速创建函数对象的简洁方法。它们不是一种新的语言特性,而是一种新的语法:
[](int i) { return i<7; }
[](double x, double y) { return x>y; }
每一行代表一个“仿函数”类型对象的实例(称为闭包),接受一个或多个参数并返回 decltype(返回语句)。所以你可以把这个对象传递给一个算法:
std::partition(begin, end, [](int i) { return i<7; });
std::sort(begin, end, [](double x, double y) { return x>y; });
这相当于更详细的:
struct LessThan7
{
bool operator()(int i) const
{
return i<7;
}
};
int main()
{
std::vector<int> v;
std::partition(v.begin(), v.end(), LessThan7());
}
明显的优点是更加清晰(执行分区的代码行变得独立)并且省略了不相关的信息(因为您不需要为函子及其参数找到一个有意义的名称)。
方括号[]被称为λ引入器,它们可以用来列出您想要“捕获”的局部变量,这意味着作为成员添加到函子中。在下面的例子中,闭包获得了 N 的副本(介绍者[ & N]将传递一个引用)。
int N = 7;
std::partition(v.begin(), v.end(), N { return i<N; });
同样,这个 lambda 相当于更详细的:
class LessThanN
{
private:
int N_;
public:
LessThanN(int N)
: N_(N)
{}
bool operator()(int i) const
{
return i<N;
}
};
还有一些语法细节。您可以在参数列表后显式指定返回类型。当您想要返回一个引用时,这确实很有用(默认情况下,返回类型是一个右值)。
[](int i) -> bool { ... }
可以使用 auto:
auto F = [](double x, double y) { return cos(x*y); }
最后,允许在成员函数中创建的 lambda 来捕捉这一点;lambda 函数调用操作符将能够访问原始上下文中可用的任何内容。实际上,lambda 主体的代码就像是直接写在声明它的地方一样。
class MyClass
{
private:
int myMember_;
void doIt() const { ... }
void doMore() { ... }
public:
int lambdizeMyself() const
{
auto L = [this]()
{
doIt(); // ok: doIt is in scope
doMore(); // error: doMore is non-const
return myMember_; // ok, private members can be read
};
return L();
}
};
下面的例子(来自 Stephan T. Lavavej)显示了 lambdas 可以与模板参数交互。这里λ用于执行未指定的一元谓词的逻辑否定。
template <typename T, typename Predicate>
void keep_if(std::vector<T>& v, Predicate pred)
{
auto notpred = &pred { return !pred(t); };
v.erase(remove_if(v.begin(), v.end(), notpred), v.end());
}
12.5.初始值设定项
如果一个函数有一个长返回类型,你可能不得不写两次——在函数签名中和在构建结果时。这种冗余很可能导致维护和重构问题。考虑 9.4.2 中的以下示例:
template <typename X>
console_assert<X, console_assert<T1, T2> > operator()(const X& x) const
{
return console_assert<X, console_assert<T1, T2> >(x, *this);
}
在经典的 TMP 中,这可以通过非显式的单参数构造函数来避免(可行时):
template <typename T1, typename T2>
class console_assert
{
public:
console_assert(int = 0) {}
};
template <typename X>
console_assert<X, console_assert<T1, T2> > operator()(const X& x) const
{
return 0; // much simpler, but we cannot pass parameters...
}
在 C++0x 中,一个新的语言特性叫做括号初始化列表允许你使用花括号构建一个对象,并且(在某些情况下)省略类型名:
std::pair<const char*, double> f()
{
return { "hello", 3.14 };
}
template <typename X>
console_assert<X, console_assert<T1, T2> > operator()(const X& x) const
{
return { x, *this };
}
编译器将根据重载决策规则,将初始值设定项列表中的项与所有构造函数的参数进行匹配,并选择最佳项。
12.6.模板类型定义
C++0x 用一个新的 using 语句扩展了传统的 typedef 语法:
typedef T MyType; // old syntax
using MyType = T; // new syntax
但是,新语法对模板也有效:
template <typename T>
using MyType = std::map<T, double>; // declares MyType<T>
MyType<string> m; // std::map<string, double>
12.7. 外部模板
12.7.1.链接模板
在经典 C++ 中,编译器需要看到函数/类模板的整个主体,才能生成模板实例。默认行为是只生成在翻译单元中实际使用的成员函数,所以粗略地说,每个。使用模板类的 cpp 文件将在相应的二进制对象中产生代码的副本。最后,链接器将收集所有的二进制对象并生成一个可执行文件,通常可以正确地识别和删除重复的对象。
在普通代码中,符号不能定义两次,但模板生成的代码被标记为“可重复”,最后一步中的链接器将删除 C++ 副本(如 vector
然而,这是因为 vector 头包含了所有相关的代码。让我们编写一个模板类,就像它是一个普通的类一样(记住,作为一个规则,这是不正确的)。
// xyz.h
template <typename T>
class XYZ
{
public:
int size() const;
};
// xyz.cpp
template <typename T>
int XYZ<T>::size() const
{
return 7;
};
现在,任何包含 xyz.h(以及针对 xyz.cpp 的链接)的翻译单元都能够正确编译任何代码,包括:
// main.cpp
#include <xyz.h>
int main()
{
XYZ<int> x;
return x.size();
}
但是,程序不会链接,因为在翻译单元 main.cpp 中,编译器看不到相关的模板体。另一方面,XYZ 可以在 xyz.cpp 内部充分利用:
// xyz.cpp
template <typename T>
int XYZ<T>::size() const
{
return 7;
};
int f()
{
XYZ<int> x; // Ok.
return x.size(); // Ok.
}
现在,作为副作用,二进制对象 xyz.obj 将包含所使用的相关成员函数的二进制代码(即构造函数 XYZ::XYZ()和 XYZ::size)。这意味着 main.cpp 现在可以正确链接了!
编译器将验证 main.cpp 的语法是否正确。由于它不能就地生成代码,它会将符号标记为“丢失”,但是链接器最终会找到并从 xyz.cpp 中借用它们。
不用说,这是可行的,因为两个文件都使用了 XYZ 和相同的成员函数。
该标准提供了一种在翻译单元中强制实例化模板及其所有成员函数的方法。这被称为显式实例化。
template class XYZ<int>;
可以使用名称空间和函数:
// assume that we included <vector>
template class std::vector<int>;
// assume that we included this template function:
// template <typename T>
// void f(T x)
template void f<int>(int x);
一种可能的用途是限制用户可以插入模板的类型集:
// xyz.cpp
template <typename T>
int XYZ<T>::size() const
{
return ...;
};
// these are the only types that the user will be able to plug in
// XYZ<T>. otherwise the program won't link.
template class XYZ<int>;
template class XYZ<double>;
template class XYZ<char>;
现在,这个翻译单元将包含 XYZ 所有成员函数的二进制代码,因此在组装最终的可执行文件时,它们可以被正确地“导出”到其他单元。
12.7.2. 外部模板
在 C++0x 中(作为许多经典 C++ 编译器的扩展),有可能阻止编译器自动实例化一个模板,并强制执行上一节中描述的行为。
extern template class XYZ<int>;
这迫使模板类像普通类一样链接(特别是,内联仍然是可能的),并且可以节省编译时间。
根据 C++ 标准,这种语法阻止隐式实例化,但是不阻止显式实例化。因此原则上可以将单个 extern 模板声明放在。hpp 文件(在模板代码之后),以及. cpp 文件中的单个显式实例化。 7
////////////////////////////
// special_string.hpp
template <typename T>
class special_string
{
public:
int size() const { ... }
};
extern template special_string<char>;
extern template special_string<wchar_t>;
////////////////////////////
// special_string.cpp
#include "special_string.hpp"
template special_string<char>;
template special_string<wchar_t>;
12.9.可变模板
从 C++11 开始,模板参数列表可以有可变的长度:
template <typename... T>
struct typearray
{};
template <size_t... N>
struct list_of_int
{};
typearray<int> t1; // Ok.
typearray<int, double, float> t3; // Also ok.
typearray<> t0; // An empty list also works.
省略号(。。。)到 T 左边的声明 T 可以匹配一个(可能是空的)参数列表。t 确实叫做模板参数包。另一方面,包含参数包名称的表达式右边的省略号扩展了它(简单地说,它克隆了包中每种类型的表达式)😗*
template <typename... T>
void doSomething(T... args) // conceptually equivalent to:
// T1 arg1, T2 arg2, ... , Tn argn
{
typearray<T> e; // error: T is unexpanded
typearray<T...> e; // ok, gives: <T1, ..., Tn>
list_of_int<sizeof(T)...> l; // ok, gives: <sizeof(T1), ..., sizeof(Tn)>
}
您可以使用模式匹配来“迭代”一个参数包:
void doSomething() // will match 0 arguments
{
}
template <typename HEAD, typename... TAIL>
void doSomething(HEAD h, TAIL... tail) // will match 1 or more arguments
{
std::cout << h << std::endl;
doSomething(tail...);
}
作为练习,看一下这个元函数计数
template <typename T, typename... A>
struct count;
template <typename T, typename... A>
struct count<T, T, A...>
{
static const int value = 1 + count<T, A...>::value;
};
template <typename T, typename T2, typename... A>
struct count<T, T2, A...> : count<T, A...>
{};
template <typename T>
struct count<T> : std::integral_constant<int, 0>
{};
省略号可以同时触发多个扩展。例如,假设您想要检查包中的类型是否重复两次:
template <typename T, typename... A>
int assert()
{
static_assert(count<T, A...>::value <= 1, "error");
return 0;
}
template <typename... N>
void expand_all(N...)
{
}
template <typename... A>
void no_duplicates(A... a)
{
expand_all(assert<A, A...>()...); // double expansion
}
这种双重扩展将调用:
expand_all(assert<A1, (all A)>(), assert<A2, (all A)>(), ...)
expand_all 获取任意数量的任意类型的参数,并完全忽略它们。这对于触发参数包的扩展是必要的。实际上,所有 assert <...>函数要么编译失败,要么返回 0,所以 no_duplicates 很容易被内联,几乎不产生任何代码。
1 由于形势发展迅速,请参考在线文档。找到一个同时完整和最新的对照表并不容易,但在撰写本文时,好的参考资料是【http://wiki.apache.org/stdcxx/C++0xCompilerSupport】的和cpprocks . com/c11-compiler-support-shootout-visual-studio-gcc-clang-Intel/
。
然而,一般来说,元函数返回一个“次优”值是可以接受的。如果已知一个类的析构函数很简单,那么可以优化代码。像“没有析构函数是微不足道的”这样激烈的假设可能会使程序变慢,但它不应该出错。
3 它们在可免费下载的“C++ 库扩展技术报告草案”(www . open-STD . org/JT C1/sc22/wg21/docs/papers/2005/n 1836 . pdf
)中有所描述。
兼容 C++11 的编译器附带的元函数列表可以在这里找到:【http://en.cppreference.com/w/cpp/header/type_traits】T4。
5 关于 auto 和 decltype 区别的详细解释,参见【17】。
6
当两个指令都存在时,旧的编译器不需要考虑这种行为,所以可能需要使用一些预处理器。*
十三、附录 A:练习
A.1 .练习
在以下所有问题中,读者应该假设给出了一个带有一些模板代码的可写文件;可以添加更多的文件,项目的其余部分是只读的。
A.1.1 .延期
给出了一个函数模板:
template <typename T>
void f(T x)
{
printf("hello T");
}
- 添加另一个重载,为从 BASE 派生的每个类调用该重载,并打印“hello BASE-or-derived”
- 确保您的解决方案是可靠的。把 f 的返回类型改成 int,看看你的解是否还成立
- 确保您的解决方案是可靠的。在同一个文件中添加一个普通函数——比如 int f(double x)——看看编译是否失败
- 想一个替代方案,最大限度地减少对现有代码的更改。
A.1.2 .整数
以下代码:
template <typename T>
uint32 f(T x) { ... }
// ...
printf("%x", f(a));
发出警告:f 的返回类型与%x 不兼容。
你会进行什么样的调查?
A.1.3 .日期格式
遵循第 7.7.1 节,使用更加自然的语法实现常量生成器,例如:
YYYYMMDD = dateformat<'Y','Y',Y','Y','M','M','D','D'>::value
或者
YYYYMMDD = dateformat<'Y','Y',Y','Y','/','M','M','/','D','D'>::value
A.1.4 .专业化
给出了一个模板类:
template <typename T>
class X
{ /* very long implementation... */ };
修改 X,使 X
A.1.5 .位计数
下面的代码:
// returns the number of bits of base
template <size_t BASE>
struct nb
{
static const size_t value
= nb<BASE % 8>::value
+ nb<(BASE/8) % 8>::value + nb<BASE/16>::value;
};
template <> struct nb<0> { static const size_t value = 0; };
template <> struct nb<1> { static const size_t value = 1; };
template <> struct nb<2> { static const size_t value = 1; };
template <> struct nb<3> { static const size_t value = 2; };
template <> struct nb<4> { static const size_t value = 1; };
template <> struct nb<5> { static const size_t value = 2; };
template <> struct nb<6> { static const size_t value = 2; };
template <> struct nb<7> { static const size_t value = 3; };
- 是完全正确的,它展示了一种在本书(或任何其他同等的书)中没有见过的新技术
- 有一个小错误,但该技术可与 3.6.6 节及以后的章节相媲美
- 至少有一个不小的错误,不容易修复
A.1.6 .质数
作为调试技术的一个练习,我们给出一个非平凡元函数 is _ prime
读者应该能够理解代码,至少在原则上,即使一些算法细节是未知的。
#define mxt_EXPLICIT_VALUE(CLASS, TPAR, VALUE) \
template <> struct CLASS<TPAR> { static const size_t value = VALUE; }
template <size_t N>
struct wheel_prime;
mxt_EXPLICIT_VALUE(wheel_prime, 0, 7);
mxt_EXPLICIT_VALUE(wheel_prime, 1, 11);
mxt_EXPLICIT_VALUE(wheel_prime, 2, 13);
mxt_EXPLICIT_VALUE(wheel_prime, 3, 17);
mxt_EXPLICIT_VALUE(wheel_prime, 4, 19);
mxt_EXPLICIT_VALUE(wheel_prime, 5, 23);
mxt_EXPLICIT_VALUE(wheel_prime, 6, 29);
mxt_EXPLICIT_VALUE(wheel_prime, 7, 31);
template <size_t A>
struct nth_tentative_prime
{
static const size_t value
= 30*((A-3)/8) + wheel_prime<(A-3) % 8>::value;
};
mxt_EXPLICIT_VALUE(nth_tentative_prime, 0, 2);
mxt_EXPLICIT_VALUE(nth_tentative_prime, 1, 3);
mxt_EXPLICIT_VALUE(nth_tentative_prime, 2, 5);
template
<
size_t A,
size_t N,
size_t K = nth_tentative_prime<N>::value,
size_t M = (A % K)
>
struct is_prime_helper
{
static const bool EXIT = (A < MXT_M_SQ(K));
static const size_t next_A = (EXIT ? 0 : A);
static const size_t next_N = (EXIT ? 1 : N+1);
};
template <size_t A, size_t N, size_t K>
struct is_prime_helper<A, N, K, 0>
{
static const size_t next_A = 0;
static const size_t next_N = 0;
};
template <size_t A, size_t N = 0>
struct is_prime
: is_prime<is_prime_helper<A, N>::next_A,
is_prime_helper<A, N>::next_N>
{
};
template <> struct is_prime<0,0> { static const bool value = false; };
template <> struct is_prime<0,1> { static const bool value = true; };
template <> struct is_prime<1,0> { static const bool value = true; };
template <> struct is_prime<2,0> { static const bool value = true; };
A.1.7 .不带 RTTI 的 Typeinfo
5.3.2 节中的 typeinfo 包装器依赖于编译器来为不同的类型生成运行时标识符。如果这不可用,那么可以使用不同的实现(至少在某些情况下):
- 创建一个 traits 类 TI
,它有一个返回 T()的静态成员函数 T f() - 使用 reinterpret_cast 并将&TI
::f 转换为 void (*)() - 使用后一个指针作为 std::map 中的索引
- 证明这是可行的(提示:由于 ICF,步骤#1 是必要的,参见第 354 页;对于步骤#3,请参见标准的第 20.3.3 节)
- 请注意,基于指针的类型标识符使用静态类型,而 typeinfo 使用动态类型,因此这种技术通常较弱。
A.1.8 .提示和部分解决方案
由于练习#1 的实际重要性,我们给出了它的解决方案。
显然单靠超载是不行的。我们可以选择 BASE,但 a DERIVED 会更喜欢模板函数(T=DERIVED,这是一个精确匹配)。
template <typename T>
void f(T x)
{
printf("hello T");
}
void f(BASE& x)
{
printf("hello BASE");
}
相反,我们引入了另一层:
template <typename T>
void f(T x)
{
g(&x, &x);
}
template <typename T>
void g(T* p, void*)
{
printf("hello T");
}
template <typename T>
void g(T* p, BASE*)
{
printf("hello BASE-OR-DERIVED");
}
优选将 T转换为 BASE。
注意,同样的技术也解决了另一个问题;当调用成员函数的参数时,我们可以防止虚调用:
template <typename T>
void g(T* p, void*)
{
printf("Not a BASE. No call was made.");
}
template <typename T>
void g(T* p, BASE* b)
{
b->doit(); // always virtual, if BASE::doit is virtual
p->doit(); // may be virtual or not
p->T::doit(); // always non-virtual
}
注意原则上 T 可能隐藏 BASE::doit,所以第二个调用不会是虚的:
class BASE
{
public:
virtual void doit();
};
class D1 : public BASE
{
public:
void doit(int i = 0);
};
class D2 : public D1
{
public:
virtual void doit();
};
十四、附录 B:参考文献
[1] Alexandrescu A .,《现代 C++ 设计》,Addison-Wesley
[2]d . Vandevoorde 和 and Josuttis,“C++ 模板:完全指南”,Addison-Wesley
[3] Abrahams D .和 Gurtovoy A .,“C++ 模板元编程”,Addison-Wesley
[4] Sutter H .,“异常的 C++ 风格”,Addison-Wesley
[5]威尔逊,《不完美的 C++》,艾迪森-卫斯理
[6] Austern M .,“泛型编程和 STL”,Addison-Wesley
[7] Cline M .,“C++ FAQ (lite)”,www . cs . rit . edu/~ mj/docs/c
++ FAQ/
[8]迈耶斯,“有效短期债务”
[9] Coplien,j .,“奇怪地重复出现的模板模式”,C++ 报告,1995 年 2 月,第 24-27 页。
[10] Stroustrup,b .,“C++ 的设计与进化”,Addison-Wesley,雷丁,MA,1993。
[11] Barton,J.J .和 Nackman L.R .,“科学与工程 C++”,Addison-Wesley,雷丁,MA,1994 年。
[12]t . Veldhuizen,“表达式模板”,C++ 报告,1995 年 6 月,在 Stanley Lippman 编辑的 C++ Gems 中重印。
[13] Myers Nathan C .,“一种新的和有用的模板技术:特征”,C++ 报告,1995 年 6 月,www.cantrip.org/traits.html
[14] C++ 0x (C++11)最终委员会草案:
ansi.org 的 c++ 11–ISO/IEC 14882:2011:60 美元
[15] Meyers S .,有效的现代 C++,O'Reilly,2014
[16] Meucci,a .“风险和资产分配”,Springer 2005 年
第一部分:先决条件
#incluse<prerequisites>
第二部分:技巧
#include<techniques>
第三部分:应用
#include<applications>