Безопасность static_cast

AFAIK, для указателей/ссылок static_cast, если определение класса не видно компилятору в этой точке, тогда static_cast будет вести себя как reinterpret_cast.

Почему static_cast небезопасно для указателей/ссылок и безопасно для числовых значений?

Ответ 1

Короче говоря, из-за множественного наследования.

В длинном:

#include <iostream>

struct A { int a; };
struct B { int b; };
struct C : A, B { int c; };

int main() {
    C c;
    std::cout << "C is at : " << (void*)(&c) << "\n";
    std::cout << "B is at : " << (void*)static_cast<B*>(&c) << "\n";
    std::cout << "A is at : " << (void*)static_cast<A*>(&c) << "\n";

}

Вывод:

C is at : 0x22ccd0
B is at : 0x22ccd4
A is at : 0x22ccd0

Обратите внимание, что для правильной конвертации в B * static_cast должен изменить значение указателя. Если у компилятора не было определения класса для C, то он не знал бы, что B является базовым классом, и он, конечно же, не знает, какое смещение применять.

Но в ситуации, когда определение не видно, static_cast не ведет себя как reinterpret_cast, это запрещено:

struct D;
struct E;

int main() {
    E *p1 = 0;
    D *p2 = static_cast<D*>(p1); // doesn't compile
    D *p3 = reinterpret_cast<D*>(p1); // compiles, but isn't very useful
}

Простой C-стиль, (B*)(&c) делает то, что вы говорите: если видно, что структура C видна, показывая, что B является базовым классом, то это то же самое, что и static_cast. Если типы только декларируются только вперед, то это то же самое, что и reinterpret_cast. Это связано с тем, что он предназначен для совместимости с C, что означает, что он должен делать то, что C делает в случаях, которые возможны в C.

static_cast всегда знает, что делать для встроенных типов, это действительно то, что встроенные средства. Он может конвертировать int в float и т.д. Поэтому он всегда безопасен для числовых типов, но он не может преобразовать указатели, если (а) он не знает, на что они указывают, и (б) существует правильный вид отношений между указанными типами. Следовательно, он может преобразовать int в float, но не от int* до float*.

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

A a;
C *cp = static_cast<C*>(&a); // compiles, undefined behaviour

Одна из вещей, которые static_cast может сделать, это "downcast" указатель на производный класс (в этом случае C является производным классом A). Но если referand не является фактически производным классом, вы обречены. A dynamic_cast выполнит проверку во время выполнения, но для моего класса класса C вы не можете использовать dynamic_cast, потому что у A нет виртуальных функций.

Аналогичным образом вы можете делать небезопасные вещи с static_cast до и void*.

Ответ 2

Нет, ваш "AFAIK" неверен. static_cast никогда не ведет себя как reinterpret_cast (за исключением, может быть, когда вы конвертируете в void *, хотя это преобразование обычно не должно выполняться reinterpret_cast).

Во-первых, когда static_cast используется для преобразования указателя или ссылки, спецификация static_cast явно требует определенного отношения между типами (и должно быть известно static_cast). Для типов классов они должны быть связаны наследованием, как это воспринимается static_cast. Это требование невозможно удовлетворить, если оба типа полностью не определены точкой static_cast. Таким образом, если определение не отображается в точке static_cast, код просто не будет компилироваться.

Чтобы проиллюстрировать вышеприведенные примеры: static_cast можно использовать [избыточно] для выполнения upcasts указателя объекта. Код

Derived *derived = /* whatever */;
Base *base = static_cast<Base *>(derived);

компилируется только тогда, когда следующий код компилируется

Base *base(derived);

и для этого для компиляции определение обоих типов должно быть видимым.

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

Base *base = /* whatever */;
Derived *derived = static_cast<Derived *>(base);

компилируется только тогда, когда следующий код компилируется

Base *base(derived); // reverse direction

и, опять же, для этого для компиляции определение обоих типов должно быть видимым.

Таким образом, вы просто не сможете использовать static_cast с типами undefined. Если ваш компилятор позволяет это, это ошибка в вашем компиляторе.

static_cast может быть небезопасным для указателей/ссылок для совершенно другой причины. static_cast может выполнять иерархические downcasts для объектов указателя/ссылочных типов без проверки фактического динамического типа объекта. static_cast также может выполнять иерархические повышения для типов указателей методов. Использование результатов этих непроверенных бросков может привести к поведению undefined, если это сделано без осторожности.

Во-вторых, когда static_cast используется с арифметическими типами, семантика совершенно различна и не имеет ничего общего с вышеизложенным. Он просто выполняет преобразования арифметического типа. Они всегда совершенно безопасны (в стороне от проблем с диапазоном), если они соответствуют вашим намерениям. На самом деле, это может быть хороший стиль программирования, чтобы избежать static_cast для арифметических преобразований и вместо этого использовать статические C-стиль, чтобы обеспечить четкое различие в исходном коде между всегда безопасными арифметическими переводами и потенциально-опасным иерархическим указателем/ссылки.