Преобразование. При использовании C ++ существует ли безопасный и законный способ реализации «безопасного преобразования» для иерархии классов, включающей множественное и виртуальное наследование без RTTI?

В настоящее время я работаю над реализацией «безопасного приведения» для небольшого подмножества моего проекта, используя систему мета-типов, потому что a) RTTI и dynamic_cast были преднамеренно отключены в среде моего проекта и b) такая функциональность значительно упростила бы и прояснила код.

В моем проекте есть многоуровневая иерархия классов с множественным наследованием в некоторых случаях. Однако существует один «корневой» класс, который фактически наследуется, прямо или косвенно, всеми другими классами. (Это единственное виртуальное наследование в иерархии.) Существует ли безопасный, законный способ выполнения downcast в такой иерархии без использования RTTI / dynamic_cast? Я искал в Интернете и рассмотрел несколько подходов, но ни один, кажется, не попал в цель. В моем случае иерархия классов известна — каждый объект знает свой (мета) тип и своих предков, в основном, с помощью тегов / перечислений. (Есть ли другая информация, такая как порядок инициализации базового класса, которая должна быть известна для реализации «безопасного приведения»?)

Один из подходов, которые я попробовал, состоял в том, чтобы уменьшить значение посредством вызова виртуальной функции, которая возвращает указатель «this». Из того, что я прочитал (и из своего собственного опыта), я понимаю, что указатель «this» в C ++ имеет тип, соответствующий классу, в котором определена функция-член, с квалификациями cv из функции-члена, примененной к нему , (Как описано здесь => Тип указателя this). Я попытался получить указатель, имеющий динамический (во время выполнения) тип объекта, возвращая указатель «this». Поскольку я хочу, чтобы все мои объекты реализовывали эту функцию, я делаю ее чистой виртуальной функцией в своем базовом классе, а затем определяю ее в своих производных классах, используя ковариантный тип возврата (указатель реализующего класса). Диспетчеризация через базовый указатель работает, как и ожидалось, но возвращаемый указатель, похоже, имеет тип, основанный на вызывающем указателе, а не тип вызываемой реализации.

Вот код, который иллюстрирует проблему:

#include <iostream>
struct Base {
virtual ~Base(){}
virtual Base* foo( void ) = 0 ;
} ;
struct Derived : public Base { // <= XXX
Derived* foo( void ){ std::cout << "In Derived::foo()" << std::endl ; return this ; }
void foo2( void ) { std::cout << "In Derived::foo2()" << std::endl ; }
} ;
int main ( int argc, char* argv[] ) {
Base* base = new Derived ;
base->foo( ) ;  // Dispatches to Derived::foo()
//   Derived* derived = base->foo( ) ;  // PROBLEM!
Derived* derived = static_cast< Derived* >( base->foo( ) ) ;  // Works, as long as inheritance at XXX is non-virtual
derived->foo2( ) ;
delete base ;
return 0 ;
}

Как написано выше, код выше компилируется и запускается без проблем. Но если я раскомментирую строку с надписью «ПРОБЛЕМА» и закомментирую строку под ней, я получу следующую ошибку компилятора (при использовании g ++ версии 4.83 в среде cygwin — НЕ в моей конечной целевой среде):

test.cpp: In function ‘int main(int, char**)’:
test.cpp:13:34: error: invalid conversion from ‘Base*’ to ‘Derived*’ [-fpermissive]
Derived* derived = base->foo( ) ;  // PROBLEM!
^

Я пробовал это с версиями icc, clang и g ++ в http://gcc.godbolt.org/ и получил аналогичные результаты, так что я считаю, что это не ошибка компилятора, и мне не хватает чего-то очень фундаментального.

Каков тип возвращаемого указателя «this» из определения виртуальной функции-члена с ковариантным типом возврата? Когда я вызываю foo () через Base *, я, очевидно, выполняю реализацию Derived :: foo (), и эта функция-член объявляется возвращающей Derived *. Итак, почему downcast необходим для выполнения присваивания из возвращенного указателя? Использование static_cast является проблемой, потому что у меня есть фактически унаследованный базовый класс в моем коде проекта, и static_cast будет зависеть от этого. У меня сложилось впечатление, что использование приведения типа C или reinterpret_cast для понижения базового класса небезопасно (по крайней мере, в общем случае), поскольку они «слепы» по отношению к данным объекта / макету виртуальной таблицы. Можно ли сделать их «безопасными», следуя некоторым правилам / ограничениям?

1

Решение

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

Так что в вашем примере значение, возвращаемое Derived имеет тип Derived, но когда вы вызываете эту функцию через Base указатель, компилятор должен решить во время компиляции может ли возвращаемое значение быть юридически назначено. Во время выполнения правильный экземпляр foo() будет называться с foo() объявлен виртуальным, но компилятор должен использовать один и тот же тип возврата независимо от того, какой подкласс Base фактически используется во время выполнения. Вот почему ковариантные возвращаемые типы не могут быть полностью произвольными. То есть вы не можете вернуть int в вашем базовом классе и string в вашем производном классе. Тип возвращаемого значения производного класса должен быть подтипом типа, возвращаемого в базовом классе.

Здесь, вероятно, стоит отметить, что, хотя вы смогли привести тип возвращаемого значения и правильно скомпилировать все, вы могли бы также привести указатель и получить правильное значение:

Derived* derived = static_cast< Derived* >(base)->foo( ); // OK

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

class Base
{
int myId;

protected:
Base(int id) : myId(id)

public:
template <class T>
T* downcast()
{
if (myId == T::ID) { return static_cast<T*>(this); }
return nullptr;
}
};
class Derived1
{
public:
static const int ID = 1;
Derived1() : Base(ID) {}
};
class Derived2
{
public:
static const int ID = 2;
Derived2() : Base(ID) {}
};Base* b = new Derived1();

Derived1* d1 = b->downcast<Derived1>(); // Ok, returns a value
Derived2* d2 = b->downcast<Derived2>(); // returns nullptr

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

Дополнительные пояснения: смысл всего этого обходного пути основан на предположении, что вы держитесь за Base* и хочу привести его к Derived* но вы не уверены, что объект на самом деле является Derived* и вы хотите какую-то проверку, способ dynamic_cast делает. если ты знать что такое производный тип, и просто хочу его привести, static_cast это путь. static_cast не проверяет, чтобы убедиться, что тип времени выполнения, который вы держите в базовом указателе, на самом деле является типом, к которому вы приводите, но он выполнит проверку времени компиляции, чтобы убедиться, что приведение является допустимым. Так что вы не можете, например, использовать static_cast изгонять из int* к string*, reinterpret_cast позволит, но тогда вы просто заставляете то, что, вероятно, является плохим, и не хотите этого делать.

2

Другие решения


По вопросам рекламы ammmcru@yandex.ru
Adblock
detector