2010-04-08 9 views
10

En el siguiente código:¿Es peligroso ampliar una clase base con un destructor no virtual?

class A { 
}; 
class B : public A { 
}; 
class C : public A { 
    int x; 
}; 

int main (int argc, char** argv) { 
    A* b = new B(); 
    A* c = new C(); 

    //in both cases, only ~A() is called, not ~B() or ~C() 
    delete b; //is this ok? 
    delete c; //does this line leak memory? 

    return 0; 
} 

Cuando se llama a borrar en una clase con un destructor no virtual con funciones miembro (como la clase C), puede el asignador de memoria diga qué el tamaño apropiado del objeto es? Si no, ¿se ha filtrado la memoria?

En segundo lugar, si la clase no tiene funciones de miembro, y no hay un comportamiento de destructor explícito (como la clase B), ¿está todo bien?

Lo pregunto porque quería crear una clase para extender std::string, (que sé que no es recomendable, pero en aras de la discusión acaba de llevar con él), y la sobrecarga del operador +=, +. -WeffC++ me da una advertencia porque std::string tiene un destructor no virtual, pero ¿Importa si la subclase no tiene miembros y no necesita hacer nada en su destructor?

FYI la sobrecarga += era hacer el formato de ruta de archivo adecuado, por lo que la clase de ruta podría ser utilizado como:

class path : public std::string { 
    //... overload, +=, + 
    //... add last_path_component, remove_path_component, ext, etc... 
}; 

path foo = "/some/file/path"; 
foo = foo + "filename.txt"; 
std::string s = foo; //easy assignment to std::string 
some_function_taking_std_string (foo); //easy implicit conversion 
//and so on... 

solo quería asegurarse de que alguien hacer esto:

path* foo = new path(); 
std::string* bar = foo; 
delete bar; 

sería no causa ningún problema con la asignación de memoria?

Respuesta

4

Así que todo el mundo ha estado diciendo que no puede hacerlo, conduce a un comportamiento indefinido. Sin embargo, hay algunos casos en los que es seguro. Si nunca está creando instancias dinámicas de su clase, entonces debería estar bien. (es decir, no hay llamadas nuevas)

Dicho esto, generalmente se considera algo malo ya que alguien podría intentar crear uno polimórficamente en una fecha posterior. (Puede protegerse contra esto teniendo un operador privado no implementado nuevo, pero no estoy seguro).

Tengo dos ejemplos en los que no odio derivar de clases con destructores no virtuales. El primero es crear azúcar sintáctica usando temporarios ... aquí hay un ejemplo artificial.

class MyList : public std::vector<int> 
{ 
    public: 
    MyList operator<<(int i) const 
    { 
     MyList retval(*this); 
     retval.push_back(i); 
     return retval; 
    } 
    private: 
    // Prevent heap allocation 
    void * operator new (size_t); 
    void * operator new[] (size_t); 
    void operator delete (void *); 
    void operator delete[] (void*); 
}; 

void do_somthing_with_a_vec(std::vector<int> v); 
void do_somthing_with_a_const_vec_ref(const std::vector<int> &v); 

int main() 
{ 
    // I think this slices correctly .. 
    // if it doesn't compile you might need to add a 
    // conversion operator to MyList 
    std::vector<int> v = MyList()<<1<<2<<3<<4; 

    // This will slice to a vector correctly. 
    do_something_with_a_vec(MyList()<<1<<2<<3<<4); 

    // This will pass a const ref - which will be OK too. 
    do_something_with_a_const_vec_ref(MyList()<<1<<2<<3<<4); 

    //This will not compile as MyList::operator new is private 
    MyList * ptr = new MyList(); 
} 

El otro uso válido que puedo pensar proviene de la falta de plantilla typedefs en C++. Así es cómo puede usarlo.

// Assume this is in code we cant control 
template<typename T1, typename T2 > 
class ComplicatedClass 
{ 
    ... 
}; 

// Now in our code we want TrivialClass = ComplicatedClass<int,int> 
// Normal typedef is OK 
typedef ComplicatedClass<int,int> TrivialClass; 

// Next we want to be able to do SimpleClass<T> = ComplicatedClass<T,T> 
// But this doesn't compile 
template<typename T> 
typedef CompilicatedClass<T,T> SimpleClass; 

// So instead we can do this - 
// so long as it is not used polymorphically if 
// ComplicatedClass doesn't have a virtual destructor we are OK. 
template<typename T> 
class SimpleClass : public ComplicatedClass<T,T> 
{ 
    // Need to add the constructors we want here :(
    // ... 
    private: 
    // Prevent heap allocation 
    void * operator new (size_t); 
    void * operator new[] (size_t); 
    void operator delete (void *); 
    void operator delete[] (void*); 
} 

Heres un ejemplo más concreto de esto. Que desea utilizar std :: mapa con un asignador de costumbre para muchos tipos diferentes, pero usted no quiere que el imposible de mantener

std::map<K,V, std::less<K>, MyAlloc<K,V> > 

llena a través de su código.

template<typename K, typename V> 
class CustomAllocMap : public std::map< K,V, std::less<K>, MyAlloc<K,V> > 
{ 
    ... 
    private: 
    // Prevent heap allocation 
    void * operator new (size_t); 
    void * operator new[] (size_t); 
    void operator delete (void *); 
    void operator delete[] (void*); 
}; 

MyCustomAllocMap<K,V> map; 
+0

Ambos ejemplos no protegen a nadie de eliminar a través del puntero base, obteniendo un comportamiento indefinido. Para el primero, es mejor tener un contenedor privado y un operador de conversión. Para el segundo, una estructura con un typedef dentro de los servidores tiene el mismo propósito. – GManNickG

