Предотвратить получение пользователем неправильной базы CRTP

Я не могу придумать правильный заголовок вопроса для описания проблемы. Надеюсь, приведенные ниже детали ясно объяснят мою проблему.

Рассмотрим следующий код

#include <iostream>

template <typename Derived>
class Base
{
    public :

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

class D1 : public Base<D1>
{
    public :

    void call_impl ()
    {
        data_ = 100;
        std::cout << data_ << std::endl;
    }

    private :

    int data_;
};

class D2 : public Base<D1> // This is wrong by intension
{
    public :

    void call_impl ()
    {
        std::cout << data_ << std::endl;
    }

    private :

    int data_;
};

int main ()
{
    D2 d2;
    d2.call_impl();
    d2.call();
    d2.call_impl();
}

Он скомпилируется и запустится, хотя определение D2 намеренно неверно. Первый вызов d2.call_impl() выведет некоторые случайные биты, которые ожидаются, поскольку D2::data_ не был инициализирован. Второй и третий вызовы будут выводить 100 для data_.

Я понимаю, почему он будет компилироваться и работать, поправьте меня, если я ошибаюсь.

Когда мы делаем вызов d2.call(), вызов преобразуется в Base<D1>::call, и это преобразует this в D1 и вызывает D1::call_impl. Поскольку D1 действительно является производным от Base<D1>, приведение в порядке во время компиляции.

Во время выполнения, после приведения, this, хотя это действительно объект D2, обрабатывается так, как если бы он был D1, и вызов D1::call_impl изменяет биты памяти, которые должны быть D1::data_, и выводит. В данном случае эти биты оказались там, где D2::data_. Я думаю, что второе d2.call_impl() также должно иметь неопределенное поведение в зависимости от реализации C++.

Дело в том, что этот код, хотя и является намеренно неверным, не даст пользователю никаких признаков ошибки. Что я действительно делаю в своем проекте, так это то, что у меня есть базовый класс CRTP, который действует как механизм диспетчеризации. Другой класс в библиотеке получает доступ к интерфейсу базового класса CRTP, скажем, call, и call будет отправляться в call_dispatch, который может быть реализацией базового класса по умолчанию или реализацией производного класса. Все это будет работать нормально, если определяемый пользователем производный класс, скажем, D, действительно является производным от Base<D>. Это вызовет ошибку времени компиляции, если оно получено из Base<Unrelated>, где Unrelated не является производным от Base<Unrelated>. Но это не помешает пользователю написать код, как указано выше.

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

Итак, мой вопрос заключается в том, есть ли способ запретить пользователю писать неправильный производный класс, как показано выше. То есть, если пользователь напишет производный класс реализации, скажем, D, но он унаследовал его от Base<OtherD>, тогда будет выдана ошибка времени компиляции.

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


person Yan Zhou    schedule 27.06.2012    source источник
comment
Краткий ответ: нет. dynamic_cast может быть дорого, но это дешевле, чем пытаться исправить своих пользователей.   -  person Rook    schedule 27.06.2012
comment
По крайней мере, сбойный dynamic_cast будет быстрее найден в модульных тестах. Вы не можете заставить компилятор защищать вас от всех возможных опечаток, например писать i + 1, имея в виду i - 1. Это похоже.   -  person Bo Persson    schedule 27.06.2012
comment
возможный дубликат Как избежать ошибок при использовании CRTP?   -  person Cassio Neri    schedule 11.08.2013
comment
Дублировать? stackoverflow.com/q/4417782/946850   -  person krlmlr    schedule 16.02.2015


Ответы (6)


1) сделать все конструкторы Base приватными (если конструкторов нет, добавить один)

2) объявить параметр производного шаблона как друга базы

template <class Derived>
class Base
{
private:

  Base(){}; // prevent undesirable inheritance making ctor private
  friend  Derived; // allow inheritance for Derived

public :

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

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

person user396672    schedule 28.06.2012
comment
Я думаю, это должно сработать! Почему эти действительно умные вещи всегда оказываются очень простыми! Это очень похоже на трюк Бартона-Накмана. - person Yan Zhou; 28.06.2012
comment
@Yan Zhou: я пробовал, кажется, работает :) Я также только что нашел связанный вопрос на SO: stackoverflow.com/questions/5907731/ и ответил на него тоже, но, возможно, есть некоторые подводные камни, так как вопрос выглядит более общим (не только о неправильном наследовании). - person user396672; 28.06.2012
comment
@user396672: user396672: Единственная ошибка, которую я вижу, это то, что он был неправильно сформирован до C++11. Стандарт явно запрещает, чтобы шаблон класса объявлял параметр типа шаблона как дружественный. - person Tanner Sansbury; 28.06.2012
comment
Проблема с таким подходом заключается в том, что он не допускает более сложных сценариев, в которых Derived не является непосредственным потомком Base. - person Григорий Шуренк&; 10.04.2017
comment
Вы даже можете пометить Base private ctor по умолчанию, это больше печатает, но тоже работает :) - person PaperBirdMaster; 10.04.2017

