简体   繁体   中英

method pointer and inheritance // kind of strategy pattern (C++)

In my design, there is a class which reads information from file. The read info represents a job (for simplicity, it's an integer, which is "job id"). The file reader class can accept objects which can handle such a job. Now my idea was, to make an Interface, eg "IJobHandler" which has a pure virtual function "DoJob()" and then you can call something like

FileReader fr;
Class1 c1; // has a base class IAcceptor with virtual method HandleJobId()
Class2 c2; // has a base class IAcceptor with virtual method HandleJobId()
fr.Register(c1);
fr.Register(c2);
fr.doJob(1); // calls c1.HandleJobId()
fr.doJob(2); // class c2.HandleJobId()

This would work fine. But what happens, if some class can handle two or more job ids? But there is only one method which this class can implement (HandleJobId()). Wouldn't the following be nice: fr.Register(c1, c1::Handle_1()) or something like that?

Maybe my intention is not very clear right now. But you will se it on the bigger code example below. Sorry for the big code block, but I don't know how to explain it that exactly...

class IAcceptable
{
public:
    // interface; implementors should return map of job-ids (int)
    // and a kind of pointer to a method which should be called to
    // handle the job.
    virtual std::map<int, SOME_KIND_OF_FUNCTION_POINTER> GetJobIds() const = 0;
};

class Class12 : public IAcceptable
{
public:
    void Handle_1(){} // method to handle job id 1
    void Handle_2(){} // method to handle job id 2

    virtual std::map<int, SOME_KIND_OF_FUNCTION_POINTER> GetJobIds() const
    {
        std::map<int, SOME_KIND_OF_FUNCTION_POINTER> intToMethodMap;
        // return map, which says: "I can handle job id 1, by calling Handle_1(), so I give you c12 pointer to this method"
        // (same thing for job id 2 and Handle_2())
        intToMethodMap.insert(std::pair<int, SOME_KIND_OF_FUNCTION_POINTER>(1, POINTER_TO_Handle_1);
        intToMethodMap.insert(std::pair<int, SOME_KIND_OF_FUNCTION_POINTER>(2, POINTER_TO_Handle_2);
        return intToMethodMap;
    }
};

class Class34 : public IAcceptable
{
    void Handle_3(){} // method to handle job id 3
    void Handle_4(){} // method to handle job id 4
    virtual std::map<int, SOME_KIND_OF_FUNCTION_POINTER> GetJobIds() const
    {
        std::map<int, SOME_KIND_OF_FUNCTION_POINTER> intToMethodMap;
        // return map, which says: "I can handle job id 3, by calling Handle_3(), so I give you c12 pointer to this method"
        // (same thing for job id 4 and Handle_4())
        intToMethodMap.insert(std::pair<int, SOME_KIND_OF_FUNCTION_POINTER>(3, POINTER_TO_Handle_3);
        intToMethodMap.insert(std::pair<int, SOME_KIND_OF_FUNCTION_POINTER>(4, POINTER_TO_Handle_4);
        return intToMethodMap;
    }
};

class FileReader
{
public:
    // register an IAcceptable
    // and add its handlers to the local list
    void Register(const IAcceptable& acc)
    {
        m_handlers.insert(acc.GetJobIds());
    }

    // if some job is to do, search for the job id and call 
    // the found function
    void doSomeJob(int i)
    {
        std::map<int, SOMEFUNCTION>::iterator specificHandler = m_handlers.find(i);
        // call here (specificHandler->second)()
    }
private:
    std::map<int, SOMEFUNCTION> m_handlers;
};


int main()
{
    Class12 c12;   // can handle job id 1 and 2
    Class34 c34;   // can handle job id 3 and 4

    FileReader fr;
    fr.Register(c12);
    fr.Register(c34);

    fr.doSomeJob(1);  // should lead to this call: c12->Handle_1()
    fr.doSomeJob(2);  // c12->Handle_2();
    fr.doSomeJob(3);  // c34->Handle_3();
    fr.doSomeJob(4);  // c34->Handle_4();
}

Well, maybe the design is my problem and someone can give me a hint how to make it better :)

Here's a complete example:

class IAcceptable; 

class DelegateBase
{
public: 
    virtual void Call() = 0; 
};

template <class Class> class Delegate: public DelegateBase
{
public: 
    typedef void (Class::*Function)(); 
    Delegate(Class* object, Function f): func(f) {}
    virtual void Call() { (object->*func)(); }

private: 
    Class* object; 
    Function func; 
}; 



class IAcceptable
{
public:
    // interface; implementors should return map of job-ids (int)
    // and a kind of pointer to a method which should be called to
    // handle the job.
    virtual std::map<int, DelegateBase*> GetJobIds() = 0;
};

class Class12 : public IAcceptable
{
public:
    void Handle_1(){} // method to handle job id 1
    void Handle_2(){} // method to handle job id 2

    virtual std::map<int, DelegateBase*> GetJobIds()
    {
        std::map<int, DelegateBase*> intToMethodMap;
        // return map, which says: "I can handle job id 1, by calling Handle_1(), so I give you c12 pointer to this method"
        // (same thing for job id 2 and Handle_2())
        intToMethodMap.insert(std::pair<int, DelegateBase*>(1, new Delegate<Class12>(this, &Class12::Handle_1)));
        intToMethodMap.insert(std::pair<int, DelegateBase*>(2, new Delegate<Class12>(this, &Class12::Handle_2)));
        return intToMethodMap;
    }
};

class Class34 : public IAcceptable
{
    void Handle_3(){} // method to handle job id 3
    void Handle_4(){} // method to handle job id 4
    virtual std::map<int, DelegateBase*> GetJobIds()
    {
        std::map<int, DelegateBase*> intToMethodMap;
        // return map, which says: "I can handle job id 3, by calling Handle_3(), so I give you c12 pointer to this method"
        // (same thing for job id 4 and Handle_4())
        intToMethodMap.insert(std::pair<int, DelegateBase*>(3, new Delegate<Class34>(this, &Class34::Handle_3)));
        intToMethodMap.insert(std::pair<int, DelegateBase*>(4, new Delegate<Class34>(this, &Class34::Handle_4)));
        return intToMethodMap;
    }
};

class FileReader
{
public:
    // register an IAcceptable
    // and add its handlers to the local list
    void Register(IAcceptable& acc)
    {
        std::map<int, DelegateBase*> jobIds = acc.GetJobIds(); 
        m_handlers.insert(jobIds.begin(), jobIds.end());
    }

    // if some job is to do, search for the job id and call 
    // the found function
    void doSomeJob(int i)
    {
        std::map<int, DelegateBase*>::iterator specificHandler = m_handlers.find(i);
        specificHandler->second->Call(); 
    }
private:
    std::map<int, DelegateBase*> m_handlers;
};


int _tmain(int argc, _TCHAR* argv[])
{
    Class12 c12;   // can handle job id 1 and 2
    Class34 c34;   // can handle job id 3 and 4

    FileReader fr;
    fr.Register(c12);
    fr.Register(c34);

    fr.doSomeJob(1);  // should lead to this call: c12->Handle_1()
    fr.doSomeJob(2);  // c12->Handle_2();
    fr.doSomeJob(3);  // c34->Handle_3();
    fr.doSomeJob(4);  // c34->Handle_4();

    return 0;
}
  1. To call a member function we need an object; so your maps should contain not simply method pointers, but something that can encapsulate a complete call: an object + a method pointer. That something is Delegate here.

  2. To make sure that the method is called correctly even if it's defined in a subclass, we need to store both the derived object and the method pointer type-correctly (no casting). So we make Delegate a template, with the derived class as its parameter.

  3. This means that delegates based on methods of different subclasses are incompatible, and cannot be put into a map. To work around this we introduce a common base class, DelegateBase, and the virtual function Call(). Call() can be called without knowing the exact type of stored object / method, and it will be dispatched to a type-correct implementation. Now we can store DelegateBase* pointers in the map.

Also check out boost::function and boost::bind, they provide a generalization for the above, and I think they could also be used to your purposes.

There are several solutions to this sort of problem.

If you have a class which can handle several different jobs, in separate functions, the simplest solution is to wrap it, several types, eg:

class JobsOneAndTwo
{
public:
    void doJobOne();
    void doJobTwo();
};

class JobOne : public AbstractJob, JobsOneAndTwo
{
public:
    virtual void doJob() { doJobOne(); }
};

class JobTwo : public AbstractJob, JobOneAndTwo
{
public:
    virtual void doJob() { doJobTwo(); }
};

If this occurs often in the set of jobs, you can create a template (over two or moer member function pointers) to generate the individual wrapper functions.

Alternatively, you can dispatch on a data member of the class:

class JobOneAndTwo : public AbstractJob
{
    int myJob;
public:
    JobOneAndTwo(int id) : myJob( id ) {}
    void JobOne();
    void JobTwo();
    virtual void doJob()
    {
        switch ( myJob ) {
        case 1:
            JobOne();
            break;

        case 2:
            JobTwo();
            break;
    }
};

In this case, you instantiate the class twice, each time passing a different argument to the constructor.

In most of the cases I've seen, when one class can handle two jobs, it's because the two jobs differ only in some parameters; this is really just a variant on the second solution above, except that you don't switch to call different member functions, you simply use the parameters (passed into the constructor) in the basic function.

More generally, don't forget that your concrete job classes can have data, and their behavior can be modified by such data. And that you can register multiple instances of a single class, with different data.

typedef void (IAccaptable::*SOME_KIND_OF_FUNCTION_POINTER)(); 
...
Register(1, (SOME_KIND_OF_FUNCTION_POINTER)(&Class12::Handle1)); 

Warning: this C-style cast will only work with single inheritance. (Well, actually the cast would compile just fine with multiple inheritance too, but when calling (derivedObject->*funcPtr)() with a funcPtr that points at a member function of a non-first base class, then it would be called without the derivedObject pointer having been properly adjusted to point at the proper subobject belonging to that base, most probably resulting in a crash.)

A better, but more complicated solution would be to register small caller objects instead of member function pointers. When calling the handler functions, these caller objects could appropriately cast the target object.

class CallerBase
{
public: 
    virtual void Call(Base* object) = 0; 
}; 

template <class Derived>
struct Caller: public CallerBase
{
public: 
    typedef void (Derived::*Function)(); 
    Caller(Function f): func(f) {}
    virtual void Call(Base* object) 
    {
        Derived* derived = static_cast<Derived*>(object); 
        (derived->*func)(); 
    }

private: 
    Function func; 
}; 

Register(1, new Caller<Derived>(&Derived::F)); 

Then your map would contain CallerBase* pointers, and once you find the proper caller, you'd do caller->Call(object) . If object in this call is a Derived*, then it will be implicitly cast to Base*, but the virtual Caller<Derived>::Call() function will cast it back to Derived* before actually calling the method.

So you say that you have many handlers, each of which can handle an arbitrary number of job IDs, and you want to register an arbitrary number of handlers and let all of them which apply handle a given job.

To that end, let every handler implement this interface:

struct Handler
{
  virtual bool canHandle(job_id_t id) const = 0;
  virtual void doJob(job_it_t id) = 0;
};

To register a handler, simply store a pointer in a container:

std::vector<Handler*> handlers;

Then, if you need to do a job, iterate the container and dispatch:

handleJob(job_it_t id)
{
  for (std::vector<Handler*>::iterator it = handlers.begin(), end = handlers.end(); it != end; ++it)
  {
    if ((*it)->canHandle(id))
      (*it)->doJob(id);
  }
}

Method pointers can be a lot of fun.

I don't want to self promote myself but check out my guide on them I wrote back in school.

http://nicolong.com/code-examples/menu-object-tutorial

Might help a little.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM