Убедитесь, что класс, полученный из родительского класса 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