C++ 高级元编程(二)
二、小对象工具包
前一章重点介绍了模板编程和风格之间的联系。简而言之,模板是优雅的,因为它们允许您编写看起来简单的高效代码,因为它们隐藏了潜在的复杂性。
如果你还记得第一章中 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;
注意,选择器的所有实例都传达相同的信息。由于 instance_of 和 selector 的构造成本较低,因此它们对于替换显式模板参数调用都很有用:
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 替换选择器**。事实上从现在开始,你可以假设后者的实现是一样的。 2**
在 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 的一个实例传递给一个需要 int 的函数。然而,编写外部函数通常更安全:
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。然而,如果编译器添加了填充(假设 T 是 char,并且任何结构的最小大小都是 4 个字节),那么第二个不等式可能确实是严格的。
该类最重要的用途是定义两种类型(参见第 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::type)= = n。
注意 fixed_size 本身可以有任意大小(至少 N,但也可能更大)。
记住,声明一个返回数组的函数是非法的,但是一个数组的引用是可以的,并且具有相同的大小 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 只有在 I 的行为类似于具有值类型(可转换为)t 的迭代器时才会编译。例如,如果 I 不支持后增量,编译器将停止并在 assert _ iterator::verify _ postincr 中报告错误。
请记住,由于宏中对逗号字符的通常限制,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:::type 的使用结合起来,在附加的宏中进行包装:
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 ,它将包含 J=1,2,4,8 的 fixed_size_allocator 。当不存在合适的 J 时,它还将求助于::operator new(都是在编译时)。
这种分配的语法是 11 :
compound_pool<64> A;
double* p = A.allocate<double>();
这个想法的梗概是这样的。复合池包含一个固定大小分配器并从复合池中派生。因此,它可以直接接受 N 字节的分配请求,并将所有其他标签分派给基类。如果最后一个 base compound _ pool<0>接受调用,则不存在更好的匹配,因此它将调用 operator new。
更准确地说,每个类都有一个返回分配器引用或指针的 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:::reference,但是 value_type 可以由 reference 构造。
9 特别是,这也将处理随机访问迭代器。也就是说,它盲目地忽略了基本/派生标签层次结构。
10 这个例子预见了 7.3 节的观点。
11 有意省略了解除分配。*
三、静态编程
模板非常善于迫使编译器和优化器只在生成可执行程序时执行一些工作。根据定义,这叫做静态工作。这与动态工作相反,动态工作指的是程序运行时所做的事情。
有些活动必须在运行时之前完成(计算整数常量),有些活动对运行时有影响(为函数模板生成机器码,稍后执行)。
TMP 可以产生两种类型的代码——元函数,完全是静态的(例如,元函数 unsigned_integer :::返回至少包含 N 位的整数的类型)和混合算法,部分是静态的,部分是运行时的。(STL 算法依赖于 iterator_category 或者 4.1.2 节解释的 zeroize 函数。
本节讨论编写高效元函数的技巧。
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 :::值,编译器需要产生 K 个实例:static_raise ,static_raise ,...
最终 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:::type:
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:::type。当编译器处理 typeif 时,它只需要知道第二个和第三个参数是有效类型;也就是说,它们已经被声明。如果条件为假,aux_t 被设置为 G 。否则,设置为的 instance_of。因为还没有请求成员,所以没有编译任何其他内容。最后,最后一行触发编译的 instance _ 或 G 。
所以,如果条件为真,G:::type 永远不会被使用。G 甚至可能缺少定义,或者可能不包含名为 type 的成员。
总结一下:
- 尽可能延迟访问成员
- 包装物品以利用界面
相同的优化适用于常数:
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;
};
正如所写的,每个助手诱导编译器再次实例化模板两次——即助手< U,N-N/2 >和助手< X,N/2>——即使只使用一个。
静态短路可以减少编译时间,如果将所有算法都移到类型内部,效果会更好。 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 对 x 的每个值最多实例化五六个类。
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 写另一个算法,而不是为 sin 写。
您可以为 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 更加专门化,所以它将被明确地使用。 9</ptrdiff_t>
这种技术的伸缩性不好,但是可以通过添加更多未命名的参数来扩展到少量的类型定义。这个例子使用了 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:::type 这样的调用应该返回一个在平台中不变的结果,所以所有的定制(如果有的话)应该是全局的,以保持客户端代码的一致性。
更灵活的方法是使用局部特征,将适当的类型作为附加参数传递给每个模板实例(默认为全局值)。
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:::value 为 false,因为 T&不满足与 const 类型的匹配。然而,add_const ::type 仍然是 T&(应用于引用的任何限定符都被忽略)。这是否意味着引用是常量?
是否应该添加返回 true 的 is_const 的专门化?还是真的希望 add_const ::type 是 const T&?
在 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 :::值应为 false。
然而,类型特征也很容易在用户代码中扩展。如果应用程序需要的话,您可以引入更多的概念,比如“侵入性常数”
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 是一个类模板,它包含一个 T 类型的成员,由构造函数初始化。然而,T 没有限制,特别是它可以是一个参考。
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 根据需要返回 const T&。
如果 T 是对 const 的引用或引用,则 const T 是 T,add_reference 返回 T,这意味着参数类型也是 T。
3.5.2.拆除类型
通过添加限定符、考虑引用、指针和数组等等,C++ 中的一个类型可以生成无限多的“变体”。但是可能发生的情况是,您必须递归地删除所有附加属性,一次删除一个。这个递归过程通常被称为拆解。 20
本节展示了一个名为 copy_q 的元函数,它将所有“限定符”从类型 T1 转移到类型 T2,因此 copy _ q:::type 将是 const int&。
类型演绎是完全递归的。一次分解一个属性,并将同一个属性移动到结果中。继续前面的例子,const double&匹配 T&其中 T 是 const double,所以结果是“对 copy_q 的结果的引用”,这又是“copy_q 的 const 结果”。由于这不匹配任何专门化,所以它给出 int。
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 >只需要一个模板实例化。typeat >可能需要 N 个不同的实例化。
还要注意较短的实现:
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 未定义时,例如 typeat ,返回错误的常见选项包括:
- 完全移除 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 的深度为 1;而实际上 typearray <mxt_list_n>的深度是 n</mxt_list_n>
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
事实上,这个调用将匹配深度,其中恰好 T3 =空。
在任何情况下,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 并递归继承自 A < pop_front>。“使用”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 ,A 的布局将如图图 3-4 所示。
图 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 (a)修复了该对的第一个类型,并让编译器匹配后面的尾部。由于唯一性假设,这确实是可能的。
事实上,C++ 标准允许在参数推导之前进行一次从派生到基的转换,如果这是精确匹配的充分必要条件的话。
这里,将类型 A 的参数绑定到对类型 A < typepair<:string tail_t="">的引用的唯一方法是将其转换为类型对<:string empty="">,然后推导出 tail_t = empty。
为了存储从一个聚集中提取的值,声明一个 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:::type,编译器将输出 front 不能在 int 上实例化(如果你没有定义主模板)或者它不包含成员类型(如果它是空的)。
然而,这种保护在这里是不必要的。convert 构建在 front 和 pop_front 之上,它们将执行所需的参数验证。在这种情况下,编译器将诊断出在 convert 内实例化的 front 是非法的。
问题只是一个不太清楚的调试信息。在您必须纠正问题的选项中,您可以编写类型特征来标识类型容器,然后放置断言:
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::type 为 void。
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,templateclass F>,它返回 T 中的第一个类型,其中 F < X > ::value 为 true。**