2009-07-02 7 views
50

Es una creencia común que la biblioteca estándar de C++ generalmente no se pretende extender mediante el uso de herencia. Ciertamente, yo (y otros) hemos criticado a personas que sugieren derivar de clases como std::vector. Sin embargo, esta pregunta: c++ exceptions, can what() be NULL? me hizo darme cuenta de que hay al menos una parte de la Biblioteca estándar que está destinada a ser tan extendida: std::exception.¿Extender la biblioteca estándar de C++ por herencia?

lo tanto, mi pregunta tiene dos partes:

  1. ¿Hay otras clases de biblioteca estándar que se pretende que se derivan de?

  2. Si uno deriva de una clase de biblioteca estándar como std::exception, ¿está vinculado por la interfaz descrita en el estándar ISO? Por ejemplo, ¿un programa que utiliza una clase de excepción que es la función de miembro what() no devolvió un NTBS (digamos que devolvió un puntero nulo) de conformidad estándar?

Respuesta

36

Buena pregunta agradable. Realmente me gustaría que el estándar fuera un poco más explícito sobre cuál es el uso previsto. Tal vez debería haber un documento de Justificación de C++ que coincida con el estándar de lenguaje. En cualquier caso, aquí está el enfoque que utilizo:

(a) No tengo conocimiento de la existencia de dicha lista. En su lugar, yo uso la lista siguiente para determinar si un tipo de biblioteca estándar es probable que ser diseñado para ser heredado de:

  • Si no tiene ningún virtual métodos, entonces no debe ser utilizarlo como una base. Esto descarta std::vector y similares.
  • Si tiene los métodos virtual, entonces es un candidato para el uso como una clase base.
  • Si hay muchas declaraciones friend flotando alrededor, entonces manténgase alejado ya que probablemente haya un problema de encapsulación.
  • Si se trata de una plantilla, mire más de cerca antes de heredar, ya que probablemente pueda personalizarla con especializaciones.
  • La presencia de un mecanismo basado en políticas (por ejemplo, std::char_traits) es una pista bastante buena de que no debe usarla como base.

Desafortunadamente no sé de un buen integral o negro y blanco de la lista. Suelo ir por instinto.

(b) Aplicaría LSP aquí. Si alguien llama al what() en su excepción, entonces su comportamiento observable debe coincidir con el de std::exception. No creo que sea realmente un problema de conformidad con los estándares tanto como un problema de corrección. El Estándar no requiere que las subclases sean sustituibles por clases base. Realmente es solo una "mejor práctica" .

+0

Mencionaste que no heredabas de clases que no tienen miembros virtuales, pero también mencionaste clases de políticas. Entonces (por ejemplo) ¿qué hay de malo en heredar de un asignador (privadamente)? ​​ –

+3

. Añadiría que algunas cosas están destinadas a tener un pequeño grado ree de extensión a través de la herencia, como 'std :: stack' y' std :: queue' porque tienen ** protected **, ya que la única razón para proteger las cosas es permitir que una clase hija lea los datos. Obviamente, deberías tener mucho cuidado con lo que haces con esto. –

17

a) se hace la biblioteca flujo ser heredado :)

4

Para contestar la pregunta 2):

yo creo que sí, que estarían obligados por la descripción de interfaz de la norma ISO . Por ejemplo, el estándar permite la redefinición de operator new y operator delete en todo el mundo. Sin embargo, el estándar garantiza que operator delete es una no operación en punteros nulos.

No respetar este comportamiento es definitivamente indefinido (al menos para Scott Myers). Creo que podemos decir que lo mismo es cierto por analogía para otras áreas de la biblioteca estándar.

3

Para la segunda pregunta, creo que la respuesta es sí. El estándar dice que el miembro de std :: exception debe devolver un valor no NULL. No debería importar si tengo una pila, una referencia o un valor de puntero para std :: exception. El retorno de what() está vinculado por el estándar.

Por supuesto, es posible devolver NULL. Pero consideraría que una clase así no cumple con los estándares.

4

Algunas de las cosas en functional, como greater<>, less<>, y mem_fun_t se derivan de unary_operator<> y binary_operator<>. Pero, IIRC, eso solo te da algunos typedefs.

5

La biblioteca estándar de C++ no es una sola unidad. Es el resultado de combinar y adoptar varias bibliotecas diferentes (una gran parte de la biblioteca estándar C, la biblioteca iostreams y el STL son los tres bloques básicos principales, y cada uno de estos se ha especificado de forma independiente)

La parte STL de la biblioteca generalmente no se pretende derivar, como usted sabe. Utiliza programación genérica, y generalmente evita OOP.

La biblioteca IOStreams es mucho más tradicional OOP, y utiliza la herencia y el polimorfismo dinámico en gran medida internamente, y se espera que los usuarios usen los mismos mecanismos para extenderla. Las secuencias personalizadas se escriben normalmente derivando de la clase de secuencia en sí o de la clase streambuf que utiliza internamente. Ambos tienen métodos virtuales que pueden anularse en las clases derivadas.

