第34章 仿函数和区间(2)
Mathew Wilson /著
刘未鹏(pongba) /译
34.2.1 数组
正如我们在第14章所看到的,让数组大小的定义出现在两个(或更多)地方是错误的潜在源头。即使它们使用了相同的常量,从效果上说,仍然存在两个定义:
int ari[10] = { . . . };
std::for_each(&ari[0], &ari[10], print_int);
同样,我们也看到:处理这种情况的最简单而有效的办法是使用dimensionof()来静态的确定数组大小:
std::for_each(&ari[0], &ari[dimensionof(ari)], print_int);
然而,有了for_all(),我们可以很轻易的对数组做特化,像这样:
template< typename T
, size_t N
, typename F
>
inline F for_all(T (&ar)[N], F f)
{
return std::for_each(&ar[0], &ar[N], f);
}
34.2.2 命名
现在,让我们考虑如何为这些算法命名。我故意选择了一个不合适的名字,这样我们就会觉得不满意。for_all()这个名字的问题很明显:它不能照搬到其它算法身上。比如说,我们有fill_all()、accumulate_all()...吗?所以这并非一个具有吸引力的选择。理想情况下,我们也应当把它称为for_each()——那么为什么不呢?遗憾的是,这有点困难:我们必须作出不愉快的选择。
我们仅仅可以特化已经存在于标准名字空间中的模板;而不能将新的模板(或非模板)函数或类型加入标准名字空间。如果我们把for_all()称为for_each(),那么其数组版本并不会被接纳为一个基于新类型的for_each特化版本——坦白的说,这个限制是很明确的。
替代方案是在我们自己的名字空间中定义for_each,如果这样做的话,我们得记住:任何时候,想要在该名字空间外部使用for_each的话,得先使用using声明(using declaration)来“use”该名字。不幸的是,我们还得记住:在需要用到标准名字空间中的for_each时,我们还得再加上using std::for_each——这是因为我们自己的for_each的using声明会隐藏位于std名字空间中的for_each。如果不这样做则可能会导致非常复杂的(编译期)错误信息。但是,如果先撇开这个小缺点不管,对于相同算法具有不同名字从而妨碍编写通用代码的问题,这种把同名算法置于自己的名字空间中的做法还是很值得考虑的[1]。
我们来看一个关于名字空间的问题:假设你通过一个using指示(using directive)(即using namespace ... ——译注)来使用你的库中的所有组件:using namespace acmelib。你会说这没问题,因为在下面的客户代码中你会使用acmelib名字空间中的很多东西,其中之一就是你的for_each的数组版本。但是,不一会儿,你又需要使用std::for_each了,于是你加入一行using声明:
using namespace acmelib; // Using directive
using std::for_each; // Using declaration
int ai[10];
for_each(ai, print_int);
然而,现在你的代码却无法通过编译了,因为通过using声明(using declaration)引入的std::for_each隐藏了通过using指示(using directive)引入的acmelib::for_each——using声明比using指示具有更高的优先级。那么我们该怎么做呢?如果我们的代码中所使用到的函数或类型在两个名字空间中具有相互冲突的定义,会发生什么呢?
我们不得不加入一个额外的using acmelib::for_each。既然如此,为什么不在一开始就使用using声明呢?这样的话,一开始可能会增加一点工作量,但是从长远来看却是在节省工夫,我们都知道:在软件开发中,与后期维护比起来,一开始的编码阶段的代价简直算不了什么[Glas03]。这也是为什么我在任何场合都拒绝使用using指示的缘故[2]。
在这种情况下,我们新建的for_each和std::for_each具有不同个数的参数,所以我们不可能写出和两者都能“合作”的通用代码。因此,我们仅仅把我们的算法命名为:for_each_c()以及fill_c()等等。
在本章的末尾我们会再次回顾名字问题。
34.3 局部仿函数
到目前为止,我们都在考虑语法问题。然而,还有一个更为重要的问题困扰着我们使用STL算法,这个问题可算是一个较为严重的缺陷。如果已经存在一个合适的函数或仿函数,那么(使用算法的)代码会很简洁,能力也很强大:
std::for_each(c.begin(),c.end(),fn());
然而,一旦你想要对一个区间中的元素做一些更为复杂或特殊的动作,你就面临两个选择——无论哪一个都不算特别吸引人。你要么手写循环,要么封装一个函数或仿函数。
34.3.1 手写循环[3]
通常我们的选择是把算法展开,手写循环。下面的例子来自Arturius编译器的复用器(见附录C):
List 34.1
void CoalesceOptions(. . .)
{
. . .
{
OptsUsed_map_t::const_iterator b = usedOptions.begin();
OptsUsed_map_t::const_iterator e = usedOptions.end();
for(; b != e; ++b)
{
OptsUsed_map_t::value_type const &v = *b;
if( !v.second &&
v.first->bUseByDefault)
{
arcc_option option;
option.name = v.first->fullName;
option.value = v.first->defaultValue;
option.bCompilerOption = v.first->type == compiler;
arguments.push_back(option);
}
}
}
. . .
通常,这样的代码会变得相当冗长。我可以从相同的文件中再找出一段长得多的代码。
34.3.2 自定义仿函数
手写循环的替代方案是用自定义的仿函数来提供你需要的行为。如果该仿函数可以被复用到各种各样的场合,那自然最好不过,但是通常只有有限的一个或几个地方用到。
因为仿函数是一个单独的类,所以它会与它的使用点从物理上分开,这就会导致代码难以理解和维护。你所能做到的最好的方式就是把仿函数类的定义和使用它的代码放在同一个编译单元中,最好紧紧靠在使用它的函数前面。
Listing 34.2
struct argument_saver
{
public:
argument_saver(ArgumentsList &args)
: m_args(args)
{}
void operator ()(OptsUsed_map_t::value_type const &o) const
{
if( !o.second &&
o.first->bUseByDefault)
{
arcc_option option;
. . .
m_args.push_back(option);
}
}
private:
ArgumentsList &m_args;
};
void CoalesceOptions(. . .)
{
. . .
std::for_each( usedOptions.begin(), usedOptions.end()
, argument_saver(arguments));
. . .
然而,封装在仿函数中的领域特定的代码仍然从物理上与使用它的(唯一的)地方分离开了,这并不理想。这种分离降低了可维护性,更糟的是,这会鼓励为了复用(或重构)它而进行过度的代码工程。
[1] 这也正是John在他的RangeLib(与Boost兼容的版本)中采用的办法,尽管他更偏向于使用显式的名字空间限定。,如:boost::rtl::rng::for_each。
[2] 在java中使用import x.*的经验教训也告诉我,不要不分青红皂白就把一堆名字引入当前名字空间中(即不要冒昧使用using namespace x。Herb Sutter和我在这个问题上持不同意见。Herb对C++具有更多更广博的知识,而我呢,这是我的书,我想怎么写就怎么写:-)
[3] 译注:这里的原文是Unrolled Loops,直译是“解循环”或“展开循环”。但是作者的意思仅仅是把使用算法的隐藏循环的方式“展开”为手动的for或while循环,所以译为手写循环,应该不会影响读者理解。