Dlaczego niezdefiniowanym zachowaniem jest usuwanie [] tablicy pochodnych obiektów za pomocą wskaźnika bazowego?

Znalazłem następujący fragment w standardzie C++03 pod 5.3.5 [expr.delete] p3:

W pierwszej alternatywie (usuń obiekt ), jeśli Typ statyczny usuwanego obiektu różni się od jego typu dynamicznego, Typ statyczny jest klasą bazową typu dynamicznego, A Typ statyczny ma wirtualny Destruktor lub zachowanie jest niezdefiniowane. w drugiej alternatywie (delete array) jeśli dynamiczny Typ usuwanego obiektu różni się ze względu na typ statyczny zachowanie jest nieokreślone.


Szybki przegląd typów statycznych i dynamicznych:

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

B* p = new D();

Typ statyczny p to B*, natomiast typ dynamiczny *p to D, 1.3.7 [defns.dynamic.type]:

[przykład: jeśli wskaźnik p, którego typ Statyczny to " wskaźnik do class B", wskazuje na obiekt class D, pochodzący z B, dynamiczny typ wyrażenia *p to "D."]


Teraz, patrząc na cytat w na początku oznaczałoby to, że kod follwing wywołuje nieokreślone zachowanie, jeśli dobrze zrozumiałem, niezależnie od obecności destruktora virtual:

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

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

Czy źle zrozumiałam sformułowanie w standardzie? Czy coś przeoczyłem? Dlaczego standard określa to jako nieokreślone zachowanie?

Author: Brian Tompsett - 汤莱恩, 2011-05-30

5 answers

Base* p = new Base[n] tworzy tablicę n O rozmiarze Base elementów, z których p wskazuje na pierwszy element. Base* p = new Derived[n] tworzy jednak tablicę n O rozmiarze Derived elementów. p następnie wskazuje na Base podobiekt pierwszego elementu . p Czy Nie odnosi się jednak do pierwszego elementu tablicy, czego wymaga poprawne wyrażenie delete[] p.

Oczywiście byłoby możliwe zlecić (a następnie wdrożyć), że delete [] p robi to, co należy™ w tym przypadku. Ale co by to wymagało? Implementacja musiałaby zadbać o to, aby w jakiś sposób odzyskać Typ elementu tablicy, a następnie moralnie dynamic_cast p do tego typu. Więc to kwestia zrobienia zwykłego delete[], Jak już to robimy.

Problem z tym polega na tym, że będzie to potrzebne za każdym razem tablica typu elementu polimorficznego, niezależnie od tego, czy polimorfizm jest używany na not. Moim zdaniem nie pasuje to do filozofii C++ nie płacenia za to co nie używasz. Ale co gorsza: polimorficzny delete[] p jest po prostu bezużyteczny, ponieważ {[3] } jest prawie bezużyteczny w twoim pytaniu. p jest wskaźnikiem do podobiektu elementu i nie więcej; poza tym jest całkowicie niezwiązany z tablicą. Z pewnością nie można zrobić p[i] (dla i > 0) z nim. Więc nie jest nierozsądne, że delete[] p nie działa.

Podsumowując:

  • Macierze mają już wiele legalnych zastosowań. Nie pozwalając tablicom zachowywać się polimorficznie (jako całość lub tylko dla delete[]) oznacza to, że tablice z typem elementu polimorficznego nie są karane za te legalne zastosowania, co jest zgodne z filozofią C++.

  • Jeśli z drugiej strony potrzebna jest tablica z zachowaniem polimorficznym, można ją zaimplementować w kategoriach tego, co już mamy.

 31
Author: Luc Danton,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-05-30 03:17:45

Źle jest traktować tablicę pochodną jako tablicę bazy, nie tylko podczas usuwania elementów. Na przykład nawet samo uzyskanie dostępu do elementów Zwykle spowoduje katastrofę:

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

b[5] Użycie rozmiaru B do obliczenia lokalizacji pamięci, do której należy uzyskać dostęp, a jeśli B i D mają różne rozmiary, nie doprowadzi to do zamierzonych wyników.

Podobnie jak std::vector<D> nie można przekształcić na std::vector<B>, wskaźnik na D[] nie powinien być zamieniany na B*, ale dla powody, dla których i tak się kompiluje. Jeśli zamiast tego zostanie użyta std::vector, spowoduje to błąd w czasie kompilacji.

Jest to również wyjaśnione w C++ FAQ Lite odpowiedzi na ten temat.

Więc delete powoduje nieokreślone zachowanie w tym przypadku, ponieważ już jest złe traktowanie tablicy w ten sposób, nawet jeśli system typów nie może wychwycić błędu.

 12
Author: sth,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-12-28 17:17:54

Aby dodać do doskonałej odpowiedzi z sth - napisałem krótki przykład, aby zilustrować ten problem z różnymi przesunięciami.

Zauważ, że jeśli skomentujesz m_c klasy pochodnej, operacja delete będzie działać dobrze.

Zdrówko, zdrówko, zdrówko, zdrówko, zdrówko,]}

Guy.

#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
Author: Guy Avraham,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-10-16 07:30:58

IMHO ma to związek zograniczeniem tablic do radzenia sobie z konstruktorem/destruktorem . Zauważ, że gdy wywołane jest new[], kompilator wymusza tworzenie instancji tylkodomyślnego konstruktora . W ten sam sposób, gdy wywołane jest delete[], kompilator Może szukać tylko destruktora wywołującego Typ statyczny wskaźnika.

Teraz w przypadku destruktora virtual, Destruktor klasy pochodnej powinien być wywołany najpierw przez klasę bazową. Ponieważ dla kompilatora tablic Może Zobacz statyczny typ wywołania obiektu (tutaj Base), może skończyć się wywołaniem tylko destruktora bazy; czyli UB.

Powiedziawszy to, niekoniecznie jest to UB dla wszystkich kompilatorów; powiedzmy na przykład gcc wywołuje destruktor w odpowiedniej kolejności.

 0
Author: iammilind,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-05-30 03:02:06

I myślę wszystko sprowadza się do zasady zero-overhead. tzn. język nie pozwala na przechowywanie informacji o dynamicznym typie elementów tablicy.

 0
Author: AraK,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-05-30 03:02:23