Pertanyaan Mengapa static_cast tidak dapat digunakan untuk down-cast ketika virtual inheritance dilibatkan?


Pertimbangkan kode berikut:

struct Base {};
struct Derived : public virtual Base {};

void f()
{
    Base* b = new Derived;
    Derived* d = static_cast<Derived*>(b);
}

Ini dilarang oleh standar ([n3290: 5.2.9/2]) sehingga kode tidak dikompilasi, karena Derived  sebenarnya mewarisi dari Base. Menghapus virtual dari warisan membuat kode valid.

Apa alasan teknis untuk aturan ini ada?


32
2017-09-20 12:10


asal


Jawaban:


Masalah teknisnya adalah tidak ada jalan keluar dari Base* apa offset antara awal Base sub-objek dan awal dari Derived obyek.

Dalam contoh Anda tampaknya OK, karena hanya ada satu kelas yang terlihat dengan Base dasar, dan sehingga tampak tidak relevan bahwa warisan itu virtual. Tetapi compiler tidak tahu apakah seseorang mendefinisikan yang lain class Derived2 : public virtual Base, public Derived {}, dan casting a Base* menunjuk pada Base subobjek itu. Secara umum [*], offset antara Base subobject dan Derived melakukan subobjek Derived2 mungkin tidak sama dengan mengimbangi antara Base subobject dan yang lengkap Derived objek dari objek yang sebagian besar berasal dari jenis Derived, justru karena Base hampir diwarisi.

Jadi tidak ada cara untuk mengetahui jenis dinamis dari objek lengkap, dan offset yang berbeda antara penunjuk yang telah Anda berikan kepada pemain, dan hasil yang diperlukan, bergantung pada jenis dinamis itu. Oleh karena itu para pemain tidak mungkin.

Anda Base tidak memiliki fungsi virtual dan karenanya tidak ada RTTI, jadi tentu saja tidak ada cara untuk memberi tahu tipe objek yang lengkap. Pemeran masih dilarang bahkan jika Base memang memiliki RTTI (saya tidak segera tahu mengapa), tapi saya kira tanpa memeriksa bahwa a dynamic_cast mungkin dalam kasus itu.

[*] Maksud saya, jika contoh ini tidak membuktikan intinya maka terus tambahkan lebih banyak warisan virtual sampai Anda menemukan kasus di mana offset berbeda ;-)


33
2017-09-20 12:30



Pertimbangkan fungsi berikut foo:

#include <iostream>

struct A
{
    int Ax;
};

struct B : virtual A
{
    int Bx;
};

struct C : B, virtual A
{
    int Cx;
};


void foo( const B& b )
{
    const B* pb = &b;
    const A* pa = &b;

    std::cout << (void*)pb << ", " << (void*)pa << "\n";

    const char* ca = reinterpret_cast<const char*>(pa);
    const char* cb = reinterpret_cast<const char*>(pb);

    std::cout << "diff " << (cb-ca) << "\n";
}

int main(int argc, const char *argv[])
{
    C c;
    foo(c);

    B b;
    foo(b);
}

Meskipun tidak benar-benar portabel, fungsi ini menunjukkan kepada kita "offset" dari A dan B. Karena kompilator dapat sangat liberal dalam menempatkan subobject A dalam hal pewarisan (juga ingat bahwa objek yang paling berasal memanggil kastor basis virtual!), penempatan sebenarnya tergantung pada jenis objek "nyata". Tapi karena foo hanya mendapat ref ke B, static_cast (yang bekerja pada waktu kompilasi dengan paling banyak menerapkan beberapa offset) pasti akan gagal.

