Не прошло и года (на самом деле прошло), как я решил написать вторую статью по программированию! Речь пойдёт об одном интересном поведении указателей в C++. Оно с одной стороны логичное, а с другой не совсем очевидное.

Очевидное-невероятное

Думаю, многие согласятся с тем, что следующее утверждение верно:

(Child *)(void *)(Parent *)&child == &child

А зря.

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

4.10.2

A prvalue of type “pointer to cv T”, where T is an object type, can be converted to a prvalue of type “pointer to cv void”. The result of converting a non-null pointer value of a pointer to object type to a “pointer to cv void” represents the address of the same byte in memory as the original pointer value. …

4.10.3

A prvalue of type “pointer to cv D”, where D is a class type, can be converted to a prvalue of type “pointer to cv B”, where B is a base class (Clause 10) of D. … The result of the conversion is a pointer to the base class subobject of the derived class object. …

Из этих нескольких «очень понятных» предложений можно сделать вывод почему правильный ответ на исходный вопрос: иногда. И предположить в каких случаях ответ: нет.

Множественное наследование

Первый такой случай связан с множественным наследованием.

Рассмотрим пару классов и их общего ребёнка:

struct Mother {
  size_t gene_x;
};

struct Father {
  size_t gene_y;
};

struct Child: Father, Mother {};

Внутри ребёнка есть копия каждого из его родителей. Таким образом, указатель на объект класса Child и указатель после его приведения к указателю на Father должны смотреть в одну и ту же область памяти, так как данные отца уже и так находятся в самом начале класса ребёнка. Попытка же привести его к указателю на класс Mother вызывает необходимость смещения указателя, иначе новый указатель будет по прежнему указывать на данные отца, а не матери:

Виртуальные методы

Второй случай вообще говоря зависит от компилятора, а точнее от соглашения о вызове виртуальных функций. Рассмотрим класс:

struct HolyChild: Mother {
  virtual void make_good() {}
};

Указатель на этот класс так же может себя «странно» вести. Дополнительную странность вводит тот факт, что удаление virtual в HolyChild или добавление виртуального метода в класс Mother эту «странность» устраняет. Связанно это с тем, что большинство компиляторов добавляют в самое начало класса указатель на таблицу виртуальных методов (если я не ошибаюсь, этого требует Itanium ABI, реализованный многими компиляторами). В базовом классе нет виртуальных методов, а значит и нет дополнительного указателя в начале. В результате выходит, что данные класса потомка начинаются раньше чем данные класса предка. И снова необходимо смещение указателя при приведении типа:

Заключение

Таким образом в исходном утверждении при преобразовании через void * теряется информация о типе (4.10.2), и при приведении указателя обратно к одному из вроде бы легальных типов он будет приведён к нему без смещения (компилятор верит программисту на слово, что этот адрес и есть адрес данных необходимого класса). То есть значение выражения:

(Child *)(Parent *)&child == &child

всегда true, в то время как значение:

(Child *)(void *)(Parent *)&child == &child

зависит от структуры классов и используемого компилятора, и в общем случае может быть как true так и false.

Всё вышесказанное может быть легко продемонстрировано небольшой программкой.