Pregunta ¿Por qué es un comportamiento indefinido eliminar [] una matriz de objetos derivados a través de un puntero base?


Encontré el siguiente fragmento en el estándar C ++ 03 bajo 5.3.5 [expr.delete] p3:

En la primera alternativa (eliminar objeto), si el tipo estático del objeto a eliminar es diferente de su tipo dinámico, el tipo estático será una clase base del tipo dinámico del operando y el tipo estático tendrá un destructor virtual o el comportamiento no está definido. En la segunda alternativa (eliminar matriz) si el tipo dinámico del objeto a eliminar difiere de su tipo estático, el comportamiento no está definido.


Revisión rápida en tipos estáticos y dinámicos:

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

B* p = new D();

Tipo estático de p es B*, mientras que el tipo dinámico de *p es D, 1.3.7 [defns.dynamic.type]:

[Ejemplo: si un puntero p cuyo tipo estático es "puntero a class B"Está apuntando a un objeto de class D, derivado de B, el tipo dinámico de la expresión *p es "D. "]


Ahora, viendo la cita en la parte superior otra vez, esto significaría que el siguiente código invoca un comportamiento indefinido si obtuve ese derecho, independientemente de la presencia de un virtual incinerador de basuras:

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

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

¿Entendí mal la redacción en el estándar de alguna manera? ¿Pasé por alto algo? ¿Por qué el estándar especifica esto como un comportamiento indefinido?


32
2018-05-30 02:35


origen


Respuestas:


Base* p = new Base[n] crea un ngama de tamaño de Base elementos, de los cuales p luego apunta al primer elemento. Base* p = new Derived[n] sin embargo, crea un ngama de tamaño de Derived elementos. p luego apunta a la Base  subobjeto del primer elemento. p hace no Sin embargo, consulte la primera elemento de la matriz, que es lo que es válido delete[] p la expresión requiere

Por supuesto, sería posible ordenar (y luego implementar) que delete [] p Lo correcto en este caso. Pero, ¿qué tomaría? Una implementación debería tener cuidado de recuperar de alguna manera el tipo de elemento de la matriz, y luego moralmente dynamic_cast  p a este tipo Entonces, se trata de hacer una simple delete[] como ya lo hacemos

El problema con eso es que esto sería necesario cada vez una matriz de tipo de elemento polimórfico, independientemente de si el polimorfismo se utiliza o no. En mi opinión, esto no encaja con la filosofía C ++ de no pagar por lo que no usas. Pero peor: un habilitado polimórfico delete[] p es simplemente inútil porque p es casi inútil en tu pregunta. p es un puntero a un subobjeto de un elemento y no más; de lo contrario, no está relacionado con la matriz. Ciertamente no puedes hacer p[i] (para i > 0) con eso. Entonces no es irracional delete[] p no funciona

Para resumir:

  • las matrices ya tienen muchos usos legítimos. Al no permitir que las matrices se comporten polimórficamente (ya sea en conjunto o solo para delete[]) esto significa que las matrices con un tipo de elemento polimórfico no son penalizadas por esos usos legítimos, lo que está en línea con la filosofía de C ++.

  • si, por otro lado, se necesita una matriz con comportamiento polimórfico, es posible implementar una en términos de lo que ya tenemos.


30
2018-05-30 03:17



Está mal tratar una matriz de derivada como una matriz de bases, no solo al eliminar elementos. Por ejemplo, incluso el simple hecho de acceder a los elementos generalmente causará un desastre:

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

b[5] usará el tamaño de B para calcular a qué ubicación de memoria acceder y si B y D tienen diferentes tamaños, esto no conducirá a los resultados previstos.

Como un std::vector<D> no se puede convertir a std::vector<B>, un puntero a D[] no debe ser convertible a B*, pero por razones históricas compila de todos modos. Si un std::vector se usaría en su lugar, produciría un error de tiempo de compilación.

Esto también se explica en el Preguntas frecuentes sobre C ++ Lite respuesta sobre este tema.

Asi que delete causa un comportamiento indefinido en este caso porque ya está mal tratar una matriz de esta manera, aunque el sistema de tipos no pueda detectar el error.


11
2018-05-30 03:26



Solo para agregar a la excelente respuesta de algo - He escrito un pequeño ejemplo para ilustrar este problema con diferentes desplazamientos.

Tenga en cuenta que si comenta el miembro m_c de la clase Derivada, la operación de eliminación funcionará bien.

Aclamaciones,

Chico.

#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



En mi humilde opinión esto tiene que ver con limitación de matrices para tratar con constructor / destructor. Tenga en cuenta que, cuando new[] se llama, el compilador obliga a instanciar solo Constructor predeterminado. De la misma manera cuando delete[] se llama, compilador podría busque solo el destructor del tipo estático del puntero de llamada.

Ahora en el caso de virtual destructor, primero se debe llamar al destructor de clase Derivado seguido de la clase Base. Desde el compilador de matrices podría ver el tipo estático del tipo de objeto llamante (aquí Base), podría terminar llamando simplemente a Destructor base; que es UB.

Habiendo dicho eso, no es necesariamente UB para todos los compiladores; decir por ejemplo, gcc llama a destructor en el orden correcto.


0
2018-05-30 03:02



yo pensar todo se reduce al principio de cero sobrecarga. es decir, el idioma no permite almacenar información sobre el tipo dinámico de elementos de la matriz.


0
2018-05-30 02:57