Разрешено ли е на указател да променя стойността при единично наследяване?

Знам, че при множествено наследяване стойността на указателя може да се променя. Но такъв ли е случаят и с единичното наследяване? Или с POD видове за този въпрос?

Вероятно знаете класическия пример:

#include <iostream>
using std::cout;
struct Base1 { virtual void f1() {} };
struct Base2 { virtual void f2() {} };
struct Derived : public Base1, public Base2 { virtual void g() {} };
int main() {
    Derived d{};
    auto *pd = &d;
    auto pb1 = (Base1*)pd;
    auto pb2 = (Base2*)pd;
    cout << pd << "\n"; // say, 0x1000
    cout << pb1 << "\n"; // say, 0x1000
    cout << pb2 << "\n"; // say, 0x1008 !!!
}

Дотук добре и това е добрата стара компилаторска практика. Обектите са разположени по начин, по който "коренът" на Base2 има отместване до Derived, което може да бъде отпечатано, когато се гледа действителната стойност на показалеца. Това не е проблем, когато се въздържате от извършване на void* и reinterpret_casts.

И доколкото знам на практика това се случва само при множествено наследяване.

Но въпросът ми: Какво казва стандартът за „промяна на стойности на показалеца по време на прехвърляне“? Може ли това да се случи само с множествено наследяване или може да се случи и с единично наследяване или предаване на POD?


person towi    schedule 24.02.2017    source източник
comment
Прост контрапример за не-POD типове: struct Base1 { int x; }; struct Derived : Base1 { virtual void f(){} };.   -  person Raymond Chen    schedule 25.02.2017
comment
@RaymondChen: Може да е добре да посочите за кой компилатор това е контрапример.   -  person Ben Voigt    schedule 25.02.2017
comment
Това е контрапример в компилаторите, които поставят vtable в началото на обекта. Повечето компилатори правят това, защото се изисква от много ABI. (Единственото изключение, за което знам е, че някои версии на Borland C++ поставят vtable в края на обекта.)   -  person Raymond Chen    schedule 25.02.2017


Отговори (2)


Ако типът е със стандартно оформление, тогава указател към съдържащата се структура на всяко ниво на наследяване е равен на указател към първия елемент. По транзитивност указателите към всички нива на наследяване също са едни и същи -- и стандартът продължава напред и също прави тази гаранция в странния случай, когато не съществуват нестатични членове на данни.

(Имайте предвид, че за да бъде със стандартно оформление, типът може да въведе членове на данни само на едно ниво в диаграмата на наследяване -- допълнителни подкласове могат да добавят функции и да останат стандартно оформление, но не могат да добавят повече данни).

Ако типът не е със стандартно оформление, тогава не се дават гаранции и трябва да използвате static_cast, а не reinterpret_cast (нито еквиваленти на reinterpret_cast, като например множество static_cast с void* като междинно), за да включите правилно каквото и да е отместване.

Разбира се, всички POD типове са със стандартно оформление, PODness изисква както стандартно оформление, така и тривиалност.

Точното правило, което обяснявам, се намира в раздел 9.2:

Ако обект на клас със стандартно оформление има нестатични членове с данни, неговият адрес е същият като адреса на първия му нестатичен член с данни. В противен случай неговият адрес е същият като адреса на неговия първи субобект от базов клас (ако има такъв).

[ Бележка: Следователно може да има неименувано подпълване в структурен обект със стандартно оформление, но не и в началото му, както е необходимо за постигане на подходящо подравняване. — крайна бележка]

[ Забележка: Обектът и неговият първи подобект са взаимно конвертируеми указател. — крайна бележка]

Правилото, което изрично посочва, че странните оформления са възможни за типове, които не са стандартни, е в раздел 10:

Редът, в който подобектите на базовия клас са разпределени в най-производния обект, не е определен.