ideone.com (http://ideone.com/2qzQu) output untuk ini:

0xbfa64ab4, 0xbfa64ac0
diff -12
0xbfa64ac4, 0xbfa64acc
diff -8

2
2017-09-20 12:48



Pada dasarnya, tidak ada alasan nyata, tetapi tujuannya adalah itu static_cast menjadi sangat murah, melibatkan paling banyak tambahan atau pengurangan konstanta ke penunjuk. Dan tidak ada cara untuk itu mengimplementasikan cast yang Anda inginkan dengan harga murah; pada dasarnya, karena posisi relatif dari Derived dan Base dalam objek dapat berubah jika ada tambahan warisan, konversi akan membutuhkan yang baik kesepakatan biaya overhead dynamic_cast; anggota komite mungkin berpikir bahwa ini mengalahkan alasan untuk menggunakan static_castdari pada dynamic_cast.


2
2017-09-20 12:55



static_cast adalah konstruk waktu kompilasi. ia memeriksa validitas cast pada waktu kompilasi dan memberikan kesalahan kompilasi jika cast tidak valid.

virtualism adalah fenomena runtime.

Keduanya tidak bisa pergi bersama.

C ++ 03 Standar §5.2.9 / 2 dan §5.2.9 / 9 relevan dalam kasus ini.

Nilai dari tipe "pointer ke cv1 B", di mana B adalah tipe kelas, dapat dikonversi ke rvalue dari tipe "pointer ke cv2 D", di mana D adalah kelas yang diturunkan (klausa 10) dari B, jika standar yang valid konversi dari "pointer ke D" menjadi "pointer to B" ada (4.10), cv2 adalah cv-kualifikasi yang sama dengan, atau kualifikasi -cv yang lebih besar daripada, cv1, dan B bukan kelas dasar virtual D. Nilai penunjuk null (4.10) dikonversi ke nilai pointer null dari tipe tujuan. Jika rvalue dari tipe “pointer to cv1 B” menunjuk ke B yang sebenarnya adalah sub-objek dari suatu objek tipe D, pointer yang dihasilkan menunjuk ke objek terlampir tipe D. Jika tidak, hasil dari cast tidak terdefinisi. .


1
2017-09-20 12:15



Saya kira, ini karena kelas dengan warisan virtual memiliki tata letak memori yang berbeda. Orang tua harus dibagi antara anak-anak, oleh karena itu hanya satu dari mereka yang dapat ditata terus menerus. Itu berarti, Anda tidak dijamin dapat memisahkan area memori terus-menerus untuk memperlakukannya sebagai objek turunan.


1
2017-09-20 12:26



static_cast hanya dapat melakukan gips di mana tata letak memori antara kelas dikenal pada saat kompilasi. dynamic_cast dapat memeriksa informasi pada saat run-time, yang memungkinkan untuk lebih akurat memeriksa ketepatan cor, serta membaca informasi run-time mengenai tata letak memori.

Virtual inheritance menempatkan informasi run-time ke setiap objek yang menentukan apa tata letak memori antara Base dan Derived. Apakah tepat setelah yang lain atau apakah ada celah tambahan? Karena static_cast tidak dapat mengakses informasi seperti itu, kompilator akan bertindak secara konservatif dan hanya memberikan kesalahan kompiler.


Lebih detail:

Pertimbangkan struktur warisan yang kompleks, di mana - karena multiple inheritance - ada banyak salinan Base. Skenario yang paling khas adalah warisan berlian:

class Base {...};
class Left : public Base {...};
class Right : public Base {...};
class Bottom : public Left, public Right {...};

Dalam skenario ini Bottom terdiri dari Left dan Right, dimana setiap memiliki salinannya sendiri Base. Struktur memori dari semua kelas di atas dikenal pada waktu kompilasi dan static_cast dapat digunakan tanpa masalah.

Mari kita sekarang mempertimbangkan struktur serupa tetapi dengan warisan virtual Base:

class Base {...};
class Left : public virtual Base {...};
class Right : public virtual Base {...};
class Bottom : public Left, public Right {...};

Menggunakan warisan virtual memastikan kapan Bottom dibuat, hanya mengandung satu salinan dari Base itu adalah bersama antara bagian-bagian objek Left dan Right. Tata letak Bottom objek bisa misalnya:

Base part
Left part
Right part
Bottom part

Sekarang, anggaplah Anda melakukan cast Bottom untuk Right (Itu adalah pemain yang sah). Anda mendapatkan Right pointer ke objek yang dalam dua bagian: Base dan Right memiliki celah memori di antaranya, mengandung (sekarang-tidak relevan) Left bagian. Informasi tentang celah ini disimpan pada saat run-time dalam bidang tersembunyi Right(biasanya disebut sebagai vbase_offset). Anda dapat membaca detailnya misalnya sini.

Namun, jeda tidak akan ada jika Anda hanya akan membuat yang berdiri sendiri Right obyek.

Jadi, jika saya memberi Anda hanya penunjuk Right Anda tidak tahu pada waktu kompilasi jika itu adalah objek yang berdiri sendiri, atau bagian dari sesuatu yang lebih besar (misalnya Bottom). Anda perlu memeriksa informasi run-time untuk mentransmisikan dengan benar Right untuk Base. Itulah mengapa static_cast akan gagal dan dynamic_cast tidak akan.


Catatan pada dynamic_cast:

Sementara static_cast tidak menggunakan informasi run-time tentang objek, dynamic_cast menggunakan dan membutuhkan itu ada! Dengan demikian, pemeran yang terakhir hanya dapat digunakan pada kelas-kelas yang mengandung setidaknya satu fungsi virtual (misalnya perusak virtual)


0
2018-06-09 21:30