Я не могу придумать правильный заголовок вопроса для описания проблемы. Надеюсь, приведенные ниже детали ясно объяснят мою проблему.
Рассмотрим следующий код
#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
. Тем не менее, это обширно, и даже когда это работает, это ошибка времени выполнения.
dynamic_cast
может быть дорого, но это дешевле, чем пытаться исправить своих пользователей. - person Rook   schedule 27.06.2012i + 1
, имея в видуi - 1
. Это похоже. - person Bo Persson   schedule 27.06.2012