Возможна ли эмуляция чистой виртуальной функции в статическом полиморфизме с использованием CRTP?
Я пытаюсь реализовать полиморфизм во время компиляции, используя CRTP, и хочу заставить производный класс реализовать функцию.
Текущая реализация такова.
template <class Derived>
struct base {
void f() {
static_cast<Derived*>(this)->f();
}
};
struct derived : base<derived>
{
void f() {
...
}
};
В этой реализации вызов функции попадает в бесконечный цикл, если производный класс не реализует f()
,
Как заставить производный класс реализовать функцию, подобную чисто виртуальной функции? Я пытался использовать "static_assert", как static_assert(&base::f != &Derived::f, "...")
но он генерирует сообщение об ошибке, в котором говорится, что два указателя на функцию-член, указывающие на функции-члены разных классов, несопоставимы.
2 ответа
Вы можете присвоить перезаписываемой вещи разные имена, например:
template <class Derived>
struct base {
void f() {
static_cast<Derived*>(this)->fimpl();
}
void fimpl() = delete;
};
struct derived : base<derived> {
void fimpl() { printf("hello world\n"); }
};
Вот, fimpl = delete
в базе, так что он не может быть вызван случайно, если fimpl
переопределяется в производном классе.
Вы также можете вставить промежуточный скрывающий слой в свой CRTP, чтобы "временно" пометить f
как delete
:
template <class Derived>
struct base {
void f() {
static_cast<Derived*>(this)->f();
}
};
template <class Derived>
struct intermediate : base<Derived> {
void f() = delete;
};
struct derived : intermediate<derived> {
void f() { printf("hello world\n"); }
};
template<typename Derived>
class Base
{
private:
static void verify(void (Derived::*)()) {}
public:
void f()
{
verify(&Derived::f);
static_cast<Derived*>(this)->f();
}
};
Если производный класс не реализует f
сам по себе, тип &Derived::f
было бы void (Base::*)()
, который нарушает компиляцию.
Начиная с C++11 мы также можем сделать эту функцию обобщенной с помощью шаблона с переменными числами.
template<typename Derived>
class Base
{
private:
template<typename T, typename...Args>
static void verify(T (Derived::*)(Args...)) {}
};
Это вопрос, который задавали много лет назад, но я столкнулся с этим недавно, поэтому я просто отправлю его здесь, надеюсь, что это может помочь некоторым людям.
С помощью auto
поскольку тип возврата может быть другим решением. Рассмотрим следующий код:
template<typename Derived>
class Base
{
public:
auto f()
{
static_cast<Derived*>(this)->f();
}
};
Если производный класс не предоставляет допустимую перегрузку, эта функция становится рекурсивной, и поскольку auto
требуется окончательный тип возвращаемого значения, он никогда не может быть выведен, поэтому будет гарантирована ошибка компиляции. Например, на MSVC это примерно так:
a function that returns 'auto' cannot be used before it is defined
Это заставляет производный класс обеспечивать реализацию, как чистую виртуальную функцию.
Хорошо то, что дополнительный код не требуется, и если производный класс также использует auto
как возвращаемый тип, эта цепочка может длиться столько, сколько требуется. В некоторых случаях это может быть удобно и гибко, как вBase
а также LevelTwo
в следующем коде, который может возвращать разные типы при вызове одного и того же интерфейса f
. Однако эта цепочка полностью отключает прямое наследование реализации от базового класса, как вLevelThree
:
template<typename Derived = void>
class Base
{
public:
Base() = default;
~Base() = default;
// interface
auto f()
{
return fImpl();
}
protected:
// implementation chain
auto fImpl()
{
if constexpr (std::is_same_v<Derived, void>)
{
return int(1);
}
else
{
static_cast<Derived*>(this)->fImpl();
}
}
};
template<typename Derived = void>
class LevelTwo : public Base<LevelTwo>
{
public:
LevelTwo() = default;
~LevelTwo() = default;
// inherit interface
using Base<LevelTwo>::f;
protected:
// provide overload
auto fImpl()
{
if constexpr (std::is_same_v<Derived, void>)
{
return float(2);
}
else
{
static_cast<Derived*>(this)->fImpl();
}
}
friend Base;
};
template<typename Derived = void>
class LevelThree : public LevelTwo<LevelThree>
{
public:
LevelThree() = default;
~LevelThree() = default;
using LevelTwo<LevelThree>::f;
protected:
// doesn't provide new implementation, compilation error here
using LevelTwo<LevelThree>::fImpl;
friend LevelTwo;
};
В моем случае производный класс, над которым я работаю, также является производным от другого класса, который предоставляет дополнительную информацию, необходимую для определения того, останавливаться ли на текущем классе или переходить к производному классу. Но в других случаях можно либо разорвать цепочку, используя фактические типы вместо "авто", либо использовать другие уловки. Но в таких ситуациях, что может быть виртуальной функцией является лучшим выбрал.