Подтвердить что ты не робот

Вызов виртуального метода базового шаблона из производного класса вариационных шаблонов

Это, по сути, продолжение более раннего вопроса (не поставлено мной, но меня интересует ответ).

Вопрос: Почему компилятор/компоновщик не может разрешить вызов виртуальной функции из производного класса?. В этом случае производный класс является классом шаблона с переменными параметрами, которые применяются множественное наследование по отношению к одному и тому же классу шаблонов несколько раз (один раз для каждого типа в вариационных параметрах).

В конкретном примере ниже производный класс JobPlant, и он вызывается из Worker. Вызов абстрактного метода work() не связывается, вызывая ссылки workaround() и выполняет ожидаемый путь.

Это сбои ссылок, как показано 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

Следуйте этой ссылке для демонстрации обходного пути.

Job является абстрактным базовым классом, и он имеет связанные производные классы. Work - это абстрактный шаблонный класс, который выполняет задание. Worker - это шаблон, который идентифицирует Job и выполняет его (struct вместо class исключительно для уменьшения помех синтаксиса):

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 - это шаблонный шаблон, параметризованный JOBS, который находит Worker для выполнения Job. JobPlant наследует от Work для каждого типа задания в JOBS. MyJobPlant является экземпляром JobPlant и реализует виртуальные методы Work из связанных абстрактных классов Work.

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

Ответ 1

Вы явно вызываете p->Work<JOB>::work(), то есть чистый виртуальный метод в Work<JOB>. Этот метод не реализован (он чист в конце концов).

Не имеет значения, что производные классы могли реализовать/переопределить этот метод. Work<JOB>:: говорит, что вы хотите версию из этого класса, а не что-то из производного класса. Динамическая отправка не происходит.

(Work<JOB>::work() - это синтаксис, который вы бы использовали для вызова метода базового класса из переопределяющего метода в производном классе, и вам действительно нужен метод базового класса.)


При удалении явного Work<JOB>:: результата возникает ошибка неоднозначности. При попытке разрешить имя work компилятор сначала пытается определить, какой из базовых классов содержит это имя. После этого следующим шагом будет разрешение перегрузки между различными методами work в этом классе.

К сожалению, первый шаг приводит к двусмысленности: более одного базового класса содержит имя work. Затем компилятор никогда не пытается найти соответствующую перегрузку. (Они не являются перегрузками, но конфликтуют друг с другом, поскольку они из разных классов).

Обычно это можно решить, переведя имена метода базового класса в производный класс с помощью using (или, тем не менее, это технически называется тем, что делает using). Если вы добавляете объявления using для всех методов work базовых классов, компилятор находит имя work в производном классе (без двусмысленности) и может затем продолжить разрешение перегрузки (также не двусмысленное).

Вариадический шаблон усложняет ситуацию, так как я не думаю, что using Work<JOBS>::work...; является законным (и мой компилятор тоже так не думает). Но если вы создаете базовые классы "вручную", все методы работы могут быть приведены в конечный класс:

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

С помощью этой конструкции проблема p->work(dynamic_cast<const JOB &>(job)); компилируется и выполняется успешно.