Статический полиморфизм с CRTP: использование базового класса для вызова производных методов

Одним из основных преимуществ virtual в C++ является возможность использовать базовый класс (указатель или ссылку) для вызова производных методов.

Я читаю об использовании CRTP для реализации статического полиморфизма, но не могу понять, как чтобы достичь того, что я упомянул выше, используя эту технику, потому что я не могу объявить функцию, принимающую тип Base, когда для этого требуется шаблон.

Мне кажется, что то, что описано в статье, можно было бы достичь, просто используя перегрузку функций, поэтому я уверен, что в этой технике должно быть нечто большее.

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

Вот мой минимальный код, который выдает ошибку «отсутствуют аргументы шаблона перед токеном ‘&’ void Print(Base& Object)».

#include <cstring>
#include <iostream>

template <typename Derived>
struct Base
{
    std::string ToStringInterface() { return static_cast<Derived*>(this)->ToString(); }

    std::string ToString()  {   return "This is Base.";     }
};

struct Derived : Base<Derived>
{
    std::string ToString()  {   return "This is Derived.";  }
};

void Print(Base& Object)
{
    std::cout << Object->ToStringInterface() << std::endl;
}

int main()
{
    Derived MyDerived;

    // This works, but could have been achieved with a function overload.
    std::cout << MyDerived.ToStringInterface() << std::endl;

    // This does not work.
    Print(MyDerived);
}

person MGA    schedule 10.06.2014    source источник
comment
потому что я не могу объявить функцию принимающей тип Base, когда для этого требуется шаблон. Функция Print должна быть шаблоном функции для статического полиморфизма, да, что-то вроде template<class Derived> void Print(Base<Derived>& Object);. Статический полиморфизм разрешается во время компиляции, поэтому Print должен точно знать, какую функцию вызывать во время компиляции.   -  person dyp    schedule 11.06.2014


Ответы (3)


Ну, вам нужно объявить функцию печати шаблона:

template<class T>
void Print(Base<T>& Object)
{
    std::cout << Object.ToStringInterface() << std::endl;
}
person Kiroxas    schedule 10.06.2014
comment
Именно то, что мне было нужно. Небольшой дополнительный вопрос: прав ли я, что в этом случае у меня не может быть метода по умолчанию для Base и я не могу создавать экземпляры объектов типа Base? (что можно сделать с помощью динамического полиморфизма). - person MGA; 11.06.2014
comment
Вы можете создать экземпляр типа Base<Whatever>, который на самом деле не является Whatever, но вызов ToStringInterface() приведет к неопределенному поведению. Сделайте ctor защищенным и/или проверьте правильный тип в ToStringInterface(). Обратите внимание, что проверка с помощью dynamic_cast не всегда возможна, потому что CRTP не обязательно включает какие-либо виртуальные функции, но они необходимы для такого приведения. - person Ulrich Eckhardt; 11.06.2014

Благодаря полученным комментариям и ответам я выкладываю свою реализацию, вдруг кому пригодится.

#include <cstring>
#include <iostream>

template <typename Derived>
class Base
{
public:
    std::string ToStringInterface()
    {
        return static_cast<Derived*>(this)->ToString();
    }
};

template<>
class Base<void> : public Base<Base<void> >
{
public:
    std::string ToString()
    {
        return "This is Base (default implementation).";
    }
};

class Derived : public Base<Derived>
{
public:
    std::string ToString()
    { 
        return "This is Derived.";
    }
};

template <typename T>
void Print(Base<T>& Object)
{
    std::cout << Object.ToStringInterface() << std::endl;
}

int main()
{   
    int Decision;
    std::cout << "Do you want to create an object of type Base (input 0) or Derived (input 1)? ";
    std::cin >> Decision;
    if (Decision == 0)
    {
        Base<void> MyBase;
        Print(MyBase);
    }
    else
    {
        Derived MyDerived;
        Print(MyDerived);
    }
}
person MGA    schedule 10.06.2014

Извините, но CRTP действительно так не работает. Идея обычно состоит в том, чтобы внедрить некоторый код в иерархию зависимостей способом, который очень специфичен для C++. В вашем примере у вас может быть, например. интерфейс, который требует функцию ToStringInterface() и использует CRTP для привязки его к существующей иерархии классов ToString():

class IStringable
{
    virtual string ToStringInterface() = 0;
};
class Unchangeable
{
    virtual string ToString();
};
template<class Derived>
class UnchangeableToIStringableMixin
{
    virtual string ToStringInterface()
    {
        return static_cast<Derived*>(this)->ToString();
    }
};
class StringableUnchangeable:
    public Unchangeable, UnchangeableToIStringableMixin<StringableUnchangeable>
{
};

Однако, если Unchangeable действительно можно изменить, вы бы не стали делать что-то подобное. Не забудьте рассмотреть возможность того, что CRTP просто не подходит для того, что вы делаете.

person Ulrich Eckhardt    schedule 10.06.2014
comment
Спасибо. Я делал это больше как упражнение в шаблонах и чтобы удовлетворить свое любопытство, чем что-либо еще. На практике я не понимаю, зачем мне идти со всей этой сложностью, а не просто использовать virtual. В конце концов я нашел решение, которое мне понравилось, специализировав класс Base на void (см. ниже). - person MGA; 11.06.2014
comment
@Ulrich ИМХО, вам не нужны виртуальные функции-члены, даже это является основным преимуществом CRTP, своего рода виртуализация во время компиляции, у вас будет более быстрый exec, поскольку ему не нужно просматривать виртуальную таблицу во время выполнения. . - person Jean Davy; 08.01.2015