В следующем фрагменте кода происходит переполнение буфера, а именно при выполнении u->b = *b;.
class A {
public:
int x;
A() {
x = 5;
}
};
class B {
public:
int x;
float y;
B() {
x = 10;
y = 3.14;
}
};
union U {
A a;
B b;
};
U* foo(U* u) {
B* b = new B();
u->b = *b;
return u;
}
int main() {
A* a = new A();
U* u = (U*) a;
u = foo(u);
return u->b.y;
}
Судя по тому, что я прочитал здесь, актерский состав (U*) a должен быть четко определен. Насколько я понимаю, так не должно быть sizeof(U) != sizeof(A).
Кажется, что U и A являются взаимоконвертируемыми указателями, но я не уверен, означает ли это, что приведение четко определено.
В этом случае у меня возникли проблемы с соблюдением стандарта, буду рад любой помощи!
*** РЕДАКТИРОВАТЬ *** Я не планирую использовать этот код, но видел, как люди таким образом активировали объединение. Например здесь
«Из того, что я здесь прочитал, состав (U*) a должен быть четко определен». Взаимоконвертируемость указателя означает, что если там уже есть два объекта, допустимо приведение и использование указателя, как если бы он был указателем на другой.
Почему люди не могут просто смириться с тем, что использование (без тегов) объединений в C++ — это просто плохая идея. isocpp.github.io/CppCoreGuidelines/…
U* u = (U*) a;, но a на самом деле не указывает на объект U, так как же это может быть нормально? Кроме того, вопрос должен быть ясен без перехода по внешним ссылкам, поэтому, возможно, отредактируйте его более подробно.
Также обратите внимание, что здесь вы эффективно используете reinterpret_cast<U*>(a). Расскажите, пожалуйста, чего вы ожидаете здесь?
Это явно не указывает на члена союза, поэтому я не понимаю, как опубликованный код имеет какое-либо отношение к «приведению указателя члена союза к указателю объединения».
на самом деле в ответе уже говорится: «Это означает, что вы можете преобразовать их друг в друга с помощью reinterpret_cast. Однако вы не можете получить доступ к памяти неправильного типа». Приведение в порядке, притворяться, что оно укажет на то, на что оно не указывает, не нормально
@PepijnKramer не беспокойся, я не буду его где-либо использовать. Это скорее теоретический вопрос, поскольку я видел, как этот тип приведения использовался где-то еще, и задавался вопросом, правильный ли сам приведение или нет.
@ 463035818_is_not_an_ai Я не обращаюсь к памяти как к неправильному типу, поскольку активирую объединение с типом B. Вы подразумеваете, что это уже путаница типов, иначе говоря, приведение небезопасно?
a указывает на A. A не является союзом. Затем вы приводите этот указатель к другому типу, на который указатель не указывает.
единственный объект, который вы создаете в этом коде, имеет тип A. Не существует союза, к любому члену которого вы могли бы получить доступ.
Ни одно из условий в ответе, который вы связали, не соответствует вашей ситуации, поэтому ваш вывод озадачивает.
сам по себе указатель не является действительным указателем. В ответе объясняется, что если вы создадите объект правильного типа, вы также сможете получить к нему доступ, но только тогда.
Я думаю, вы неверно истолковали правила конвертируемости, применимые к типам, но они говорят: «Два объекта a и b взаимно конвертируются по указателям, если [...]» (выделено мной). Кроме того, размеры не имеют никакого отношения к конвертируемости.
@ 463035818_is_not_an_ai Мне больше всего интересно, в какой момент эта программа нарушает гарантии безопасности: во время каста, во время доступа? На мой взгляд это похоже на путаницу типов. Подразумевает ли взаимопреобразование указателей типобезопасное преобразование? Думаю, нет?
Это четко определено, потому что sizeof(U*) == sizeof(A*). Использование разыменования указателя на неправильный тип недопустимо, но, похоже, это не имеет отношения к вопросу. (Вот почему этот код дает сбой на моей машине.)
@vwvw В основном меня не так беспокоит, когда люди используют тег «language-laywer» (правильно). Но другие люди тоже читают комментарии, и... ну вы знаете ;)





