C++ 高级元编程(六)
七、代码生成器
本章讨论生成代码的模板——部分是静态的,部分是在运行时执行的。假设您必须执行一个简单的权力比较:
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 可能会使用 static_pow <2, deduce>,所以在源文件中后者必须在前者之前。
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 的容器。这个容器的一个实例通过一个插入成员函数一次接收一个 T 类型的值, 12 ,它可以被要求提供到目前为止遇到的最小的 N 个元素。
出于稍后将讨论的原因,让我们强加一个额外的要求,即容器不应该从动态内存中分配它的工作空间。
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]为了简单起见,在类型上,它总是返回选择器,除了 7 → MyType1 和 13 → MyType2。
再次假设我的类型只是选择器。
现在你会看到一个模板类 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 初始化...}.*