Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Calling virtual method of base template from derived variadic template class

This is essentially a follow-up to an earlier question (not posed by me, but I am interested in an answer).

The question is: Why does the compiler/linker fail to resolve the call to the virtual function from the derived class? In this case, the derived class is a template class with variadic parameters that applies multiple inheritance against the same template class multiple times (once for each type in the variadic parameters).

In the concrete example below, the derived class is JobPlant, and it is being called from Worker. Invoking the abstract work() method fails to link, while invoking workaround() links and executes in the expected way.

These are the link failures as shown by ideone:

/home/g6xLmI/ccpFAanK.o: In function `main':
prog.cpp:(.text.startup+0x8e): undefined reference to `Work<JobA>::work(JobA const&)'
prog.cpp:(.text.startup+0xc9): undefined reference to `Work<JobB>::work(JobB const&)'
prog.cpp:(.text.startup+0xff): undefined reference to `Work<JobC>::work(JobC const&)'
collect2: error: ld returned 1 exit status

Follow this link for demonstration of the workaround working.

Job is an abstract base class, and it has associated derived classes. Work is an abstract template class that performs a job. Worker is a template that identifies the JOB and performs it (struct instead of class purely to reduce syntax clutter):

struct Job { virtual ~Job() {} };

struct JobA : Job {};
struct JobB : Job {};
struct JobC : Job {};

template <typename JOB>
struct Work {
    virtual ~Work() {}
    virtual void work(const JOB &) = 0;
    void workaround(const Job &job) { work(dynamic_cast<const JOB &>(job)); }
};

template <typename PLANT, typename... JOBS> struct Worker;

template <typename PLANT, typename JOB, typename... JOBS>
struct Worker<PLANT, JOB, JOBS...> {
    bool operator()(PLANT *p, const Job &job) const {
        if (Worker<PLANT, JOB>()(p, job)) return true;
        return Worker<PLANT, JOBS...>()(p, job);
    }
};

template <typename PLANT, typename JOB>
struct Worker<PLANT, JOB> {
    bool operator()(PLANT *p, const Job &job) const {
        if (dynamic_cast<const JOB *>(&job)) {
            p->Work<JOB>::work(dynamic_cast<const JOB &>(job));
            //p->Work<JOB>::workaround(job);
            return true;
        }
        return false;
    }
};

A JobPlant is a template class parameterized by JOBS, that finds a Worker to perform a job. The JobPlant inherits from Work for each job type in JOBS. MyJobPlant is an instance of JobPlant, and implements the virtual work methods from the associated Work abstract classes.

template <typename... JOBS>
struct JobPlant : Work<JOBS>... {
    typedef Worker<JobPlant, JOBS...> WORKER;
    bool worker(const Job &job) { return WORKER()(this, job); }
};

struct MyJobPlant : JobPlant<JobA, JobB, JobC> {
    void work(const JobA &) { std::cout << "Job A." << std::endl; }
    void work(const JobB &) { std::cout << "Job B." << std::endl; }
    void work(const JobC &) { std::cout << "Job C." << std::endl; }
};

int main() {
    JobB j;
    MyJobPlant().worker(j);
}
like image 364
jxh Avatar asked Oct 15 '14 03:10

jxh


People also ask

Can a virtual function be re implemented in a derived class?

Derived classes do not have to implement all virtual functions themselves. They only need to implement the pure ones. That means the Derived class in the question is correct.

Can a class member function template be virtual?

No, template member functions cannot be virtual.

What functions Cannot be virtual?

A virtual function cannot be global or static because, by definition, a virtual function is a member function of a base class and relies on a specific object to determine which implementation of the function is called. You can declare a virtual function to be a friend of another class.


1 Answers

You explicitly call p->Work<JOB>::work(), that is, the pure virtual method in Work<JOB>. This method is not implemented (it's pure after all).

It doesn't matter that derived classes might have implemented/overridden that method. The Work<JOB>:: says that you want the version from that class, not something from a derived class. No dynamic dispatch happens.

(Work<JOB>::work() is the syntax you would use to call a base class method from a overriding method in a derived class, and there you really want the base class method.)


When you remove then explicit Work<JOB>::, the result is an ambiguity error. When trying to resolve the name work, the compiler first tries to determine which of the base classes contains that name. After that, the next step then would be overload resolution amongst the different work methods in that class.

Unfortunately the first step results in ambiguity: more than one base class contains the name work. The compiler then never tries to figure out the matching overload. (They are not really overloads but conflicting with each other, since they are from different classes).

Usually this can be solved by bringing the base class method names into the derived class with using (or however it's technically called what using does). If you add using declarations for all the work methods of the base classes, the compiler finds the name work in the derived class (no ambiguity) and can then proceed with overload resolution (also not ambigious).

The variadic template complicates things since I don't think using Work<JOBS>::work...; is legal (and my compiler doesn't think so either). But if you compose the base classes "manually", all the work methods can be brought into the final class:

template <typename JOB, typename... JOBS>
struct CollectWork : Work<JOB>, CollectWork<JOBS...> {
   using Work<JOB>::work;
   using CollectWork<JOBS...>::work;
};

template <typename JOB>
struct CollectWork<JOB> : Work<JOB> {
   using Work<JOB>::work;
};

template<typename... JOBS>
struct JobPlant : CollectWork<JOBS...> {                                           
   using CollectWork<JOBS...>::work;
   typedef Worker<JobPlant, JOBS...> WORKER;
   bool worker(const Job &job) { return WORKER()(this, job); }
};

With this construct, the problematic p->work(dynamic_cast<const JOB &>(job)); compiles and runs successfully.

like image 66
sth Avatar answered Oct 03 '22 15:10

sth