将C++中的成员函数指针传递给父类

4
我该如何使以下代码正常工作?我不能将成员设置为静态的,父类不知道子类,而且我无法使用boost。我不使用虚函数的原因是Child类应该能够定义1-N个处理程序。
class Parent
{
public:
    void registerFileHandler(string ext, memFuncPtr);
};

class Child : public Parent
{
    Child()
    {
        registerFileHandler("jpg", &Child::loadJpg);
        registerFileHandler("png", &Child::loadPNG);
    }
    void loadJpg(string filename);
    void loadPNG(string filename);
};

编辑: 有许多答案,而对我来说最好的是使用关键词“erasure”,“std :: bind”和“std :: function”,当然这些都依赖于c ++ 11。 以下是一个完整可编译示例:

#include <string>
#include <map>
#include <iostream>
#include <functional>

using namespace std;

class Parent
{
public:
    void load(string filename)
    {
        // See if we can find a handler based on the extension.
        for(auto it = handlers.begin();it!=handlers.end();it++)
            if(filename.substr(filename.size()-it->first.size(), it->first.size())==it->first)
                it->second(filename);
    }
    template<typename Class>
    void registerFileHandler(Class* p, void (Class::*func)(string), string ext)
    {
        using namespace std::placeholders; //for _1, _2, _3...
        handlers[ext] = std::bind(func, p, _1);
    }
private:
    map<string, std::function<void(string)> > handlers;
};

class Child : public Parent
{
public:
    Child()
    {
        registerFileHandler(this, &Child::loadJpg, "jpg");
        registerFileHandler(this, &Child::loadPNG, "png");
    }
    void loadJpg(string filename)
    {
        cout << "loading the jpeg "<< filename << endl;
    }
    void loadPNG(string filename)
    {
        cout << "loading the png "<< filename << endl;
    }
};


int main(int argc, char* argv[])
{
    Child child;
    child.load("blah.jpg");
    child.load("blah.png");
    return 0;
}

我想请教有关如何使用std::function的问题。因为我正在传递一个类型为“void(Child&,string)”的函数,所以“typedef std :: function <void(Parent&,string)> memFuncPtr;”无法正常工作。 - bgp2000
如果Child::loadJpg不是静态的,你需要一个实际的Child对象。那么Parent如何知道要使用哪个呢?是dynamic_cast<Child*>(this)吗? - MSalters
7个回答

4

您需要一些类型擦除。假设您不能使用已经存在的复杂类型擦除方法(boost::function, std::function),那么您可以自己编写:

class MemFuncPtr {
    void *obj;
    void (*caller)(void*, string);
  public:
    MemFuncPtr(void *obj, void(*caller)(void*, string)) : obj(obj), caller(caller) {}
    void operator()(string filename) {
        caller(obj, filename);
    }
};

class Child : public Parent
{
    Child()
    {
        registerFileHandler("jpg", MemFuncPtr(this, &jpgcaller));
        registerFileHandler("png", MemFuncPtr(this, &pgncaller));
    }
    void loadJpg(string filename);
    void loadPNG(string filename);
  private:
    static void jpgcaller(void *obj, string filename) {
        static_cast<Child*>(obj)->loadJpg(filename);
    }
    static void pngcaller(void *obj, string filename) {
        static_cast<Child*>(obj)->loadPng(filename);
    }
};

我认为你可以使用一个具有指向成员的模板参数的函数模板来摆脱那些static函数。但是,如果我在没有测试的情况下编写代码,我可能会把它搞糟...


@JanHudec:那么你的回答将会比我的得到更多的赞,如果你写的话;-) - Steve Jessop
嗯,你说得对;它不会自动转换并且似乎不正确,尽管MFC在所有地方都这样做。 - Jan Hudec
@JanHudec:不过,你已经相当让我信服了。你可以使用 static_cast 来实现,我尝试了一个具有多继承的案例,并且即使在基础指针上调用 (void*)baseptr != (void*)childptrloadJpg 函数也会接收到正确的 this 值。所以显然在这里存在一种机制(thunk/trampoline 代码?或者只是指向成员函数的指针中的干扰项),使其正常工作。 - Steve Jessop
@JanHudec:C++03中的5.2.9/9(在C++11中为/12)表示,即使基类不包含成员,这也适用于指向成员的指针。但它没有明确提到指向成员函数的指针。 - Steve Jessop
谢谢。我已经在4.11/2中找到了相反方向的条款,但是又错过了这个。我已经查过一次,因为我想知道MFC中的使用是否无效或编译器是否违反规范。但我没有正确记住它。看来最终还是编译器的问题。 - Jan Hudec

4

关于 std::functionstd::bind

class Parent
{
public:

    void registerFileHandler(string ext, const std::function<void(string)> &f)
    {
    }
};

class Child : public Parent
{
public:
    Child()
    {
        using namespace std::placeholders; //for _1, _2, _3...
        registerFileHandler("jpg", std::bind(&Child::loadJpg, this, _1));
        registerFileHandler("png", std::bind(&Child::loadPNG, this, _1));
    }

    ... 

1
有点杀鸡焉用牛刀。当然,它会增加很好的灵活性。 - Jan Hudec
这是我选择的那一个。我只是集成了Jan Hudec的模板函数,以将可能会令使用者感到困惑的C++x11内容隐藏起来。 - bgp2000

2

有很多关于如何解决传递函数指针的建议 - 也许您可以稍微重新设计,并正确使用继承..

class FileLoader
{
  virtual void load() = 0; // real load function
};

class LoadManager
{
  // Here is your registry
  std::map<std::string, std::uniqe_ptr<FileLoader>> _loaders;
};

class JpegLoader : public FileLoader
{
};

class BitmapLoader : public FileLoader
{
};

// etc.

// Now register these with the LoadManager and use from there...

这个设计看起来不是更清晰了吗?显然,这个建议基于你发布的简单代码片段,如果你的架构更加复杂,则情况就不同了...


如果每个类只处理一种类型,那么我只需要经典的多态性。 - bgp2000
但是,我在问题中指出我想让一个类能够注册N个回调函数。 - bgp2000

0
class Child;  // unfortunately this is needed

class Parent
{
public:
    void registerFileHandler(string ext, void (Child::*mem_fn) (string), Child* obj) {
                                       // ^^^^the member func  ^^^^^^^^  //the object to be used 
         (obj->*mem_fn)("test");
    }
};


class Child : public Parent
{
    Child()
    {
        registerFileHandler("jpg", &Child::loadJpg, this);
        registerFileHandler("png", &Child::loadPNG, this);
                                                            // You need to pass this.
    }
    void loadJpg(string filename);
    void loadPNG(string filename);
};

然而,你所做的可能过于复杂了。难道你不能使用这样的东西吗?

class Parent
{
public:
    virtual void registerFileHandler(string ext)  = 0;
};


class Child : public Parent
{
    Child()
    {
        registerFileHandler("jpg");
        registerFileHandler("png");
    }
    void loadJpg(string filename);
    void loadPNG(string filename);

    virtual void registerFileHandler(string ext)   {
        loadJpg(ext);
        loadJpg(ext);
    }
};

通常情况下,你会有不止一个子元素。 - MSalters
父级不知道子级。 - bgp2000
@bgp2000 这就是为什么不幸地必须要前向声明它的原因。 - stardust
1
父类无法知道子类的存在,因为父类位于库中,而子类是在稍后编写的用户代码。 - bgp2000
它并不涵盖相同的内容。registerFileHandler() 不应该立即使用参数 "jpg" 调用 loadJpg()。在稍后的某个时刻,当它找到一个扩展名为 "jpg" 的文件时,它应该使用文件名调用 loadJpg。 - bgp2000
显示剩余2条评论

0

从声明的角度来看,你可以轻松地摆脱它

class Parent
{
public:
    template <typename Child>
    void registerFileHandler(string ext, void (Child::*memFuncPtr)(string filename));
};

当然,这并没有解决如何存储这些指针的问题。一个常见的解决方案是

struct Handler {
   virtual ~Handler() { };
  virtual void Load(Parent*, std::string) = 0;
};
template<typename Child>
struct ChildHandler {
  void (Child::*memFuncPtr)(string filename);
  virtual void Load(Parent* p, string filename) {
      dynamic_cast<Child&>(*p).*memFuncPtr(filename);
  }
};

这当然是另一种类型擦除的形式。


我猜 ChildHandler<Child> 需要动态分配。如果是这样的话,那么 Handler 要么需要一个虚析构函数,要么您可以使用 shared_ptr<Handler> 管理指针,它会捕获传递给其构造函数的指针类型。 unique_ptr<Handler> 不能捕获传递给构造函数的指针类型。 - Steve Jessop
你为什么要手动进行类型转换,而不是让编译器自动隐式地完成呢? - Jan Hudec
@Jan:这段代码中只有一个转换。那就是向下的 dynamic_cast,它不能隐式进行。 - Steve Jessop
@SteveJessop:不,它不是隐式的,但也不需要。模板也不需要。 - Jan Hudec
@SteveJessop:关于虚析构函数的观点是正确的。这也是一种类型擦除的形式,使用两种不同的机制确实没有意义。添加了~Handler以明确表示。 - MSalters

0
class Parent
{
public:
    // so we don't need to static-cast at the call site
    template <typename ChildT>
    void registerFileHandler(string ext, void (ChildT::*func)(string)) {
         doRegisterFileHandler(ext, static_cast<void (Parent::*func)(string)>(func));
    }
    void doRegisterFileHandler(string ext, void (Parent::*func)(string));
};

class Child : public Parent
{
    Child()
    {
        registerFileHandler("jpg", &Child::loadJpg);
        registerFileHandler("png", &Child::loadPNG);
    }
    void loadJpg(string filename);
    void loadPNG(string filename);
};

您可以像平常一样调用函数:

this->*func(string)

在父级中。

规范说明应该可以工作(C++03中的5.2.9/9,C++11中的5.2.9/12),并且MFC在消息映射中使用它。然而,Microsoft Visual C++是实际上并非在所有情况下都能正常工作的编译器。因为Microsoft提出了这种“成员指针的最小表示法”,其中成员指针的表示方式取决于类是否使用多重继承或虚拟继承。因此,如果子类使用多重继承,但父类不使用,则无法进行转换,因为具有多重继承的类的成员指针无法适应具有单一继承的类的成员指针。有一个编译器选项始终使用通用格式,但使用和不使用它编译的代码是二进制不兼容的。


我喜欢模板函数的签名。它使用户代码非常简单。我不喜欢static_cast,但决定使用std::bind是安全的选择。 - bgp2000
@bgp2000:std::bind 显然更加灵活,允许你将加载器实现为单独的对象,这在这里可能是更好的设计。当然运行时会花费一些代价,但对于每秒钟执行数百万次的紧耦合的操作而言,它可能相当重要。 - Jan Hudec

0
为了让你的代码正常工作,registerFileHandler() 应该传递一个指向实际子对象的指针来加载图像。
可能更好的方法是定义 static Child Child::loadPNG(string filename),但这取决于 Child 和 Parent 对象的实际操作。

实际上不需要。注册仅在this上完成,因此它将是this - Jan Hudec

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接