jxh
jxh

Reputation: 70502

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);
}

Upvotes: 13

Views: 1874

Answers (1)

sth
sth

Reputation: 229904

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.

Upvotes: 9

Related Questions