C++成员函数指针与泛型编程

在CodeProject VisualC++论坛上,一个提问引发了对成员函数指针的研究。随后,将之前在该线程中展示的代码片段整理成更通用的形式,并考虑了一些更典型的用例。

成员函数指针

普通函数指针的声明方式如下:

bool (*pfn)(int);

其中,pfn是一个指向接受单个整数参数并返回布尔值的函数的指针。示例用法如下:

bool f1(int i) { return i > 10; } int main() { bool (*pfn)(int); pfn = f1; if ((*pfn)(11)) { std::cout << "pfn(11) true\n"; } return 0; }

pfn的值将成为CPU理解的实际函数地址。成员函数指针略有不同:

bool (Foo::*pmfn)(int);

pmfn是指向类Foo的成员函数的指针,该函数接受单个整数参数并返回布尔值。

class Foo { private: int t_; public: Foo(int t) : t_(t) {} bool f1(int i) { return i > t_; } }; int main() { bool (Foo::*pmfn)(int); pmfn = &Foo::f1; Foo foo(10); if ((foo.*pmfn)(11)) { std::cout << "pmfn(11) true\n"; } return 0; }

这段代码显然没有实际意义,但它足以展示语法。pmfn可以设置为Foo的任何成员函数,只要该函数的签名与int参数和bool返回值相匹配。成员指针的实际实现取决于类是否有虚函数,在这种情况下,指针必须是虚函数指针表地址和其中偏移量的组合。对于普通类,指针可能是传统的函数指针。细节依赖于实现和架构。

函数对象

函数对象是类或结构体的实例,可以在语法上被视为函数。

struct PlusFunctor { int operator()(int l, int r) { return l + r; } }; int main() { PlusFunctor f; std::cout << "f(3, 4) = " << f(3, 4) << std::endl; return 0; }

函数适配器

函数适配器是一种函数对象,允许组合函数或函数对象,或者绑定参数。STL提供了一个有趣的函数适配器类,允许调用类的成员函数:

class SomeObject { private: int value_; public: SomeObject() : value_(0) {} SomeObject(int value) : value_(value) {} void ShowValue() { std::cout << value_ << "\n"; } }; int main() { std::vector v; v.push_back(SomeObject(1)); v.push_back(SomeObject(2)); v.push_back(SomeObject(3)); v.push_back(SomeObject(4)); std::for_each(v.begin(), v.end(), std::mem_fun_ref(&SomeObject::ShowValue)); return 0; }

这是以一种相当可读和简洁的方式调用集合中每个对象的指定成员函数。但是,如果想要调用集合中每个对象的其他对象的成员函数呢?一个场景可能是将一系列更新应用到文档上:

class Update { private: // update data public: Update() {} // access functions }; class Document { private: // document data public: Document() {} void ApplyUpdate(const Update& update) { // ... } // other document functions };

有两种明显的应用更新的方法,手动编写循环以枚举容器:

template void ApplyUpdatesToDocument1(Document& doc, Container& updates) { for (auto it = updates.begin(); it != updates.end(); ++it) { doc.ApplyUpdate(*it); } }

通过将容器类型作为模板参数,可以选择使用vector、list、stack等。编译器可以在调用时推断容器的类型:

int main() { std::vector vu; vu.resize(10); Document doc; ApplyUpdatesToDocument1(doc, vu); return 0; }

然而,当for_each就在那里等待帮助时,不得不编写自己的循环似乎不够优雅。将for_each带入行动的一种方法是创建一个自定义函数对象,它保留对文档的引用:

struct ApplyUpdateFunctor { Document& doc_; ApplyUpdateFunctor(Document& doc) : doc_(doc) {} void operator()(const Update& update) { doc_.ApplyUpdate(update); } }; template void ApplyUpdatesToDocument2(Document& doc, Container& updates) { std::for_each(updates.begin(), updates.end(), ApplyUpdateFunctor(doc)); }

这很好,除了必须为每种类型的操作编写一个函数对象,将它们存储在某个地方,并在一周、一个月或一年后回到代码时记住它们的作用。

核心内容

通过一些模板魔法,可以编写一个通用函数对象,如下所示:

template void ApplyUpdatesToDocument3(Document& doc, Container& updates) { std::for_each(updates.begin(), updates.end(), mem_fun_bind1(doc, &Document::ApplyUpdate)); }

调用看起来比ApplyUpdatesToDocument2更复杂,但它不需要在其他地方定义自定义函数对象,操作直接写在for_each调用中。希望对每个更新调用doc.ApplyUpdate()。后续维护应该希望需要更少地浏览其他文件。

以下是上面使用的通用函数对象代码:

template struct mem_fun_bind1_t : public std::unary_function { Result (C::*pmf_)(Arg); C& rC_; explicit mem_fun_bind1_t(C& rC, Result (C::*pmf)(Arg)) : rC_(rC), pmf_(pmf) {} Result operator()(Arg a) { return (rC_.*pmf_)(a); } }; template mem_fun_bind1_t mem_fun_bind1(C& c, Result (C::*fn)(Arg)) { return mem_fun_bind1_t(c, fn); }

辅助函数mem_fun_bind1生成函数对象mem_fun_bind1_t。这允许编译器推断模板参数,并简化了必须手工编写的内容。mem_fun_bind1_t与上面显示的ApplyUpdateFunctor直接类似,增加了适当的成员函数指针和转换为模板以允许与不同对象和成员重用。

对于具有两个参数的二元函数的等效函数对象:

template struct mem_fun_bind2_t : public std::binary_function { Result (C::*pmf_)(Arg1, Arg2); C& rC_; explicit mem_fun_bind2_t(C& rC, Result (C::*pmf)(Arg1, Arg2)) : rC_(rC), pmf_(pmf) {} Result operator()(Arg1 a1, Arg2 a2) { return (rC_.*pmf_)(a1, a2); } }; template mem_fun_bind2_t mem_fun_bind2(C& c, Result (C::*fn)(Arg1, Arg2)) { return mem_fun_bind2_t(c, fn); }

这个二元版本将适合原始std::sort问题的发布者。扩展到更多(或更少)参数,如果能找到它们的用途,留给读者作为练习。

构建说明

本文中的所有代码都已使用VisualC++版本7.1构建和测试。这显然需要添加适当的包含文件,如iostream、vector、algorithm和functional。

没有要下载的文件,将包含函数对象和辅助函数的头文件的名称选择留给。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485