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

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

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

11
задан Peter Mortensen 23 November 2017 в 13:02
поделиться

2 ответа

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

В длинном:

#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 может «понижать» указатель на производный класс (в этом случае C является производным классом от A).Но если референдум не относится к производному классу, вы обречены. dynamic_cast будет выполнять проверку во время выполнения, но для моего примера класса C вы не можете использовать dynamic_cast , потому что A не имеет виртуальных функций.

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

22
ответ дан 3 December 2019 в 03:35
поделиться

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

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

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

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

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

Base *base(derived);

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

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

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

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

Base *base(derived); // reverse direction

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

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

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

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

5
ответ дан 3 December 2019 в 03:35
поделиться
Другие вопросы по тегам:

Похожие вопросы: