Я не могу придумать правильный заголовок вопроса для описания проблемы. Надеюсь, приведенные ниже детали ясно объяснят мою проблему.
Рассмотрим следующий код.
#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
, и это приводит 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
. Тем не менее, это обширно, и даже когда это работает, это ошибка времени выполнения.