C++ не может преобразовать базу A в производный тип B через виртуальную базу A.

У меня есть три класса:

class A {};

class B : virtual public A {};
class C : virtual public A {};

class D: public B, public C {};

При попытке статического приведения от A * к B * я получаю следующую ошибку:

cannot convert from base A to derived type B via virtual base A

person Panayiotis Karabassis    schedule 19.09.2010    source источник
comment
Это 4 класса, кстати   -  person Alex Gidan    schedule 08.11.2019


Ответы (7)


Чтобы понять систему приведения, вам нужно погрузиться в объектную модель.

Классическим представлением простой иерархической модели является включение: если B происходит от A, то объект B фактически будет содержать подобъект A наряду со своими собственными атрибутами.

В этой модели понижение приведения представляет собой простую манипуляцию указателем со смещением, известным во время компиляции, которое зависит от схемы памяти B.

Это то, что делает static_cast: статическое приведение называется статическим, потому что вычисление того, что необходимо для приведения, выполняется во время компиляции, будь то арифметика указателя или преобразования (*).

Однако, когда вступает в действие virtual наследование, все становится немного сложнее. Основная проблема заключается в том, что при наследовании virtual все подклассы совместно используют один и тот же экземпляр подобъекта. Для этого B будет иметь указатель на A вместо собственно A, а объект базового класса A будет создан за пределами B.

Следовательно, во время компиляции невозможно вывести необходимую арифметику указателя: это зависит от типа объекта во время выполнения.

Всякий раз, когда есть зависимость от типа среды выполнения, вам нужна RTTI (информация о типе среды выполнения), а использование RTTI для приведения — это задача dynamic_cast.

В итоге:

  • понижение времени компиляции: static_cast
  • пониженное время выполнения: dynamic_cast

Два других также являются приведениями во время компиляции, но они настолько специфичны, что легко запомнить, для чего они нужны... и они вонючие, так что лучше их вообще не использовать.

(*) Как заметил @curiousguy в комментариях, это справедливо только для понижения. static_cast позволяет повышать приведение независимо от виртуального или простого наследования, хотя в этом случае приведение также не требуется.

