Убедитесь, что класс, полученный из родительского класса CRTP, реализует функцию

Коротко:

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

Подробнее:

У меня есть код вроде этого

class Base
{
public:
    class Params
    {
    public:
        virtual ~Params() {}
    };

    virtual void myFunc( Params& p ) = 0;
};

template< typename T >
class CRTP : public Base
{
public:
    virtual void myFunc( Base::Params& p ) override
    {
        typename T::Params& typedParams = dynamic_cast<typename T::Params&>( p );
        static_cast<T*>( this )->myFunc( typeParams );
    }

};

class Imp : public CRTP<Imp>
{
public:
    class Params : public CRTP<Imp>::Params
    {
    public:
        virtual ~Params() {}

        int x, y, z;
    };

    virtual void myFunc( Imp::Params& p );
};

Предполагается, что у меня может быть несколько дочерних классов Imp, которые выполняют разные вещи в myFunc и принимают свои собственные требуемые параметры. Интерфейс, предоставляемый Base, затем используется функциями более высокого уровня, которые должны иметь только указатель/ссылку типа Base::Params и Base. Моя проблема заключается в том, что любой Imp предоставляет специализированный myFunc. Чтобы избежать бесконечной рекурсии Imp, необходимо реализовать myFunc.

Моя первая попытка заключалась в добавлении чистой виртуальной функции в CRTP

virtual void myFunc( typename T::Params& p ) = 0;

но это не работает, поскольку Imp не был полностью определен, когда CRTP определяется. Этот вопрос использует static_assert, который заставлял меня думать о том, чтобы сделать то же самое с static_assert внутри CRTP::myFunc. Кроме того, я не уверен, что должно быть выражением в статическом утверждении для нестатической функции.

  • Можно ли использовать static_assert для чего мне нужно?
  • Это лучший/самый чистый способ гарантировать, что производный класс имеет необходимую функцию?
  • Разве я увлекся своим классом, и есть лучший способ сделать что-то?

Спасибо.

Ответ 1

Почему бы просто не использовать другое имя для функции? Тогда у вас будет ошибка компиляции для каждого вывода класса CRTP без и реализации. Рассмотрим это:

class Base
{
public:
    class Params
    {
    public:
        virtual ~Params() {}
    };

    virtual void myFunc( Params& p ) = 0;
};

template< typename T >
class CRTP : public Base
{
public:
    virtual void myFunc( Base::Params& p ) final override
    {
        typename T::Params& typedParams = dynamic_cast<typename T::Params&>( p );
        static_cast<const T*>( this )->myFuncImp( typedParams );
    }

};

class Imp : public CRTP<Imp>
{
public:
    class Params : public CRTP<Imp>::Params
    {
    public:
        virtual ~Params() {}

        int x, y, z;
    };
};

int main(int argc, char** argv)
{
    Imp imp;
}

Сбой компиляции из-за отсутствия myFuncImp, предоставленного Imp.

Ответ 2

Вы можете разбить динамический полиморфизм и перейти к статическому полиморфизму:

#include <iostream>
#include <type_traits>

class Base
{
    public:
    class Params
    {
        public:
        virtual ~Params() {}
    };

    virtual ~Base() {}
    virtual void myFunc(Params& p) = 0;
};


namespace Detail {
    // Helper for the static assertion
    // Omit this if "‘void CRTP<T>::myFunc(Base::Params&) [with T = Imp]’ is private" is good enough
    struct is_MyFunc_callable_implementation
    {
        template<typename Object, typename Params>
        static decltype(std::declval<Object>().myFunc(std::declval<Params&>()), std::true_type())
        test(int);

        template<typename Object, typename Params>
        static std::false_type
        test(...);
    };

    template<typename Object, typename... A>
    using is_MyFunc_callable = decltype(is_MyFunc_callable_implementation::test<Object, A...>(0));

    // Helper function to break recursion
    template<typename Object, typename Params>
    inline void invokeMyFunc(Object& object, Params& params) {
        static_assert(is_MyFunc_callable<Object, Params>::value, "The derived class is missing 'MyFunc'");
        object.myFunc(params);
    }
} // namespace Detail

template<typename T>
class CRTP: public Base
{
    private:
    // Make this final!
    virtual void myFunc(Base::Params& p) override final
    {
        static_assert(std::is_base_of<Base, T>::value, "T must derive from CRTP");
        typename T::Params& typeParams = dynamic_cast<typename T::Params&>(p);
        Detail::invokeMyFunc(static_cast<T&>(*this), typeParams);
    }
};

class Imp: public CRTP<Imp>
{
    public:
    class Params: public CRTP<Imp>::Params
    {
        public:
        int x = 1;
        int y = 2;
        int z = 3;
    };

    // Without this function:
    // error: static assertion failed: The derived class is missing 'MyFunc'
    // error: ‘void CRTP<T>::myFunc(Base::Params&) [with T = Imp]’ is private
    #if 0
    void myFunc(Params& p) {
        std::cout << p.x << p.y << p.z << '\n';
    }
    #endif
};

int main()
{
    Imp imp;
    Base* base = &imp;
    Imp::Params params;
    base->myFunc(params);
}

Однако, мое мнение таково: дизайн базового класса является сбоем, а код выше - это просто работа.

Ответ 3

Идея использования другого имени для члена производных классов (как в ответе Рудольфа Бундулиса) хороша. Однако я бы сделал это методом protected, чтобы пользователи не пытались использовать его.

Кроме того, использование Derived::Params в базе CRTP вызывает дополнительные сложности (поскольку Derived=Imp не полностью объявлено в точке его использования в CRTP<Imp>), лучше всего Base::Params сохранить Base::Params как параметр функции.

struct Base                                          // public user interface
{
  struct Params { virtual ~Params() {} };
  virtual void myFunc( Params& ) = 0;
};

namespace details {                                  // deter clients from direct access
  template< typename Derived >
  struct CRTP : Base
  {
    virtual void myFunc( Params& p ) final           // cannot be overridden
    {
      static_cast<Derived*>( this )->myFuncImp(p);
    }
  };

  class Imp : public CRTP<Imp>
  {
    struct Params : CRTP<Imp>::Params { int x, y, z; };
    void myFuncImpDetails( Params* );
  protected:                                         // protected from clients
    void myFuncImp( Base::Params& p )
    {
      auto pars=dynamic_cast<Params*>(&p);
      if(pars)
        myFuncImpDetails(pars);
      else
        throw std::runtime_error("invalid parameter type provided to Imp::myFunc()");
    }
  };
} // namespace details