Нереализованная производная функция в CRTP

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

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

#include <Windows.h>
#include <iostream>

struct AbstractBackend
{
  virtual ~AbstractBackend() = 0;

  virtual void foo()
  {
    throw "implementation missing: failed to override in derived class";
  }

  virtual void bar()
  {
    throw "implementation missing: failed to override in derived class";
  }
};

AbstractBackend::~AbstractBackend() {}

struct ConcreteBackendA : AbstractBackend
{
  int backendResource;

  ConcreteBackendA(int rsc) :
    backendResource(rsc)
  {}

  virtual void foo()
  {
    printf("executing ConcreteBackendA::foo!\n");
  }

  // ConcreteBackendA does not support "bar" feature
};

struct ConcreteBackendB : AbstractBackend
{
  HDC backendResource;

  ConcreteBackendB(HDC hdc) :
    backendResource(hdc)
  {}

  virtual void foo()
  {
    printf("executing ConcreteBackendB::foo!\n");
  }

  virtual void bar()
  {
    printf("executing ConcreteBackendB::bar!\n");
  }

};

struct FrontEnd
{
  AbstractBackend *backend;

  FrontEnd(int rsc) :
    backend(new ConcreteBackendA(rsc))
  {}

  FrontEnd(HDC hdc) :
    backend(new ConcreteBackendB(hdc))
  {}

  ~FrontEnd()
  {
    delete backend;
  }

  void foo()
  {
    backend->foo();
  }

  void bar()
  {
    backend->bar();
  }
};

int main()
{
  int rsc = 0;
  HDC hdc = 0;
  FrontEnd A(rsc);
  FrontEnd B(hdc);

  A.foo();
  A.bar(); // throws an error, A::bar is not a feature of this engine

  B.foo();
  B.bar();

  std::cin.get();
}

В этом примере AbstractBackend поддерживает две функции: foo и bar. ConcreteBackendA поддерживает только foo, bar — это функция, которую он не поддерживает (может быть, что-то вроде Draw3dText), но это нормально. Пользователь может поймать исключения и двигаться дальше. Одним небольшим недостатком является использование виртуальных функций. Я хотел бы развлечь мысль об использовании CRTP следующим образом:

#include <Windows.h>
#include <iostream>

template <class Derived>
struct AbstractBackend
{
  virtual ~AbstractBackend() = 0;

  void foo()
  {
    static_cast<Derived*>(this)->foo();
  }

  void bar()
  {
    static_cast<Derived*>(this)->bar();
  }
};

template <class Derived>
AbstractBackend<Derived>::~AbstractBackend() {}

struct ConcreteBackendA : AbstractBackend<ConcreteBackendA>
{
  int backendResource;

  ConcreteBackendA(int rsc) :
    backendResource(rsc)
  {}

  void foo()
  {
    printf("executing ConcreteBackendA::foo!\n");
  }

  // ConcreteBackendA does not support "bar" feature
};

struct ConcreteBackendB : AbstractBackend<ConcreteBackendB>
{
  HDC backendResource;

  ConcreteBackendB(HDC hdc) :
    backendResource(hdc)
  {}

  void foo()
  {
    printf("executing ConcreteBackendB::foo!\n");
  }

  void bar()
  {
    printf("executing ConcreteBackendB::bar!\n");
  }
};

template <class ConcreteBackend>
struct FrontEnd
{
  AbstractBackend<ConcreteBackend> *backend;

  FrontEnd(int rsc) :
    backend(new ConcreteBackendA(rsc))
  {}

  FrontEnd(HDC hdc) :
    backend(new ConcreteBackendB(hdc))
  {}

  ~FrontEnd()
  {
    delete backend;
  }

  void foo()
  {
    backend->foo();
  }

  void bar()
  {
    backend->bar();
  }
};

int main()
{
  int rsc = 0;
  HDC hdc = 0;
  FrontEnd<ConcreteBackendA> A(rsc);
  FrontEnd<ConcreteBackendB> B(hdc);

  A.foo();
  A.bar(); // no implementation: stack overflow

  B.foo();
  B.bar();

  std::cin.get();
}

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

Как я могу воспроизвести поведение виртуальной абстрактной реализации с помощью CRTP?


person Michael Choi    schedule 01.02.2019    source источник
comment
Пользователь может перехватывать исключения и двигаться дальше. Но это на самом деле не должно. Вызов нереализованной функции — это либо нормально, либо логическая ошибка программирования (т. е. вызывающая сторона не проверила, реализована ли она). А если это логическая ошибка, то в вашей программе есть ошибка. Продолжать дальше не очень разумно в этот момент.   -  person Nicol Bolas    schedule 01.02.2019
comment
@NicolBolas Я вижу, это может быть ошибкой в ​​моем подходе к исключениям. Я подумал, что если бы было, скажем, 2 функции, drawtext и draw3dtext, пользователь мог бы попробовать draw3dtext, и если это не удастся, то он просто нарисует текст.   -  person Michael Choi    schedule 01.02.2019
comment
@YSC извините за то, что я пытался минимизировать код, насколько это было возможно, во время компиляции.   -  person Michael Choi    schedule 01.02.2019
comment
@MichaelChoi: Исключения не известны особенно высокой производительностью. Использование их в середине того, что, вероятно, является вашим циклом рендеринга, является особенно плохой идеей.   -  person Nicol Bolas    schedule 01.02.2019
comment
@NicolBolas Как насчет возврата логического значения, а затем его обработки? так что насчет чего-то вроде: if(!frontEnd.Draw3dText(params)) frontEnd.DrawText(params);   -  person Michael Choi    schedule 01.02.2019
comment
Или вы можете просто сделать if(frontEnd.CanDo3DText()) frontEnd.Draw3dText(params). Я не знаю, почему вы настаиваете на объединении попытки сделать что-то с выполнением самого дела.   -  person Nicol Bolas    schedule 01.02.2019
comment
Давайте продолжим это обсуждение в чате.   -  person Michael Choi    schedule 01.02.2019


