Pertanyaan Mengapa perilaku tidak terdefinisi untuk menghapus [] array objek yang diturunkan melalui pointer basis?


Saya menemukan potongan berikut di C ++ 03 Standard di bawah 5.3.5 [expr.delete] p3:

Dalam alternatif pertama (hapus objek), jika jenis statis objek yang akan dihapus berbeda dari jenis dinamisnya, jenis statis akan menjadi kelas dasar dari tipe dinamis operand dan tipe statis harus memiliki destruktor virtual atau perilaku tidak terdefinisi. Di alternatif kedua (hapus larik) jika jenis dinamis objek yang akan dihapus berbeda dari jenis statisnya, perilaku tidak terdefinisi.


Ulasan cepat pada jenis statis dan dinamis:

struct B{ virtual ~B(){} };
struct D : B{};

B* p = new D();

Tipe statis dari p aku s B*, sedangkan tipe dinamis *p aku s D, 1.3.7 [defns.dynamic.type]:

[Contoh: jika pointer p yang tipe statisnya adalah "penunjuk ke class B”Menunjuk ke objek class D, berasal dari B, jenis ekspresi dinamis *p aku s "D. "]


Sekarang, melihat kutipan di atas lagi, ini berarti bahwa kode follwing memanggil perilaku tidak terdefinisi jika saya mendapatkan hak itu, terlepas dari keberadaan virtual perusak:

struct B{ virtual ~B(){} };
struct D : B{};

B* p = new D[20];
delete [] p; // undefined behaviour here

Apakah saya salah paham kata-kata dalam standar entah bagaimana? Apakah saya mengabaikan sesuatu? Mengapa standar tersebut menetapkan perilaku tidak terdefinisi ini?


32
2018-05-30 02:35


asal


Jawaban:


Base* p = new Base[n] menciptakan sebuah n-ukuran array Base elemen, yang p kemudian menunjuk ke elemen pertama. Base* p = new Derived[n] namun, membuat n-ukuran array Derived elemen. p kemudian menunjuk ke Base  subobject dari elemen pertama. p tidak tidak Namun lihat yang pertama elemen dari array, yang merupakan apa yang valid delete[] p ekspresi membutuhkan.

Tentu saja akan mungkin untuk memberi mandat (dan kemudian menerapkan) itu delete [] p Apakah Hal yang Tepat dalam kasus ini. Tapi apa yang dibutuhkan? Implementasi harus berhati-hati untuk entah bagaimana mengambil tipe elemen dari array, dan kemudian secara moral dynamic_cast  p untuk jenis ini. Maka itu masalah melakukan polos delete[] seperti yang sudah kami lakukan.

Masalahnya adalah bahwa ini akan dibutuhkan setiap saat sebuah array dari tipe elemen polimorfik, terlepas dari apakah polimorfisme digunakan pada tidak. Menurut pendapat saya, ini tidak sesuai dengan filosofi C ++ dari tidak membayar untuk apa yang tidak Anda gunakan. Tapi yang lebih buruk: sebuah polimorfik-diaktifkan delete[] p hanya tidak berguna karena p hampir tidak berguna dalam pertanyaanmu. p adalah penunjuk ke subobject dari elemen dan tidak lebih; itu jika tidak benar-benar tidak terkait dengan array. Anda tentu tidak bisa melakukannya p[i] (untuk i > 0) dengan itu. Jadi itu tidak masuk akal delete[] p tidak berfungsi.

Untuk menyimpulkan:

  • array sudah memiliki banyak kegunaan yang sah. Dengan tidak mengizinkan array berperilaku polimorfik (baik secara keseluruhan atau hanya untuk delete[]) ini berarti bahwa array dengan tipe elemen polimorfik tidak dihukum untuk penggunaan yang sah, yang sejalan dengan filosofi C ++.

  • jika di sisi lain array dengan perilaku polimorfik diperlukan, mungkin untuk menerapkannya dalam hal apa yang sudah kita miliki.


30
2018-05-30 03:17



Ini salah untuk memperlakukan array-of-derived sebagai array-of-base, tidak hanya saat menghapus item. Misalnya bahkan hanya mengakses elemen biasanya akan menyebabkan bencana:

B *b = new D[10];
b[5].foo();

b[5] akan menggunakan ukuran B untuk menghitung lokasi memori mana yang akan diakses, dan jika B dan D memiliki ukuran yang berbeda, ini tidak akan mengarah pada hasil yang diinginkan.

Sama seperti a std::vector<D> tidak dapat dikonversi menjadi std::vector<B>, sebuah penunjuk ke D[] seharusnya tidak dapat dikonversi menjadi a B*, tetapi untuk alasan historis itu tetap kompilasi. Jika sebuah std::vector akan digunakan sebagai gantinya, itu akan menghasilkan kesalahan waktu kompilasi.

Ini juga dijelaskan dalam Jawaban C + + FAQ Lite tentang topik ini.

Begitu delete menyebabkan perilaku tidak terdefinisi dalam kasus ini karena sudah salah untuk memperlakukan array dengan cara ini, meskipun sistem jenis tidak dapat menangkap kesalahan.


11
2018-05-30 03:26



Hanya untuk menambah jawaban yang sangat baik sth - Saya telah menulis contoh singkat untuk mengilustrasikan masalah ini dengan offset yang berbeda.

Perhatikan bahwa jika Anda mengomentari anggota m_c dari kelas Turun, operasi penghapusan akan berfungsi dengan baik.

Tepuk tangan,

Orang.

#include <iostream>
using namespace std;

class Base 
{

    public:
        Base(int a, int b)
        : m_a(a)
        , m_b(b)    
        {
           cout << "Base::Base - setting m_a:" << m_a << " m_b:" << m_b << endl;
        }

        virtual ~Base()
        {
            cout << "Base::~Base" << endl;
        }

        protected:
            int m_a;
            int m_b;
};


class Derived : public Base
{
    public:
    Derived() 
    : Base(1, 2) , m_c(3)   
    {

    }

    virtual ~Derived()
    {
        cout << "Derived::Derived" << endl;
    }

    private:    
    int m_c;
};

int main(int argc, char** argv)
{
    // create an array of Derived object and point them with a Base pointer
    Base* pArr = new Derived [3];

    // now go ahead and delete the array using the "usual" delete notation for an array
    delete [] pArr;

    return 0;
}

1
2017-10-16 07:30



IMHO ini ada hubungannya dengan pembatasan array untuk menangani konstruktor / destruktor. Perhatikan itu, kapan new[] disebut, pasukan kompilator untuk instantiate saja konstruktor default. Dengan cara yang sama kapan delete[] disebut, kompilator mungkin mencari hanya destruktor dari tipe statis pemanggil yang memanggil.

Sekarang dalam kasus virtual destruktor, destructor kelas Berasal harus disebut pertama diikuti oleh kelas Base. Karena untuk compiler array mungkin melihat tipe statis objek panggilan (di sini Basis) jenis, mungkin berakhir hanya memanggil Base destructor; yaitu UB.

Karena itu, belum tentu UB untuk semua penyusun; mengatakan misalnya gcc memanggil destructor dengan urutan yang benar.


0
2018-05-30 03:02



saya berpikir semuanya bermuara pada prinsip zero-overhead. yaitu bahasa tidak mengizinkan penyimpanan informasi tentang jenis elemen dinamis dari larik.


0
2018-05-30 02:57