Объекты типа U конвертируются указателями в свой подобъект U::a. Это можно проверить с помощью std::is_pointer_interconvertible_with_class(&U::a).
(U*) a это reinterpret_cast<U*>(a). Это static_cast<U*>(static_cast<void*>(a)), где [expr.static.cast]p14 говорит:
В противном случае, если исходное значение указателя указывает на объект a, и существует объект b типа, аналогичный
T, который является взаимоконвертируемым указателем с a, результатом будет указатель на b.
По этому адресу может отсутствовать объект типа U, то есть вы не получите указатель на U, а только указатель типа U, указывающий на *a.
Последующий доступ к нему как к U является неопределённым поведением (по очевидным причинам, указанным в [basic.lval]p11). Итак, u->b = *b; — это неопределенное поведение.
Однако не исключено, что это могло бы избежать УБ. Вызов operator new может вернуть больше байтов, чем запрошено ([basic.stc.dynamic.allocation]p2 , [new.delete.single]p3 ), и operator new неявно создает объект ( [intro.object] стр14) и std::is_implicit_lifetime_v<U>. Этот вызов operator new может выделить достаточно байтов для хранения объекта U, и в этом случае он неявно создаст U, что сделает его правильно сформированным.
Более стандартный способ сделать это:
A* a = static_cast<A*>(operator new(sizeof(U)));
U* u = reinterpret_cast<U*>(a);
u = foo(u);
return u->b.y;
Что явно позволяет избежать проблемы с размером, о которой вы беспокоились.
Спасибо за подробный ответ. Можем ли мы также заключить, что такое приведение является путаницей типов, поскольку оно нарушает безопасность типов? Если я понял правильно, типобезопасная программа всегда должна иметь объекты с допустимыми типами для базовой памяти. Можете ли вы сделать что-нибудь еще с u, кроме как вернуть его обратно в A, поскольку доступ к нему небезопасен?
Это предполагает, что на моей машине работает более стандартный способ, как я и ожидал. Стоит понять, как работает эта языковая функция, а затем избегать ее использования, кроме случаев, когда это необходимо, и вместо этого рассмотреть std::variant.
Re: «Вполне возможно, что это позволит избежать UB» — поведение не определено, поскольку стандарт C++ не говорит вам, что оно делает. Это не изменится, если operator new выделит достаточно байтов, чтобы код мог сделать то, что кто-то от него ожидает.
@PeteBecker, потому что может быть несколько неявно созданных объектов, если вы получаете достаточно памяти от new, стандарт сообщает вам, что происходит: *a является подобъектом AU и взаимоконвертируется указателем с неявно созданным U объектом
@Калет - цитируй, пожалуйста? «может выделить» не является техническим термином в стандарте C++. Код создает объект типа A, а затем говорит: «Представьте, что это на самом деле B». «Pointer-interconvertible» говорит, что у них одинаковый адрес; это не делает это преобразование действительным.
@PeteBecker reinterpret_cast разрешено выполнять преобразование между указателями, которые указывают на объекты, конвертируемые между указателями. Эти приведения действительны и являются целью взаимоконвертируемости указателей.
@Caleth - приведение допустимо; это не делает топтание памяти действительным. Посмотрите все комментарии к вопросу.
@PeteBecker приведение допустимо, если там есть U, т. е. если выделение было не менее sizeof(U) байтов
@PeteBecker На самом деле у нас было именно это обсуждение: возникает ли путаница типов, когда объект приводится к неправильному типу, или только в том случае, если осуществляется доступ к некоторым элементам этого незаконного приведения.
@PeteBecker: приведение типа-члена объединения к типу объединения, а затем доступ к этому же члену либо через объединение, либо через memcpy (с тем же размером, что и член), может завершиться неудачей, если объединение содержит какие-либо типы, выравнивание которых более грубое, чем у члена. . Подобный сбой легко продемонстрировать с помощью лязга, нацеленного на Cortex-M0.
afaik, приведение в порядке, а то, что вы делаете с указателем в вашем коде, - нет.