Ответы (2)


template <class Derived>
struct AbstractBackend
{
  virtual ~AbstractBackend() = 0;

  void foo()
  {
    static_cast<Derived*>(this)->foo_impl();
  }

  void bar()
  {
    static_cast<Derived*>(this)->bar_impl();
  }

  void foo_impl()
  {
    throw "implementation missing: failed to override in derived class";
  }

  void bar_impl()
  {
    throw "implementation missing: failed to override in derived class";
  }

};

теперь у вас может быть реализация по умолчанию foo/bar.

Производные классы переопределяют foo_impl вместо foo.

Однако это конкретное использование — плохой план; во время компиляции вы знаете, реализован ли данный AbstractBackend<D> или нет.

В конце концов, мы реализуем «динамический» dipatch во время компиляции; почему бы не оценить ошибку во время компиляции?

  void foo_impl() = delete;
  void bar_impl() = delete;

теперь, когда отправка выполняется в вашем коде во время компиляции, вы получаете сообщение об ошибке вместо того, чтобы ждать до момента компиляции.

person Yakk - Adam Nevraumont    schedule 01.02.2019
comment
Спасибо, я решил не использовать это решение, пока не пойму, почему мой дизайн может быть плохим, но оно отлично отвечает на мой вопрос. В любом случае, мне нравится идея удаления функции. Ошибка времени компиляции гораздо полезнее, чем ошибка времени выполнения. - person Michael Choi; 02.02.2019

Вы злоупотребляете объектно-ориентированным программированием.

Семантически AbstractBackend — это интерфейс: контракт. Если класс Alice наследуется от AbstractBackend, то Alice является AbstractBackend. Не частично AbstractBackend. Полностью AbstractBackend. Это принцип подстановки Лисков (буква L в SOLID).

Если классы Bob и Charlie частично реализуют AbstractBackend, это означает, что у вас действительно есть два контракта: Interface1 и Interface2:

  • Bob реализует (наследует) Interface1,
  • Charlie реализует (наследует) Interface2,
  • Alice реализует (наследует) Interface1 и Interface2.

CRTP снова можно использовать, ваш код пахнет приятно и свежо, жизнь приятна. Хороших выходных.

person YSC    schedule 01.02.2019
comment
Хм. Я мог бы получить много критики за это, но мой мыслительный процесс состоял в том, чтобы имитировать интерфейсы Java по умолчанию. Я понимаю, что введение методов по умолчанию было сделано из прагматических, а не философских соображений; тем не менее, я считаю, что мой вариант использования тоже здесь, поэтому я хотел бы разрешить частичную реализацию. Что вы думаете об этом? - person Michael Choi; 01.02.2019
comment
В Java нет множественного наследования. Я понимаю, что вы можете быть хорошо знакомы с Java и пытаться чувствовать себя как дома в C++, делая это, но это не вариант. C++ на самом деле не Java. - person YSC; 01.02.2019
comment
Откуда взялось множественное наследование? Я просто имею в виду, что если есть абстрактный класс (интерфейс Java), я бы хотел предоставить функции по умолчанию (методы защитника Java). Чтобы уточнить, конкретный класс будет наследоваться от абстрактного класса, и если конкретный класс не сможет реализовать все функции, он вернется к своей реализации по умолчанию. Это то, что я хотел бы сделать с CRTP вместо виртуальных функций. - person Michael Choi; 01.02.2019
comment
Возможно, вам нужны обратные вызовы. В любом случае, ваше использование ООП в C++ ошибочно. Наследование — это самое сильное из отношений между двумя классами, не злоупотребляйте им, иначе оно вас укусит. - person YSC; 01.02.2019
comment
Хорошо, я ценю предупреждение. Я поговорю со своим старшим разработчиком, чтобы, возможно, вместо этого предложить несколько лучших идей, поскольку кажется, что моя реализация абстрактного движка рендеринга может быть ошибочной. В любом случае, я все же хотел бы знать ответ на исходный вопрос, может ли CRTP поддерживать операции по умолчанию, не сталкиваясь с переполнением стека. - person Michael Choi; 01.02.2019
comment
Привет @Yakk. Я никогда этого не говорил, я люблю CRTP! - person YSC; 02.02.2019