Здравствуйте, okman, Вы писали: O>Хотелось бы понять, насколько такое поведение (т.е. разыменование указателя, возможно висячего, O>при выполнении приведений типов по иерархии наследования) легально с точки зрения стандарта C++. O>Или же это сугубо implementation-defined?..
Если я правильно понял, то Вы наткнулись на неопределенное поведение при преобразовании указателей:
С++14
12.7 Construction and destruction
3. To explicitly or implicitly convert a pointer (a glvalue) referring to an object of class X to a pointer (reference) to a direct or indirect base class B of X, the construction of X and the construction of all of its direct or indirect bases that directly or indirectly derive from B shall have started and the destruction of these classes shall not have completed, otherwise the conversion results in undefined behavior. To form a pointer to (or access the value of) a direct non-static member of an object obj, the construction of obj shall have started and its destruction shall not have completed, otherwise the computation of the pointer value (or accessing the member value) results in undefined behavior.
[Example:
struct A { };
struct B : virtual A { };
struct C : B { };
struct D : virtual A { D(A*); };
struct X { X(A*); };
struct E : C, D, X { //undefined: upcast from E* to A*
E() : D(this), //might use path E* → D* → A*
// but D is not constructed
// D((C*)this), // defined:
// E* → C* defined because E() has started
// and C* → A* defined because
// C fully constructed
X(this) { // defined: upon construction of X,
} // C/B/D/A sublattice is fully constructed
};
— end example ]
То есть для Вашего случая сначала необходимо сделать преобразование (присваивание) пока объект не уничтожен, и только затем удалять объект:
O>#include <cstdio>
O>struct Base
O>{
O> virtual ~Base() {}
O>};
O>struct X1 : public virtual Base {};
O>struct X2 : public virtual Base {};
O>struct Child : public X1, public X2 {};
O>int main()
O>{
O> X1 * p1 = new X1();
O> delete p1;
O> Base * p2;
O> p2 = p1;
O> printf("p2 = %p\r\n", (void *)p2);
O> return 0;
O>}
O>
O>При запуске на VS2015 или VS2008 в режиме Debug программа падает на строке 'p2 = p1':
У меня также (на VS2015) данный пример падает на строке 'p2 = p1'
После чего я изменил пример так:
#include <cstdio>
struct Base
{
virtual ~Base() {}
};
struct X1 : public virtual Base {};
struct X2 : public virtual Base {};
struct Child : public X1, public X2 {};
int main()
{
X1 * p1 = new X1();
delete p1;
X1 * p2;// Эта строка изменена!!!
p2 = p1;
printf("p2 = %p\r\n", (void *)p2);
return 0;
}
...и все падения прекратились!
O>Или же это сугубо implementation-defined?..
+100500
ИМХО похоже именно на это.
Re[3]: Безопасно ли присваивать один указатель другому?..
На стандарт ссылку не дам, но, думаю, суть в том, что при присваивании p2 = p1 происходит не просто копирование, а преобразование адреса, для которого требуется прочитать RTTI. Если адрес p1 невалидный, то при обращении к RTTI происходит исключение. В случае валидного адреса p1, переменная p2 будет содержать адрес с неким смещением.
Re[4]: Безопасно ли присваивать один указатель другому?..
Здравствуйте, Croessmah, Вы писали:
C>Если я правильно понял, то Вы наткнулись на неопределенное поведение при преобразовании указателей: C>...
Спасибо, именно эта цитата все объясняет:
To explicitly or implicitly convert a pointer (a glvalue) referring to an object of class X to a
pointer (reference) to a direct or indirect base class B of X, the construction of X and the construction
of all of its direct or indirect bases that directly or indirectly derive from B shall have
started and the destruction of these classes shall not have completed, otherwise the conversion
results in undefined behavior.
C>То есть для Вашего случая сначала необходимо сделать преобразование (присваивание) пока объект не уничтожен, и только затем удалять объект: C>...
Похоже, что разработчики библиотек в курсе про этот нюанс. Вот нашел в исходниках
Boost 1.66.0 следующий комментарий (/boost/smart_ptr/weak_ptr.hpp):
//
// The "obvious" converting constructor implementation:
//
// template<class Y>
// weak_ptr(weak_ptr<Y> const & r): px(r.px), pn(r.pn)
// {
// }
//
// has a serious problem.
//
// r.px may already have been invalidated. The px(r.px)
// conversion may require access to *r.px (virtual inheritance).
//
// It is not possible to avoid spurious access violations since
// in multithreaded programs r.px may be invalidated at any point.
//
Судя по всему, они подразумевают примерно такой кейс:
Абсолютно безопасно за исключением случая с указателем на виртуальную функцию, который имеет извращенный тип.
Проблемы могут возникнуть исключительно в момент разименования.
Более того, данная операция атомарна и соответственно потокобезопасна на любой архитектуре у которой размер типа указателя меньше либо равен разрядности процессора.
Возможно на некоторых других то-же, но это надо смотреть документацию.
Так что можно пользоваться невозбранно за исключением хитрых embeded систем у которых память адресуется по банкам.
но правильно такая операция пишеться
TypeA * a;
TypeB * b;
a=reinterpret_cast<TypeA *>(b);
Потому что при простом приравнивании для указателей на родственные классы начинает работать таинственная адресная магия, которая требует возможности их разименования.
Здравствуйте, okman, Вы писали:
O>Как думаете, возможно ли в C или C++ получить какой-нибудь побочный эффект во время O>присваивания одного указателя другому? Т.е., упрощенно говоря, может ли программа упасть O>на выполнении простой конструкции типа x = y? Считаем, что x и y — это самые обычные O>"сырые" указатели, т.е. не смарт-поинтеры, не классы с переопределенным оператором O>присваивания и ничего такого, а просто самые обычные указатели: O>Или такое присваивание всегда безопасно, даже если сами указатели содержат null или "мусор"?
Можно получить не идемпотентное поведение.
Derived* d;
Base* b = d; // up-cast разрешён всегда
Derived* d1 = static_cast<Derived*>(b); // down-cast должен быть явным
Base* b1 = d1;
assert(d == d1);
assert(b == b1);
Здравствуйте, okman, Вы писали:
O>Или такое присваивание всегда безопасно, даже если сами указатели содержат null или "мусор"?
Ответ в рамках существующих систем — безопасно.
Ответ формальный — нет. Потому что указатель — это не адрес вообще-то, а лишь нечто такое, применение к которому операции дерефренсирования дает доступ к объекту, на который он указывает.
Механизм этого дерефренсирования может в принципе быть любым.
Поэтому и механизм копирования указателя может в принципе быть любым, а значит, и содержать в себе любые проверки, которые не пройдут при некорректном присваивании.
Все это очень далеко от реальности и чисто умозрительно. Но не невозможно в принципе.
With best regards
Pavel Dvorkin
Re[2]: Безопасно ли присваивать один указатель другому?..
Здравствуйте, Pavel Dvorkin, Вы писали:
PD>Здравствуйте, okman, Вы писали:
O>>Или такое присваивание всегда безопасно, даже если сами указатели содержат null или "мусор"?
PD>Ответ в рамках существующих систем — безопасно.
Здравствуйте, okman, Вы писали:
O>Как думаете, возможно ли в C или C++ получить какой-нибудь побочный эффект во время O>присваивания одного указателя другому?
если приемник плохо выровнен, то это UB и на практике приводит к bus error
зы. я твой пример уже прочитал, зачётно. просто решил пополнить множество вариантов
Re[4]: Безопасно ли присваивать один указатель другому?..
Здравствуйте, zou, Вы писали:
zou>Здравствуйте, okman, Вы писали:
zou>На стандарт ссылку не дам, но, думаю, суть в том, что при присваивании p2 = p1 происходит не просто копирование, а преобразование адреса, для которого требуется прочитать RTTI. Если адрес p1 невалидный, то при обращении к RTTI происходит исключение. В случае валидного адреса p1, переменная p2 будет содержать адрес с неким смещением.
Как раз примерно это же хотел ответить.
При обычном (невиртуальном) наследовании все смещения при "хождении" по иерархии классов (т.е. при преобразовании указателей в пределах иерархии наследования) известны во время компиляции.
А вот при виртуальном наследовании смещение во время компиляции уже неизвестно и его нужно взять из таблицы смещений (или как она называется), к которой обращаемся через vptr, а в случае мертвого объекта vptr уже невалиден. Отсюда и падение.
Здравствуйте, zou, Вы писали:
zou>Здравствуйте, okman, Вы писали:
zou>На стандарт ссылку не дам, но, думаю, суть в том, что при присваивании p2 = p1 происходит не просто копирование, а преобразование адреса, для которого требуется прочитать RTTI. Если адрес p1 невалидный, то при обращении к RTTI происходит исключение. В случае валидного адреса p1, переменная p2 будет содержать адрес с неким смещением.
А если кастануть как-то типа (Base *)(void *)p2;? Какие тут могут быть подводные камни? Как я понимаю, для простой печати адреса объекта — так делать можно. Про RTTI я тоже понял, просто уж очень экзотический случай
Re[4]: Безопасно ли присваивать один указатель другому?..
Здравствуйте, SaZ, Вы писали:
SaZ>Честно прочитал все ссылки. Что-либо по моему вопросу будет?
Вроде выше, в моём ответе, было именно по твоему вопросу (по поводу void*).
SaZ>Интересует, например, чему должен быть равен sizeof указателя на метод?
sizeof указателя на метод — такой же, как sizeof(int*);
В общем — sizeof такой же, как у всех других указателей в приложениях данной архитектуры (то есть 4 байта для x86; 8 байт для x64).
class Foo
{
public:
int f(string str)
{
std::cout << "Foo::f()" << std::endl;
return 1;
}
};
int main()
{
int (Foo::*fptr) (string) = &Foo::f;
std::cout << "sizeof-fptr=" << sizeof(fptr) << std::endl;
...
return 0;
}
Здравствуйте, AlexGin, Вы писали:
AG> AG>sizeof указателя на метод — такой же, как sizeof(int*); AG>В общем — sizeof такой же, как у всех других указателей в приложениях данной архитектуры (то есть 4 байта для x86; 8 байт для x64).
Здравствуйте, AlexGin, Вы писали:
SaZ>>Честно прочитал все ссылки. Что-либо по моему вопросу будет? AG>Вроде выше, в моём ответе, было именно по твоему вопросу (по поводу void*).
SaZ>>Интересует, например, чему должен быть равен sizeof указателя на метод? AG> AG>sizeof указателя на метод — такой же, как sizeof(int*); AG>В общем — sizeof такой же, как у всех других указателей в приложениях данной архитектуры (то есть 4 байта для x86; 8 байт для x64).
это не так.
как минимум на старых студиях под 32 бита этот размер был 8.
с указателем на обычную функцию тоже никаких гарантий не было что можно безопасно к void* кастить.
Re[6]: Безопасно ли присваивать один указатель другому?..
Здравствуйте, night beast, Вы писали:
NB>Здравствуйте, AlexGin, Вы писали:
SaZ>>>Честно прочитал все ссылки. Что-либо по моему вопросу будет? AG>>Вроде выше, в моём ответе, было именно по твоему вопросу (по поводу void*).
SaZ>>>Интересует, например, чему должен быть равен sizeof указателя на метод? AG>> AG>>sizeof указателя на метод — такой же, как sizeof(int*); AG>>В общем — sizeof такой же, как у всех других указателей в приложениях данной архитектуры (то есть 4 байта для x86; 8 байт для x64).
NB>это не так.
Я здесь привёл примеры для MSVC2015 — всё вполне логично (размеры указателей одинаковы).
Здесь у меня под рукой нет другой студии (дома проверю под MSVC2008, но более древних не найду).
NB>как минимум на старых студиях под 32 бита этот размер был 8. NB>с указателем на обычную функцию тоже никаких гарантий не было что можно безопасно к void* кастить.
Где хоть какое-то объяснение данному феномену?
Указатель — это адрес в памяти.
Как он может иметь разные размерности в одном и том же процессе?
Re[7]: Безопасно ли присваивать один указатель другому?..