Если у вас есть С++ 11, вы можете использовать static_assert (если нет, я уверен, что вы можете эмулировать эти вещи с повышением). Вы можете утверждать, например. is_convertible<Derived*,Base*> или is_base_of<Base,Derived>.

Все это происходит в Базе, и все, что у нее когда-либо есть, — это информация Производного. У него никогда не будет возможности увидеть, является ли контекст вызова D2 или D1, так как это не имеет значения, поскольку Base<D1> создается один раз одним определенным способом, независимо от того, был ли он создан D1 или D2, производным от него ( или пользователем, явно создающим его экземпляр).

Поскольку вы не хотите (по понятным причинам, поскольку иногда это требует значительных затрат времени и памяти) использовать dynamic_cast, попробуйте использовать что-то, что часто называют «поли-кастом» (у boost тоже есть свой вариант):

template<class R, class T>
R poly_cast( T& t )
{
#ifndef NDEBUG
        (void)dynamic_cast<R>(t);
#endif
        return static_cast<R>(t);
}

Таким образом, в ваших отладочных/тестовых сборках обнаруживается ошибка. Хотя это и не 100% гарантия, на практике это часто позволяет выявить все ошибки, которые совершают люди.

person PlasmaHH    schedule 27.06.2012
comment
У меня была такая же идея, но это не так просто, как кажется. Если вы используете его в теле Base, как это static_assert( is_base_of<Base<Derived>, Derived>::value, "..."), это не сработает, потому что, если шаблон будет создан с помощью D1, он станет static_assert( is_base_of< Base< D1 >, D1 >::value, "..."), который не запускает утверждение (в конце концов, D1 получено из Base< D1 >). Единственные ошибки, которые он улавливает, - это те, которые происходят от Base< D3 >, где D3 не наследуется от правильного Base. Однако эти ошибки уже перехвачены файлом static_cast. - person LiKao; 27.06.2012
comment
@LiKao: а, теперь я понимаю, что ты имеешь в виду, позвольте мне добавить это к ответу. - person PlasmaHH; 27.06.2012
comment
Спасибо за подсказку с поликастом. У меня просто очень похожая идея. Я утверждаю dynamic_cast в режиме отладки, а затем выполняю static_cast. Шаблонное решение явно тоже очень элегантное. - person Yan Zhou; 27.06.2012
comment
+1 за поли каст. Этот тип приведения (или аналогичный, такой как assert_cast) настолько хорош, что должен быть частью стандарта ИМХО. - person LiKao; 27.06.2012
comment
Это решение требует, чтобы Base был полиморфным типом. С исходным опубликованным кодом, где Base не является полиморфным, само динамическое приведение не сможет скомпилироваться. - person Tanner Sansbury; 27.06.2012
comment
@twsansbury: я предполагал, что это очевидно и что это можно легко сделать, заключив виртуальный dtor в тот же NDEBUG в Base... - person PlasmaHH; 27.06.2012

Общий момент: шаблоны не защищены от создания экземпляров с неправильными параметрами. Это хорошо известная проблема. Не рекомендуется тратить время на попытки исправить это. Количество способов злоупотребления шаблонами бесконечно. В вашем конкретном случае вы можете что-то изобрести. Позже вы измените свой код, и появятся новые способы злоупотребления.

Я знаю, что в С++ 11 есть статические утверждения, которые могут помочь. Я не знаю полных деталей.

Другое дело. Помимо компиляции ошибок есть статический анализ. То, о чем вы просите, имеет что-то с этим. Анализ не обязательно ищет недостатки безопасности. Это может гарантировать, что в коде нет рекурсии. Он может проверять, что нет производных от какого-то класса, можно ставить ограничения на параметры шаблонов и функций и т.д. Это все анализ. Такие широко варьирующиеся ограничения не могут поддерживаться компилятором. Я не уверен, что это правильный путь, просто рассказываю о такой возможности.

p.s. Наша компания оказывает услуги в этой области.

person Kirill Kobelev    schedule 27.06.2012
comment
Я не могу найти здесь ничего, что могло бы ответить на его вопрос, только продвижение вашей компании... - person PlasmaHH; 27.06.2012
comment
Ну давай же. Я говорю, что: 1. С++ 2003 не обеспечивает хорошей защиты от неправильного создания экземпляра шаблона. 2. C++11 может помочь. 3. Может помочь алгоритмический анализ. Не обязательно с помощью наших инструментов. Где я говорю, что другие инструменты не будут работать? - person Kirill Kobelev; 27.06.2012
comment
Он спрашивает /что/ помогает, конкретно. Сказать, что есть что-то, что могло бы помочь, я думаю, он уже понял. - person PlasmaHH; 27.06.2012
comment
Мой главный совет заключается в том, что поиск способов защиты шаблонов в C++2003 — неправильный путь. Об этом есть статьи и, может быть, даже книги. - person Kirill Kobelev; 27.06.2012

