Предотвратить вывод пользователя из неправильной базы 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::call, и это приводит thisна D1и вызовите D1::call_impl. Поскольку D1действительно является производным от Base, поэтому приведение выполняется во время компиляции.

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

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

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

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

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

24
задан Mikhail 28 May 2017 в 17:34
поделиться