person Ben Voigt    schedule 24.02.2017
comment
Само за да сте сигурни: class Base { int i; }; class Derived : public Base { int j; }; нямат стандартно оформление (и двете добавят нестатични членове на данните). И следователно техните указатели могат да имат различни стойности. нали - person towi; 25.02.2017
comment
@towi: правилно и в двете точки, въпреки че никога не съм виждал компилатор, който действително използва това разрешение и подравнява такъв прост случай при ненулево отместване - person Ben Voigt; 25.02.2017

Разрешено ли е на указател да променя стойността при единично наследяване?

Не мога да намеря нищо в стандарта, което би попречило на изпълнението да направи това.

Намерих следното в стандарта C++11, което е свързано с преобразуване на указатели между указатели към базови класове и производни класове:

4.10 Преобразуване на указател

3 Първозначение от тип „указател към cv D“, където D е тип клас, може да бъде преобразувано в първозначение от тип „указател към cv B“, където B е базов клас (клауза 10) на D. Ако B е недостъпен (клауза 11) или двусмислен (10.2) базов клас на D, програма, която налага това преобразуване, е неправилно оформена. Резултатът от преобразуването е указател към субобекта на основния клас на обекта на производния клас. Стойността на нулевия указател се преобразува в стойността на нулевия указател на типа дестинация.

и

5.2.9 Статично предаване

2 l-стойност от тип “cv1 B,” където B е тип клас, може да бъде преобразувана в тип “препратка към cv2 D,” където D е производен клас ( Клауза 10) от B, ако съществува валидно стандартно преобразуване от „указател към D“ в „указател към B“ (4.10), cv2 е същата cv-квалификация като или по-голяма cv-квалификация от , cv1 и B не е нито виртуален базов клас на D, нито базов клас на виртуален базов клас на D. Резултатът има тип „cv2 D.“ Xvalue от тип “cv1 B” може да бъде преобразуван към тип “rvalue препратка към cv2 D” със същите ограничения като за lvalue от тип “cv1< /em> B.“ Ако обектът от тип „cv1 B“ всъщност е подобект на обект от тип D, резултатът се отнася до обхващащия обект от тип D. В противен случай резултатът от каста е недефиниран. [ Пример:

struct B { };
struct D : public B { };
D d;
B &br = d;
static_cast<D&>(br); // produces lvalue to the original d object

краен пример ]


дадени

class B { ... };
class D : public B { ... };

Реализацията е свободна да избере следното оформление:

+------------------+
|  D members       |
+------------------+
|  B members       |
+------------------+

стига да прави правилното нещо при преобразуване на указатели и препратки.

При такова изпълнение,

D d;
D* dptr = &d;
B* bptr = &d;

Стойностите на dptr и bptr ще бъдат различни.

person R Sahu    schedule 24.02.2017
comment
Благодаря. Признавам, че не мога да прочета 5.2.9 по начин, по който мога да разбера, че вашият пример е покрит от него. Трябва да ви повярвам, че вашият пример отговаря на този параграф. Предполагам, че си сигурен... - person towi; 25.02.2017
comment
@towi, 5.2.9 разчита на 4.10. ако съществува валидно стандартно преобразуване от „указател към D“ в „указател към B“ (4.10) Докато D* указател може да бъде преобразуван в B*, 5.2.9 е добре дефиниран. - person R Sahu; 25.02.2017
comment
Този отговор е наполовина грешен. Той не се отнася до POD, за който беше зададен въпросът, или стандартно оформление, което налага различни изисквания. И не мога да намеря нищо в Стандарта е куцо извинение, когато точният текст и местоположението на много подходящо правило са дадени от по-стар отговор. - person Ben Voigt; 25.02.2017
comment
Дори изявленията, които правите, свързани с вашия пример, може да са напълно несъответстващи, в зависимост от това какво крие .... - person Ben Voigt; 25.02.2017
comment
@BenVoigt, моля, разяснете. - person R Sahu; 25.02.2017