1

Тема: Перетворення масива наслідників до базового?

Вітаю!

В ході вивчення С++, та написання власної програми зіткнувся з такою ситуацією:
є два класи class base (батько) і class derived (нащадок);
є деяка віртуальна функція в class base, яка приймає як параметр масив об'єктів класу f(base **arr);
щоб скористатися цією функцією для класу нащадка, необхідно привести масив нащадків класу до масиву батьківського;
Масиви: base *baseArr[100], derived *derivedArr[100]

Як таке зробити?

2

Re: Перетворення масива наслідників до базового?

mimik написав:

Вітаю!
віртуальна функція в class base, яка приймає як параметр масив об'єктів класу f(base **arr);

а можна трохи більше деталей чому такий дизайн? щось воно не виглядає здорово, може ми щось краще придумаємо

Подякували: koala, leofun012

3

Re: Перетворення масива наслідників до базового?

Чому такий дизайн? Ну, просто так. Я якось переписував код з книжки де пояснювали класи, наслідування.. потім дещо пробував для себе, щоб розібратися, ніякого мета-значення тут немає, швидше просто цікаво як це зробити.

4

Re: Перетворення масива наслідників до базового?

Просто зберігайте посилання на нащадків у масиві посилань на предків.

5

Re: Перетворення масива наслідників до базового?

https://ideone.com/SwfjLR

Подякували: mimik1

6

Re: Перетворення масива наслідників до базового?

Так, таке я вже бачив. Мене швидше цікавить чому я не можу зробити отак:
https://ideone.com/2B56Yy

7

Re: Перетворення масива наслідників до базового?

mimik написав:

Мене швидше цікавить чому я не можу зробити отак:

Бо це UB.
https://rextester.com/IKSXJW90373

Подякували: mimik1

8

Re: Перетворення масива наслідників до базового?

Success

Гм. Цікаве питання. І чому ж?

Подякували: mimik, leofun012

9

Re: Перетворення масива наслідників до базового?

wander, так воно працює ж, а якщо написати отак:
f(reinterpret_cast<Base*>(derived_arr));
вже ні

10 Востаннє редагувалося wander (03.02.2020 17:02:40)

Re: Перетворення масива наслідників до базового?

koala написав:

Гм. Цікаве питання. І чому ж?

Буде порушення strict aliasing rule(s) при спробі читання.
І нема гарантії, що layout структури буде таким яким ми його очікуємо.

https://rextester.com/KJD4595

mimik написав:

так воно працює ж

На те воно і UB.

mimik написав:

а якщо написати отак:

І чого ви цим хотіли добитися? Вам навіть компілятор по вашій же ссилці все явно сказав

prog.cpp: In function ‘int main()’:
prog.cpp:27:4: error: cannot convert ‘Base*’ to ‘Base**’
  f(reinterpret_cast<Base*>(derived_arr));
    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