person Matthieu M.    schedule 20.09.2010
comment
Хороший ответ, который заставил меня понять, как на самом деле работает виртуальное наследование! +1 - person undu; 12.06.2012
comment
Мне нравится ваш ответ, но ОП, по-видимому, спрашивал об ошибке для DOWNCASTING, а не для повышения. - person h9uest; 22.12.2015
comment
@h9uest: Спасибо, что указали на оплошность, я изменил приведение вверх на приведение вниз, и теперь все в порядке. - person Matthieu M.; 22.12.2015
comment
ЛОЖЬ. static_cast можно использовать для преобразования производного PTR в виртуальный базовый PTR. - person curiousguy; 26.12.2015
comment
@curiousguy: Действительно ... хотя тогда вам даже не нужен актерский состав, поэтому я не подумал принять это во внимание. Напрашивается вопрос, почему понижение тоже недоступно. - person Matthieu M.; 26.12.2015
comment
Преобразование в базовый класс возможно для неполиморфных классов. dynamic_cast возможно только для полиморфных классов: dynamic_cast означает использование vptr (в большинстве случаев). Преобразование в виртуальный базовый класс использует либо vptr, либо внутренний указатель (или фиксированное смещение, если известен окончательный полный тип). - person curiousguy; 26.12.2015
comment
@curiousguy, @matthieu-m: Я не согласен. static_cast выполняет приведение во время выполнения. Upcast (без выражения приведения) также выполняется во время выполнения. Хотя правда, что скомпилированный код короче, чем для dynamic_cast. Для указателей static_cast компилирует if != nullptr, а затем добавляет смещение, известное во время компиляции. Однако, если включено виртуальное наследование, static_cast и восходящее преобразование также компилируют if != nullptr, а затем добавляют смещение, известное во время выполнения. Пример. static_cast также может вызывать ctors., что также полностью выполняется во время выполнения. - person Dr. Gut; 23.10.2019
comment
@Dr.Gut Вы сделали здесь много баллов. 1) также может вызывать ctors. Действительно, static_cast может многое. Я думал, что мы обсуждаем здесь приведение ptr, а не неявное преобразование в целом или как создать объект. 2) Можем ли мы согласиться с тем, что контекст: выполнение одного и того же преобразования из типа X в тип Y либо подразумевается, с какой-либо операцией приведения или другой. 3) Можете ли вы описать X и Y таким образом, чтобы эти варианты были действительными и предоставили код, скомпилированный для сравнения? - person curiousguy; 23.10.2019
comment
@curiousguy: 1) Предложение (*) заканчивается конверсией или. Поэтому я подумал, что он хочет быть общим. Я отредактировал ответ. Теперь кажется яснее. Надеюсь, вам это понравится. Первая часть касается сдерживания: смещение известно во время компиляции, будь то восходящее или нисходящее. Вторая часть: о виртуальном наследовании (downcast выдает сообщение об ошибке, upcast возможен, но смещение известно во время выполнения). 2) Давайте сосредоточимся на приведениях между указателями связанных типов классов. В противном случае термины «время компиляции» и «время выполнения» могут быть неправильно истолкованы. 3) Я не понимаю, о чем вы просите. Какие варианты? - person Dr. Gut; 25.10.2019
comment
@Dr.Gut 1) Вы хотели быть общим, но, похоже, подразумевали, что static_cast всегда будет применять фиксированное постоянное смещение. 3) Выбор двух преобразований X и Y, от U->V между приведением нового стиля *_cast, приведением старого стиля, неявным преобразованием для двух заданных типов U,V. В каком случае X и Y а) оба действительны б) различны на уровне генерации кода? - person curiousguy; 26.10.2019
comment
@curiousguy: Все еще не уверен, о чем ты просишь. Это, вероятно, не ново для вас. Так что, я не знаю. Может быть, такого примера нет. Задайте его как вопрос здесь, на StackOverflow. - person Dr. Gut; 28.10.2019

Насколько я знаю, вам нужно использовать dynamic_cast, потому что наследование virtual и вы понижаете.

person Jon Purdy    schedule 19.09.2010

Вы не можете использовать static_cast в этой ситуации, потому что компилятор не знает смещения B относительно A во время компиляции. Смещение должно вычисляться во время выполнения на основе точного типа самого производного объекта. Поэтому вы должны использовать dynamic_cast.

person Yakov Galka    schedule 19.09.2010
comment
При преобразовании производного в виртуальную базу можно использовать static_cast. - person curiousguy; 26.12.2015
comment
@curiousguy: да, но вопрос в преобразовании базы в производную. - person Yakov Galka; 26.12.2015
comment
Непонятно, почему ваш аргумент не применим и к производным от базы. - person curiousguy; 26.12.2015
comment
@curiousguy: ну, если ты спрашиваешь, почему, то я думаю, что это хороший вопрос, и я не уверен, каков ответ. Я предполагаю, что это связано с тем, что преобразование из производного в базовое является вопросом простой косвенности через vtable. С другой стороны, для преобразования из базового в производное вы должны выполнить тот же (не постоянное время) алгоритм, что и dynamic_cast для невиртуальное наследование. Таким образом, в противном случае не было бы разницы между статическим и динамическим приведением типов для виртуального наследования. Или, другими словами, нет аналогии для невиртуального static_cast в случае виртуального наследования. - person Yakov Galka; 27.12.2015

Да, вам нужно использовать dynamic_cast, но вам нужно будет сделать базовый класс A полиморфным, например. добавив виртуальный dtor.

person Nico    schedule 19.09.2010
comment
или добавление хотя бы одного виртуального метода. - person Liton; 20.09.2010

Согласно стандартным документам,

Раздел 5.2.9–9, для Статической трансляции,

Значение r типа «указатель на cv1 B», где B — тип класса, может быть преобразовано в значение r типа «указатель на cv2 D», где D — класс, производный (раздел 10) от B, если действующий стандарт существует преобразование из «указателя на D» в «указатель на B» (4.10), cv2 — это та же cv-квалификация, что и cv1, или более высокая cv-квалификация, чем cv1, и B не является ни виртуальным базовым классом D, ни базовый класс виртуального базового класса D.

Следовательно, это невозможно, и вы должны использовать dynamic_cast...

person liaK    schedule 20.09.2010

$5.2.9/2- «Выражение e может быть явно преобразовано в тип T с использованием static_cast формы static_cast(e), если объявление «T t(e);» корректно для некоторой выдуманной временной переменной t (8.5)».

В вашем коде вы пытаетесь использовать static_cast с «T = B*» и «e = A*»

Теперь «B * t (A *)» неправильно сформирован в C ++ (но «A * t (B *)» потому, что «A» является виртуальной однозначной и доступной базой «B». Поэтому код дает ошибку .

person Chubsdad    schedule 20.09.2010

Не знаю, "безопасно" ли это, но.

Предполагая

B получен из A (и A чисто виртуальный)

Поскольку я ЗНАЮ, что указатель на B все еще остается указателем на B.

    class A
    {
            virtual void doSomething(const void* p) const =0;
    };

    class B
    {
    public:
            int value;
            virtual void doSomething(const void*p)const
            {
            const B * other = reinterpret_cast<const B*>(p);
            cout<<"hello!"<< other->value <<endl;
            }
    };

    int main()
    {
            B  foo(1),bar(2);
            A * p = &foo, q=&bar;
            p->doSomething(q);
            return 0;
    }

эта программа выполняется и корректно возвращает вывод "hello!" и значение другого объекта (в данном случае «2»).

кстати, то, что я делаю, очень небезопасно (лично я даю разные идентификаторы каждому классу и утверждаю после переинтерпретации приведения, что текущий идентификатор равен другому идентификатору, чтобы быть уверенным, что мы делаем что-то с двумя одинаковыми классами) и как видите ли, я ограничился "константными" методами. Таким образом, это будет работать с "неконстантными" методами, но если вы сделаете что-то не так, отлов ошибки будет практически невозможен. И даже с утверждением есть 1 шанс из 4 миллиардов добиться успеха утверждения, даже если оно должно потерпеть неудачу (assert(ID== other->ID);)

Между прочим... Хороший объектно-ориентированный дизайн не должен требовать такого рода вещей, но в моем случае я попытался реорганизовать/перепроектировать код, не имея возможности отказаться от повторного приведения. вообще говоря, вы МОЖЕТЕ избежать такого рода вещей.

person CoffeDeveloper    schedule 16.11.2012
comment
Вы уверены, что это необходимо? - person Panayiotis Karabassis; 16.11.2012
comment
это конкретно для вашей проблемы. редизайн должен предотвратить это в большинстве случаев (избегайте моего примера, если можете). о боже, я забыл константу. - person CoffeDeveloper; 17.11.2012
comment
Я имею в виду, ты уверен, что тебе это нужно? Что, если я передам doSomething указатель типа int? Это бы катастрофически провалилось. Почему бы не использовать динамическое приведение и не проверить результат? Я не знаю, каковы ваши точные требования, но я считаю, что если вы добавите статическую систему полиморфизма (например, шаблон CRTP), вы сможете придумать что-то более безопасное. - person Panayiotis Karabassis; 14.12.2012
comment
статический полиморфизм не подходит во многих случаях, в любом случае в моем реальном коде я делаю это безопасным способом, поэтому у пользователей нет возможности передать int* вокруг. Первоначально у меня была система ID, но позже я нашел лучшую систему.. все же в разы быстрее, чем динамическое приведение. - person CoffeDeveloper; 02.06.2013
comment
если вам все еще интересно, как этот код может быть полезен, вы можете взглянуть на мой недавний проект. трюки с void* используются более чем в одном месте: code.google.com/p/infectorpp - person CoffeDeveloper; 13.07.2013