std::exception es otro ejemplo.

Y, como D.Shawley dijo, aplicaría el LSP a su segunda pregunta. Siempre debe ser legal sustituir la clase base por una derivada. Si llamo al exception::what(), debe cumplir con el contrato especificado por la clase exception, sin importar de dónde provenga el objeto exception, o si se trata realmente de una clase derivada que se haya subido. Y en este caso, ese contrato es la promesa del estándar de devolver un NTBS. Si hizo que una clase derivada se comportara de manera diferente, violaría el estándar porque un objeto de tipo std::exception ya no devuelve un NTBS.

1

w.r.t pregunta 2), según la norma de C++, la clase de excepción derivada debe especificar una especificación de no-tiro, es decir, throw(), junto con la devolución no nula. Esto significa en muchos casos que la clase de excepción derivada no debe usar std :: string, ya que std :: string podría arrojarse dependiendo de la implementación.

4

La regla parsimoniosa es "Cualquier clase se puede utilizar como una clase base; la responsabilidad de utilizarla de forma segura en ausencia de métodos virtuales, incluido un destructor virtual, es completamente del autor que la deriva". Agregar un miembro que no sea POD en un elemento secundario de std :: exception es el mismo error de usuario que en una clase derivada de std :: vector. La idea de que los contenedores no están "destinados" a ser clases básicas es un ejemplo de ingeniería de lo que los profesores de literatura llaman La falacia del intento autorial.

El principio IS-A domina. No obtenga D de B a menos que D pueda sustituir a B en todos los aspectos en la interfaz pública de B, incluida la operación de eliminación en un puntero B. Si B tiene métodos virtuales, esta restricción es menos onerosa; pero si B solo tiene métodos no virtuales, sigue siendo posible y legítimo especializarse con la herencia.

C++ es multiparadigmática. La biblioteca de plantillas usa herencia, incluso herencia de clases sin destructores virtuales, y por lo tanto demuestra, por ejemplo, que tales construcciones son seguras y útiles; si fueron intencionados es una pregunta psicológica.

1

Sé que esta pregunta es antigua, pero me gustaría añadir mi comentario aquí.

Desde hace algunos años, uso class CfgValue que hereda de std :: string, aunque la documentación (o algún libro o documento estándar, no tengo la fuente a mano ahora) dice que los usuarios no deben heredar de std :: string Y esta clase contiene un "TODO: remove inheritance from std :: string" comentario desde hace años.

La clase CfgValue solo agrega algunos constructores y adaptadores y getters para convertir rápidamente entre cadenas y valores numéricos y booleanos, y la conversión de la codificación utf8 (mantenida en std :: cadena) a ucs2 (mantenida en std :: wstring) y así en.

yo sepa, no son muchos diferentes maneras de hacer esto, pero es sólo extremadamente útil para el usuario, y funciona muy bien (lo que significa, que no se rompe ninguna conceptos stdlib, manejo de excepciones y la como):

class CfgValue : public std::string { 
public: 
    ... 
    CfgValue(const int i) : std::string() { SetInteger(i); } 
    ... 
    void SetInteger(int i); 
    ... 
    int GetInteger() const; 
    ... 
    operator std::wstring() { return utf8_to_ucs16(*this); } 
    operator std::wstring() const { return utf8_to_ucs16(*this); } 
    ... 
}; 
7

en cuanto a su parte b, a partir 17.3.1.2 "Requisitos", párrafo 1:

La biblioteca puede ser extendido por un programa en C++. Cada cláusula, según corresponda, describe los requisitos que dichas extensiones deben cumplir. Tales extensiones son generalmente uno de los siguientes:

  • argumentos de plantilla
  • Las clases derivadas
  • Contenedores, iteradores, y/o algoritmos que cumplen una convención interfaz

Mientras 17.3 es informativo en lugar de vinculante, la intención del comité sobre el comportamiento derivado de la clase es clara.

Para otros puntos muy similares de extensión, hay requisitos claros:

  • 17.1.15 "comportamiento requerido" cubre el reemplazo (operador de nuevo, etc.) y funciones de controlador (terminar manipuladores, etc.) y arroja todo el comportamiento no conforme en UB-land.
  • 17.4.3.6/1: "En ciertos casos (funciones de reemplazo, funciones del manejador, operaciones en tipos utilizados para crear instancias de componentes de plantilla de biblioteca estándar), la biblioteca C++ Standard depende de los componentes suministrados por un programa C++. Si estos componentes no cumplir con sus requisitos, el estándar no establece requisitos sobre la implementación."

En el último punto no está claro para mí que la lista entre paréntesis sea exhaustiva, pero teniendo en cuenta cuán específicamente se trata cada caso mencionado en el párrafo siguiente, sería exagerado decir que el texto actual estaba destinado para cubrir las clases derivadas. Además, ese texto 17.4.3.6/1 no se modifica en el borrador de 2008 (donde está en 17.6.4.8) y no veo issues direccionando ni los métodos virtuales de las clases derivadas

Cuestiones relacionadas