Как реализовать проверку во время компиляции, что понижение допустимо в CRTP?

У меня есть старый добрый CRPT (прошу не отвлекаться на ограничения доступа - вопрос не в них):

 template<class Derived>
 class Base {
     void MethodToOverride()
     {
        // generic stuff here
     }
     void ProblematicMethod()
     {
         static_cast<Derived*>(this)->MethodToOverride();
     } 
 };

который, как обычно, предназначен для использования следующим образом:

 class ConcreteDerived : public Base<ConcreteDerived> {
     void MethodToOverride()
     {
        //custom stuff here, then maybe
        Base::MethodToOverride();
     }
 };

Теперь меня беспокоит static_cast. Мне нужно приведение вниз (а не приведение вверх), поэтому я должен использовать явное приведение. Во всех разумных случаях приведение будет действительным, поскольку текущий объект действительно принадлежит производному классу.

Но что, если я каким-то образом изменю иерархию, и приведение теперь станет недействительным?

Могу ли я каким-то образом обеспечить проверку во время компиляции, что в этом случае допустимо явное понижение?


person sharptooth    schedule 06.05.2011    source источник
comment
Вам не нужно иметь MethodToOverride в базовом классе.   -  person ysdx    schedule 06.05.2011
comment
@ysdx: мне нужно, если я хочу, чтобы его можно было переопределить или иметь какую-то общую реализацию, и я этого хочу.   -  person sharptooth    schedule 06.05.2011
comment
Но если у вас есть функция в базовом классе, вызов всегда будет работать, так как есть есть функция для вызова.   -  person Bo Persson    schedule 06.05.2011
comment
@Bo Persson: Да, именно поэтому мне нужно приведение вниз, чтобы вызвать наиболее производную функцию, и эта наиболее производная функция может вызвать базовую версию, если захочет.   -  person sharptooth    schedule 06.05.2011
comment
И вы не рассматриваете возможность использования для этого виртуальной функции? :-)   -  person Bo Persson    schedule 06.05.2011
comment
@Bo Persson: Ну, виртуальные функции великолепны, за исключением того, что они вносят заметные накладные расходы. Здесь, если базовая версия ничего не делает и не переопределена, компилятор увидит, что вызов полностью исключен. Я считаю, что это одна из причин, по которой CRTP используется в первую очередь.   -  person sharptooth    schedule 06.05.2011
comment
@sharptooth: Даже если Base является производным от некоторого класса, который передается как Derived, перед компиляцией ему также потребуется функция с именем MethodToOverride() .... Все это звучит невероятно маловероятно. В любом случае, вы можете проверить boost.org/doc/libs/1_44_0/libs/type_traits/doc/html/   -  person Tony Delroy    schedule 06.05.2011
comment
@Sharptooth, можете ли вы привести небольшой пример того, в каких случаях вы ожидаете, что приведение станет недействительным? Я не могу понять эту часть вопроса.   -  person iammilind    schedule 06.05.2011
comment
@iammilind: Например, я мог случайно передать какой-то несвязанный класс в качестве параметра шаблона.   -  person sharptooth    schedule 06.05.2011
comment
@sharptooth, ты имел в виду class Concrete : public Base<Other> ? Если это так, то выдает ошибку. Или может быть еще я не ясно. :)   -  person iammilind    schedule 06.05.2011
comment
возможный дубликат Как избежать ошибок при использовании CRTP?   -  person krlmlr    schedule 16.02.2015


Ответы (5)


Во время компиляции вы можете проверять только статические типы, и это уже делает static_cast.

При наличии Base* во время выполнения может быть известен только его динамический тип, то есть указывает ли он на самом деле на ConcreteDerived или на что-то другое. Поэтому, если вы хотите проверить это, это нужно сделать во время выполнения (например, с помощью dynamic_cast)

person jalf    schedule 06.05.2011
comment
Но для использования dynamic_cast вам потребуются некоторые виртуальные функции, которых, как мне кажется, эта конструкция пытается избежать. - person Bo Persson; 06.05.2011
comment
истинный. Я просто хочу сказать, что эта проверка может выполняться только во время выполнения, что, как вы указываете, несет связанные с этим затраты. - person jalf; 06.05.2011

