Подтвердить что ты не робот

Предотвращение получения пользователем неправильной базы 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 изменяет биты памяти, которые должны be D1::data_ и выводить. В этом случае эти биты оказались там, где D2::data_. Я думаю, что второй d2.call_impl() также должен быть undefined в зависимости от реализации С++.

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

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

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

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

4b9b3361

Ответ 1

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

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

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.

Ответ 2

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

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

Поскольку вы не хотите (по понятным причинам, поскольку он иногда имеет значительные затраты времени на рабочую среду и издержки памяти), используйте dynamic_cast, попробуйте использовать что-то часто называемое "poly 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);
}

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

Ответ 3

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

Я знаю, что С++ 11 имеет static assert, который может помочь. Я не знаю подробностей.

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

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

Ответ 4

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

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

    статический Производный * производный() {return NULL; }

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

    шаблон < typename T > статическая проверка bool (T * received_this) {   return (производный_this == Base <Derived> :: производный()); }

  • В ваших конструкторах 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]                                                                                                                                          

Ответ 5

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

  • Использование static_assert (либо из С++ 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 >, static assert не поймает это, так как D1 actualy получается из 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 (на нем есть статья о д-р Джоббс), которая автоматически запускает неудавшееся утверждение, когда dynamic_cast терпит неудачу.

Ответ 6

Невозможно запретить пользователю писать неправильные производные классы; однако есть способы, чтобы ваш код не вызывал классы с неожиданными иерархиями. Если есть точки, в которых пользователь передает 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 ) не сможет скомпилироваться. Ошибка компилятора может быть не такой явной, как хотелось бы пользователю, поэтому может быть полезно рассмотреть статические утверждения.