第34章 仿函数和区间(3)
Mathew Wilson /著
刘未鹏(pongba) /译
34.3.3 内嵌的仿函数
缓解这种物理上的分离问题的一个办法可能是把仿函数定义到使用它们的函数体中。例如,我们可能想将argument_saver定义到fn()中,像这样:
void CoalesceOptions(. . .)
{
. . .
struct argument_saver
{
. . .
};
std::for_each( usedOptions.begin(), usedOptions.end()
, argument_saver(arguments));
. . .
唉,然而局部仿函数类在C++中却是非法的,因为一个模板实参必须具有外部连接(C++98:14.3.2)[1]。
Imperfection: C++不支持局部仿函数类(被用于模板算法)。
虽然这是非法的,但还是有一些编译器允许它:CodeWarrio、Digital Mars、以及Watcom。此外,通过一个trick——把仿函数类作为另一个类的内嵌类——Borland和Visual C++也支持它。像这样:
Listing 34.3
void CoalesceOptions(. . .)
{
. . .
struct X
{
struct argument_saver
{
. . .
};
};
std::for_each( usedOptions.begin(), usedOptions.end()
, X::argument_saver(arguments));
. . .
}
Table 34.1总结了一些流行的编译器对这两种形式的支持。Comeau、GCC以及Intel则两者都不支持[2]。
如果你正使用Borland、CodeWarrior、Digital Mars、Visual C++、Watcom中的一个,那么你可以选择这个方法。但是必须警告:这是非法的,这么做会危及可移植性。
Table 34.1
Compiler
Local class
Nested local class
Borland
No
Yes
CodeWarrior
Yes
Yes
Comeau
No
No
Digital Mars
Yes
Yes
GCC
No
No
Intel
No
No
Visual C++
No
Yes
Watcom
Yes
Yes
34.3.4 温和一点
我所知的可以让这种代码工作的唯一合法途径却是华而不实的(其代价让人无法承受)。既然模板实参必须具有外部连接,那么我们可以先在函数外部定义一个仿函数。由于我们是想要在一个局部类中特化仿函数的行为,所以我们可以把外部和内部的类联系到一起。既然我们不能使用模板,那么我们就退回到C++的“老干将”——多态——身边来。局部类argument_saver继承自具有外部连接的argument_processor类,并且重写其operator()() const,像这样:
Listing 34.4
struct argument_processor
{
public:
virtual void operator ()(OptsUsed_map_t::value_type const &o) const = 0
};
void CoalesceOptions(. . .)
{
. . .
struct argument_saver
: argument_processor
{
virtual void
operator ()(OptsUsed_map_t::value_type const &o) const
{
. . .
}
};
看起来还不赖。然而,要将它使用在模板算法上必须有一个前提:模板实参必须是具有外部连接的那个类(基类)。并且,由于基类是纯虚类,所以仿函数必须被作为const引用传递,而不是传值。
此外,由于std::for_each()将仿函数类型作为其第二个模板形参,所以我们还得显式给出iterator的类型。因此,使用这个“方便的”for_each()的方式就显得臃肿——当然,更称不上完美:
for_each< OptsUsed_map_t::const_iterator
, argument_processor const &>( &ari[0], &ari[10]
, argument_saver());
到了这个地步,你可能会赞同手动枚举更好一些。但是等一下,还有个办法可以让上面的代码变得更漂亮一点:定义一个等价的for_each,它的形参排列顺序和std::for_each相反,这样(仿)函数类型可以被隐式的推导出来:
template< typename F
, typename I
>
inline F for_each_l(I first, I last, F fn)
{
return std::for_each<I, F>(first, last, fn);
}
这引出了最终的形式:
for_each_l<argument_processor const &>( &ari[0], &ari[10]
, argument_saver());
但是为了我口袋里的钱,这仍然从哪里看都不像是完美的。考虑可怜的做(代码)维护的程序员(它们该如何去理清这堆东西)吧![3]
34.3.5 泛化的仿函数:类型隧道(Type Tunneling)
如果我们不能把仿函数变得更局部化,或许我们可以通过把它们变得更通用来改善这个情况?让我们来看一个例子,其中我们可以扩大is_large(见34.1)的一般性。我们可以把它使用于value_type为(或者可以转换为)char const*的序列,比如glob_sequence。遗憾的是,它也仅仅能被用于这些类型了。如果我们想要将同样的函数使用于一个使用Unicode编码和wchar_t的类型,则不行。
这个问题的答案之一是将is_large改为一个模板,通过其接受的字符(character)类型来参数化:
template <typename C>
: public std::unary_function<C const *, bool>
struct is_large
{
bool operator ()(C const *file) const;
};
现在,只要我们指定恰当的模板参数,这可以和char或wchar_t的序列协同工作了(使用虚构的Unicode版的globw_sequence为例):
glob_sequence gs("/usr/include/", "impcpp*");
n = std::count_if(gs.begin(), gs.end(), is_large<char>());
globw_sequence gsw(L"/usr/include/", L"impcpp*");
n = std::count_if(gsw.begin(), gsw.end(), is_large<wchar_t>());
这可有用多了,但仍然不是全部。回顾20.6.3节,我们考察了另一个文件系统枚举序列readdir_sequence,它的value_type——struct dirent const* ——并不能隐式的转换为char const*。当时这个问题的解决方案是使用Access Shims(20.6.1节),这里我们可以故技重施。然而,由于涉及了模板,所以目前的情况更为复杂一点,见Listing 34.5:
Listing 34.5
template< typename C
, typename A = C const *
>
struct is_large
: public std::unary_function<A, bool>
{
template <typename S>
bool operator ()(S const &file) const
{
return is_large_(c_str_ptr(file)); // apply c_str_ptr shim
}
private:
static bool is_large_(C const *file)
{
. . . // determines whether large or not
}
};
其中,operator()()const被改成了一个模板成员函数,它试图把任意类型通过c_str_ptr()转换为C const*,然后把结果传给is_large_(),后者是static成员函数,负责具体实现。现在我们可以对任何类型使用该仿函数了——只要对该类型存在合适的(并是可见的)c_str_ptr()定义,因此:
readdir_sequence rs("/usr/include/");
n = std::count_if(rs.begin(), rs.end(), is_large<char>());
我把这种机制称为类型隧道(Type Tunneling)。
定义: 类型隧道是一种通过Access Shims使两个逻辑相关但物理不相关的类型能够互操作的机制。shim允许一个外部类型通过一个接口以另一番姿态(可识别,兼容)呈现在内部类型的面前。
我在以往的几年中一直将这种机制使用在我的工作中,效果很好。除了通过使用C-string形式来将一集物理上不相关的类型间的互操作轻松地去耦之外,还有对句柄(handles),指针甚至同步对象的泛化操纵。类型隧道(和shims)成功地把编译器变成了一个“通讯员”。在21.2节我们曾看到关于类型隧道的另一个例子,藉此几乎所有与COM相容的类型都可以“穿越隧道”呈现在logging API面前——将模板构造函数和InitialiseVariant()作为access shim。
[1] 这条规则对所有模板都有效,并非只是模板算法和仿函数。
[2] 如果这三者都说你的代码是错的,那么几乎可以肯定的说——你确实是错的。
[3] 在开始写这一章到最终完成之前有几个星期的间隔,在这段不算长的时间内,我竟忘了这是如何工作的,所以我把它写了下来。