Для дополнительной безопасности вы можете добавить защищенный конструктор в Base, чтобы убедиться, что что-то является производным от него. Тогда единственная проблема будет для действительно глупых:

class ConcreteDerived : public Base<SomeOtherClass>

но это должно быть уловлено первым обзором кода или тестовым примером.

person Bo Persson    schedule 06.05.2011

Чтобы расширить то, что сказал @Bo Persson, вы можете выполнить проверку времени компиляции в указанном конструкторе, используя, например, Boost.TypeTraits или C++0x/11 <type_traits>:

#include <type_traits>

template<class Derived>
struct Base{
  typedef Base<Derived> MyType;

  Base(){
    typedef char ERROR_You_screwed_up[ std::is_base_of<MyType,Derived>::value ? 1 : -1 ];
  }
};

class ConcreteDerived : public Base<int>{
};

int main(){
  ConcreteDerived cd;
}

Полный пример на Ideone.

person Xeo    schedule 06.05.2011
comment
Не могли бы вы заменить F-слово на вы его облажались, чтобы на вас не обрушился дождь отрицательных голосов и флагов? - person sharptooth; 06.05.2011
comment
здесь небольшая проблема, вопрос заключается не только в том, чтобы убедиться, что параметр Derived фактически получен из Base<..>, но также и в том, чтобы текущее значение this было фактически Derived (или его производным типом), что вы можете проверить только во время выполнения. - person Matthieu M.; 06.05.2011
comment
@Matthieu: Мой ответ является расширением ответа @Bo Personn, поэтому то, что он сказал о защищенном конструкторе, справедливо и для меня. И затем, если this является чем-то иным, чем чем-то производным, значит, что-то очень и очень не так. - person Xeo; 06.05.2011
comment
Я согласен, что в CRTP this должно быть все в порядке, но вопрос заключается в том, чтобы определить, когда это не так. - person Matthieu M.; 06.05.2011
comment
к сожалению, это не охватывает случай, когда два класса происходят из одной и той же базы CRTP, и один из них ошибочно передает другой, например, class ConcreteDerived1 : public Base<ConcreteDerived1>{ }; class ConcreteDerived2 : public Base<ConcreteDerived1>{ };. Что еще хуже, так это то, что от static_cast до Derived все равно будет компилироваться, поскольку компилятор «знает, как» выполнять преобразование вниз. - person golvok; 11.10.2019

Кажется, существует способ проверить корректность CRPT во время компиляции.

Делая Base абстрактным (добавляя к Base некоторый чисто виртуальный метод), мы гарантируем, что любой экземпляр Base является частью некоторого производного экземпляра.

Делая все конструкторы Base закрытыми, мы можем предотвратить нежелательное наследование от Base.

Объявляя Derived как друга Base, мы допускаем единственное наследование, ожидаемое CRPT.

После этого приведение CRPT вниз должно быть правильным (поскольку что-то наследуется от базы и это "что-то" может быть только Derived, а не каким-то другим классом)

Возможно, для практических целей первый шаг (создание абстракции Base) является излишним, поскольку успешный static_cast гарантирует, что Derived находится где-то в иерархии Base. Это допускает только экзотическую ошибку, если Derived наследуется от Base <Derived> (как ожидает CRPT), но в то же время Derived создает другой экземпляр Base <derived> (без наследования) где-то в коде Derived (может, потому что это друг). Однако я сомневаюсь, что кто-то может случайно написать такой экзотический код.

person user396672    schedule 28.06.2012

Когда вы делаете что-то вроде ниже:

struct ConcreteDerived : public Base<Other>  // Other was not inteded

Вы можете создавать объекты class (производные или базовые). Но если вы попытаетесь вызвать функцию, она выдаст ошибку компиляции, связанную только с static_cast. ИМХО удовлетворит все практические сценарии.

Если я правильно понял вопрос, то чувствую ответ в самом Вашем вопросе. :)

person iammilind    schedule 06.05.2011