+0

@GMan Estoy de acuerdo en que la eliminación sería mala, y específicamente digo que no debes usar estos objetos con asignaciones nuevas. Si estás eliminando algo que no fue renovado, te topas con UB sin importar nada. También sugiero cómo evitar la convocatoria de nuevos ... pero yo mismo no lo he intentado. –

+0

@GMan también, encuentro "CustomAllocMap :: type map_" un poco menos intuitivo de leer que "CustomAllocMap map_" ... pero el typedef en una estructura con plantilla es una buena opción. –

2

problema puede ocurrir si almacena la dirección de memoria de un tipo derivado dentro del tipo base y luego llama a eliminar el tipo de base:

B* b = new C(); 
delete b; 

Si B tenía un destructor virtual a continuación, destructor de C sería llamado y luego B's Pero sin un destructor virtual tiene un comportamiento indefinido.

los siguientes 2 eliminaciones causan ningún problema:

B* b = new B(); 
delete b; 
C* c = new C() 
delete c; 
+0

clase B no tiene variables miembro, y no hay comportamiento de destructor necesario ... así que si no me importa si se llama ~ B(), ¿hay algún problema? Supongo que estoy preguntando si la eliminación necesita hacer algún trabajo adicional para la subclase – Akusete

+0

@Akusete: puede ser MUY cuidadoso para no almacenar tipos derivados dentro de los punteros de tipo base y llamar a eliminar en los tipos base. –

+0

Si no hay forma de hacerlo semánticamente imposible, podría apostar que sucedería eventualmente :) ... una buena forma de hacer el formateo de ruta no vale ni siquiera un error adicional. – Akusete

12

No, no es seguro para heredar públicamente de clases sin destructores virtuales, porque si elimina la deriva a través de una base introduce un comportamiento indefinido. La definición de la clase derivada es irrelevante (miembros de datos o no, etc.):

§5.3.5/3: en la primera alternativa (eliminar objeto), si el tipo estático del operando 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. (El subrayado es mío.)

Ambos de estos ejemplos en su código conducen a un comportamiento indefinido. Puedes heredar de manera no pública, pero eso obviamente frustra el propósito de usar esa clase y luego extenderla. (Dado que ya no es posible eliminarlo a través de un puntero base.)

Este es (un motivo *) por el que no debe heredar de las clases de biblioteca estándar. El best solution es ampliarlo con funciones libres. De hecho, incluso si pudiera, debería prefer free-functions anyway.


* Otro ser: ¿Realmente desea reemplazar toda su uso de cadena con una nueva clase de cuerdas, sólo para obtener algunas funciones? Eso es mucho trabajo innecesario.

+0

ok, gracias, por lo que el comportamiento no está definido. Tomaré nota de eso. – Akusete

+0

Tenía la esperanza de que dado que el destructor de la subclase no era operativo, estaría bien que no se llamara. – Akusete

+0

@Akusete: Eso no hace una diferencia: §5.3.5/3: * En la primera alternativa (eliminar objeto), si el tipo estático del operando es diferente de su tipo dinámico, el tipo estático debe ser una base la clase del tipo dinámico del operando y el tipo estático tendrá un destructor virtual o el comportamiento no está definido. "* – GManNickG

1

Solo es seguro privado heredar de las clases base sin un destructor virtual. La herencia pública permitiría eliminar una clase derivada a través de un puntero de clase base, que es un comportamiento indefinido en C++.

Este es uno de los únicos usos razonables de la herencia privada en C++.

+0

Definitivamente ... no. Se preferirá utilizar la composición para usar herencia privada.El único uso razonable de la herencia privada que conozco es activar 'la optimización de la base vacía' ... y se trata más de rendimiento que del diseño real. –

2

Esta no es una respuesta a su pregunta, sino al problema que estaba tratando de resolver (formateo de ruta). Echar un vistazo a boost::filesystem, que tiene una mejor manera de concatenar caminos:

boost::filesystem::path p = "/some/file/path"; 
p /= "filename.txt"; 

entonces usted puede recuperar la ruta como una cadena en ambos formatos independientes de plataforma y específicas de la plataforma.

La mejor parte es que ha sido aceptado en TR2, lo que significa que se convertirá en parte del estándar C++ en el futuro.

1

Para agregar a lo que se ha dicho.

  • En realidad, está considerando agregar métodos a una clase que ya se considera hinchada.
  • usted quiere que su clase path a ser considerado como un string a pesar de una serie de operaciones tendría poco sentido

yo propondría que utilice Composition sobre Inheritance.De esta forma, solo volverás a implementar (reenviar) aquellas operaciones que realmente tengan sentido para tu clase (no creo que realmente necesites el operador de subíndices, por ejemplo).

Además, es posible considerar el uso de un std::wstring vez, o tal vez una cadena , sea capaz de manejar más de caracteres ASCII (Soy una pequeña crítica, pero luego soy francés e inferior ASCII es insuficiente para la lengua francesa) .

Es, realmente, una cuestión de encapsulación. Si decides manejar los caracteres UTF-8 correctamente un día y cambiar tu clase básica ... es probable que tus clientes te cuelguen por los pies. Por otro lado, si usaste la composición, nunca tendrá ningún problema siempre que lo pises con cuidado con la interfaz.

Finalmente, como se ha sugerido, las funciones gratuitas deben marcar el camino. Una vez más porque proporcionan una mejor encapsulación (siempre que no sean amigos ...).

Cuestiones relacionadas