Если вы не умеете считать с С++ 11, вы можете попробовать этот трюк:

  1. Добавьте статическую функцию в Base, которая возвращает указатель на свой специальный тип:

    статический Derived *derived() { return NULL; }

  2. Добавьте в базу шаблон статической функции check, который принимает указатель:

    template‹ typename T > static bool check( T *derived_this ) { return (derived_this == Base‹ Derived >::derived() ); }

  3. В ваших конструкторах Dn вызовите check( this ):

    Проверь это )

Теперь, если вы попытаетесь скомпилировать:

$ g++ -Wall check_inherit.cpp -o check_inherit
check_inherit.cpp: In instantiation of ‘static bool Base<Derived>::check(T*) [with T = D2; Derived = D1]’:
check_inherit.cpp:46:16:   required from here
check_inherit.cpp:19:62: error: comparison between distinct pointer types ‘D2*’ and ‘D1*’ lacks a cast                                                                                                                             
check_inherit.cpp: In static member function ‘static bool Base<Derived>::check(T*) [with T = D2; Derived = D1]’:                                                                                                                   
check_inherit.cpp:20:5: warning: control reaches end of non-void function [-Wreturn-type]                                                                                                                                          
person j4x    schedule 27.06.2012

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

  • Использование static_assert (либо из C++11, либо из boost) не работает, потому что проверка в определении Base может использовать только типы Base<Derived> и Derived. Таким образом, следующее будет выглядеть хорошо, но потерпит неудачу:

    template <typename Derived>
    class Base
    {
       public :
    
       void call ()
       {
          static_assert( sizeof( Derived ) != 0 && std::is_base_of< Base< Derived >, Derived >::value, "Missuse of CRTP" );
          static_cast<Derived *>(this)->call_impl();
       }
    };
    

Если вы попытаетесь объявить D2 как class D2 : Base< D1 >, статическое утверждение не поймает этого, поскольку D1 на самом деле является производным от Base< D1 >, а статическое утверждение полностью допустимо. Однако если вы наследуете от Base< D3 >, где D3 — это любой класс, не производный от Base< D3 >, как static_assert, так и static_cast вызовут ошибки компиляции, так что это абсолютно бесполезно.

Поскольку тип D2, который вам нужно будет проверить в коде Base, никогда не передается в шаблон, единственным способом использования static_assert будет перемещение его после объявлений D2, что потребует проверки того же человека, который реализовал D2, что опять же бесполезно.

Одним из способов обойти это было бы добавление макроса, но это не породило бы ничего, кроме чистого уродства:

#define MAKE_DISPATCHABLE_BEGIN( DeRiVeD ) \
   class DeRiVeD : Base< DeRiVed > {
#define MAKE_DISPATCHABLE_END( DeRiVeD )
    }; \
    static_assert( is_base_of< Base< Derived >, Derived >::value, "Error" );

Это только уродует, а static_assert снова лишний, потому что шаблон следит за тем, чтобы типы всегда совпадали. Так что тут никакой выгоды.

  • Лучший вариант: Забудьте обо всем этом и используйте dynamic_cast, который явно предназначен для этого сценария. Если вам это нужно чаще, вероятно, имеет смысл реализовать свой собственный asserted_cast (об этом есть статья на Dr. Jobbs), который автоматически запускает неудачное утверждение, когда dynamic_cast терпит неудачу.
person LiKao    schedule 27.06.2012

Невозможно предотвратить написание пользователем неверных производных классов; однако есть способы предотвратить вызов в вашем коде классов с неожиданными иерархиями. Если есть точки, в которых пользователь передает Derived библиотечным функциям, рассмотрите возможность того, чтобы эти библиотечные функции выполняли static_cast для ожидаемого производного типа. Например:

template < typename Derived >
void safe_call( Derived& t )
{
  static_cast< Base< Derived >& >( t ).call();
}

Или, если существует несколько уровней иерархии, рассмотрите следующее:

template < typename Derived,
           typename BaseArg >
void safe_call_helper( Derived& d,
                       Base< BaseArg >& b )
{
   // Verify that Derived does inherit from BaseArg.
   static_cast< BaseArg& >( d ).call();
}

template < typename T >
void safe_call( T& t )
{
  safe_call_helper( t, t );  
}

В обоих этих случаях safe_call( d1 ) скомпилируется, а safe_call( d2 ) не скомпилируется. Ошибка компилятора может быть не такой явной, как хотелось бы пользователю, поэтому стоит рассмотреть статические утверждения.

person Tanner Sansbury    schedule 27.06.2012