prog.cpp:18:15: note:   initializing argument 1 of ‘void f(Base**)’
void f(Base **base_arr) {
        ~~~~~~~^~~~~~~~

Подякували: mimik1

11

Re: Перетворення масива наслідників до базового?

Мені зараз ліньки лізти в стандарт, але яким чином два масиви посилань можуть розкластися в пам'яті по-різному?

Подякували: mimik1

12

Re: Перетворення масива наслідників до базового?

wander, так, туплю. Я скопіював ваш код собі, а потім намагався зрозуміти, що ви там зробили, тому пробував, що в голову приходило.

13 Востаннє редагувалося wander (03.02.2020 18:01:02)

Re: Перетворення масива наслідників до базового?

koala написав:

яким чином два масиви посилань можуть розкластися в пам'яті по-різному?

Не самі ж масиви нас цікавлять, а як ми з ними працюємо. Ми спочатку робимо reinterpret_cast, який (1) насправді нічого не кастить і (2) не враховує layout класу:

http://eel.is/c++draft/class.mem#19 написав:

[ Note: Non-static data members of a (non-union) class with the same access control and non-zero size are allocated so that later members have higher addresses within a class object. The order of allocation of non-static data members with different access control is unspecified. Implementation alignment requirements might cause two adjacent members not to be allocated immediately after each other; so might requirements for space for managing virtual functions and virtual base classes. — end note ]

(2) Тобто звідси, може бути:

struct B {
    // мембери B
};
struct D {
    B base;
    // мембери D
};

Може бути і так:

struct B {
    // мембери B
};
struct D {
    // мембери D
    B base;
};

Чи так:

struct B {
    // мембери B
};
struct D {
    // мембери D
    B base;
    // мембери D
};

Приклад з поліморфізмом - просто окремий випадок.

(1) Звідси висновок, що ці типи також не є similar.

http://eel.is/c++draft/expr.add#6 написав:

For addition or subtraction, if the expressions P or Q have type “pointer to cv T”, where T and the array element type are not similar, the behavior is undefined. [ Note: In particular, a pointer to a base class cannot be used for pointer arithmetic when the array contains objects of a derived class type. — end note ]

Тут же ж і порушення strict aliasing rule(s) при спробі читання

http://eel.is/c++draft/basic.lval#11 написав:

[...] If a program attempts to access the stored value of an object through a glvalue whose type is not similar to one of the following types the behavior is undefined [...]

Подякували: mimik, leofun012

14

Re: Перетворення масива наслідників до базового?

Я тепер нічого не розумію.

15

Re: Перетворення масива наслідників до базового?

Навіть, допустимо, ви чудово знаєте свою платформу, та добре знаєте, як у вас реалізується layout, vtables (і їх розміщення), paddings, etc. То, ви можете не вистрілити собі ніде в ногу і це навіть працюватиме стабільно, але є ще один підводний камінь, що якщо після розіменування B** ми можемо отримати B*, який вказує вже не на потрібний нам derived, а на якогось іншого спадкоємця?

#include <iostream>

struct B {    
    virtual ~B() {}
    virtual void print() = 0;
};
 
struct D : B {
    int i;
    
    void print() override { std::cout << "[D::print] j = " << i << '\n'; }
    virtual void do_nothing() { std::cout << "[D::do_nothing]\n"; }
};
 
struct XXX : B {    
    void print() override { }
    virtual void hack() { std::cout << "[XXX::hack]\n"; }
};
 
int main() {
 
    D* dparr[2]{new D, new D};
    
    dparr[1]->do_nothing();
    
    B** bpp = (B**)dparr; // same as reinterpret_cast<B**>(dparr)
    XXX hacker;
    bpp[1] = &hacker;
    
    dparr[1]->do_nothing(); // calls XXX::hack 
}

https://rextester.com/UTZEY51315

mimik написав:

Я тепер нічого не розумію.

Коротше кажучи, на більшості сучасних платформ та компіляторів код з https://rextester.com/IKSXJW90373 - працюватиме, бо загалом розробники компіляторів теж не дурні, а на більшості систем layout буде очікуваним (тобто у вас UB лише формально, на ділі всеодно все працюватиме), але якщо ви пишете, наприклад, бібліотеку, якою можуть користуватися хтозна де, то боронь Боже вас такий код писати)

Подякували: mimik1

16

Re: Перетворення масива наслідників до базового?

У нас відбувається тут два перетворення:
- reinterpret_cast масиву вказівників на масив інших вказівників. Ніяких проблем з лейаутом тут бути не може, правильно? Це прості типи одного розміру.
- використання вказівників з масиву як вказівників на базовий клас. І тут стандарт каже, що це можливо:

англійська

7.3.11 Pointer conversions[conv.ptr]
A prvalue of type “pointer to cv D”, where D is a complete class type, can be converted to a prvalue of type “pointer to cv B”, where B is a base class ([class.derived]) of D. If B is an inaccessible ([class.access]) or ambiguous ([class.member.lookup]) base class of D, a program that necessitates this conversion is ill-formed. The result of the conversion is a pointer to the base class subobject of the derived class object. The null pointer value is converted to the null pointer value of the destination type.
http://eel.is/c++draft/conv.ptr

Подякували: leofun011

17 Востаннє редагувалося wander (03.02.2020 18:44:25)

Re: Перетворення масива наслідників до базового?

koala написав:

І тут стандарт каже, що це можливо:
A prvalue of type “pointer to cv D”, where D is a complete class type, can be converted to a prvalue of type “pointer to cv B”

В нас там немає convertion. Ми не отримуємо інший тип.
Уявімо, що базовий клас - член похідного.
Після static_cast у нас інший тип. Ми тоді читаємо з об'єкта іншого типу. Саме тому static_cast - це convertion чи cast. В його результаті ми працюємо з іншим об'єктом. А reinterpret_cast - це не каст, в його результаті об'єкт не змінюється.

Подякували: koala, mimik, leofun013

18 Востаннє редагувалося koala (03.02.2020 18:43:27)

Re: Перетворення масива наслідників до базового?

Я був неправий. Наявність статичного перетворення не робить це перетворення тотожним; наприклад, при множинному успадкуванні у класу є два батьківських, один з яких розташований гарантовано з певним зсувом. Перетворення банальне (додати цей зсув), і компілятор його легко робить через static_cast чи взагалі неявно; але перетворення, зроблене через reinterpret_cast, поламає типи.

Гадаю, відповідь на початкове питання має бути такою: масиви в C++ не є "громадянами першого класу", деякі операції з ними заборонені. Це - свідома частина дизайну мови; масиви потребують тривалої обробки, і така дія має бути прямо виражена програмістом (цикл по елементах, рекурсія, бінарний пошук, щось іще залежно від конкретного типу), а не підставлятися компілятором. Вам тут треба, фактично, додати до кожного вказівника певне зміщення (зазвичай нульове, але в загальному випадку може бути і іншим); це не буде зроблено автоматично.

Подякували: wander, mimik, leofun013

19

Re: Перетворення масива наслідників до базового?

Отут ще є стаття, власне, звідки мій попередній код і є.

англійська

Because converting Derived** → Base** would be invalid and dangerous.

Проте, там зачеплена лише верхівка айсбергу)

Подякували: mimik, leofun012

20 Востаннє редагувалося wander (03.02.2020 18:57:12)

Re: Перетворення масива наслідників до базового?

koala написав:

додати зсув і компілятор його легко робить через static_cast чи взагалі неявно

Я це собі уявляю десь так:

/* Емуляція наслідування */

struct B {};
struct D /* : B */ {
    vptr_t__* vptr;
    B         base;
};
D* d = ...
B* p = (B*)((unsigned char*)d + sizeof(vptr_t__)); // &d->base  // static_cast<B*>(d) 

reinterpret_cast же нічого цього не робить.

Подякували: koala, mimik2