2011-01-19 15 views
13

¿Existen patrones establecidos para verificar las invariantes de clases en C++?comprobando invariantes en C++

Lo ideal es que las invariantes se verifiquen automáticamente al principio y al final de cada función de miembro público. Por lo que sé, C con clases proporcionó funciones de miembro especiales before y after, pero desafortunadamente, el diseño por contrato no era muy popular en ese momento y nadie, excepto Bjarne, usó esa característica, por lo que la eliminó.

Por supuesto, insertar manualmente las llamadas check_invariants() al principio y al final de cada función de miembro público es tedioso y propenso a errores. Desde RAII es el arma de elección para tratar con excepciones, se me ocurrió con el siguiente esquema de definición de un verificador de invariancia como la primera variable local, y que corrector invariancia comprueba los invariantes, tanto en la construcción y destrucción de tiempo:

template <typename T> 
class invariants_checker 
{ 
    const T* p; 

public: 

    invariants_checker(const T* p) : p(p) 
    { 
     p->check_invariants(); 
    } 

    ~invariants_checker() 
    { 
     p->check_invariants(); 
    } 
}; 

void Foo::bar() 
{ 
    // class invariants checked by construction of _ 
    invariants_checker<Foo> _(this); 

    // ... mutate the object 

    // class invariants checked by destruction of _ 
} 

Pregunta # 0: ¿Supongo que no hay forma de declarar una variable local sin nombre? :)

Aún tendríamos que llamar manualmente al check_invariants() al final del constructor Foo y al comienzo del destructor Foo. Sin embargo, muchos cuerpos de constructor y cuerpos de destructor están vacíos. En ese caso, ¿podríamos usar un invariants_checker como último miembro?

#include <string> 
#include <stdexcept> 

class Foo 
{ 
    std::string str; 
    std::string::size_type cached_length; 
    invariants_checker<Foo> _; 

public: 

    Foo(const std::string& str) 
    : str(str), cached_length(str.length()), _(this) {} 

    void check_invariants() const 
    { 
     if (str.length() != cached_length) 
      throw std::logic_error("wrong cached length"); 
    } 

    // ... 
}; 

Pregunta # 1: ¿Es válido para pasar al constructor thisinvariants_checker el que llama inmediatamente check_invariants a través de ese puntero, aunque el objeto Foo está todavía en construcción?

Pregunta # 2: ¿Ve algún otro problema con este enfoque? ¿Puedes mejorarlo?

Pregunta n. ° 3: ¿Es este enfoque nuevo o bien conocido? ¿Hay mejores soluciones disponibles?

+0

No puede usar 'this' en la lista de inicializadores. Sin embargo, podrías usarlo en el cuerpo del constructor. – Benoit

+0

@Benoit: ¿Qué quieres decir con * can not *? ¿Está estrictamente prohibido? ¿Invoca un comportamiento indefinido? – fredoverflow

+1

Thorsten Ottesen (creo que fue) tenía una propuesta para Design By Contract. No llegó a despegar en la primera ronda, debido a la dificultad de decidir qué es interno y qué es una llamada externa (puede romper temporalmente la llamada interna invariante). Pero aún podría aparecer. No sé si se ha trabajado activamente. –

Respuesta

10

Respuesta # 0: Puede tener variables locales sin nombre, pero se pierde el control sobre el tiempo de vida del objeto, y todo el sentido del objeto es porque usted tener una buena idea cuando se sale del alcance. Puede usar

void Foo::bar() 
{ 
    invariants_checker<Foo>(this); // goes out of scope at the semicolon 
    new invariants_checker<Foo>(this); // the constructed object is never destructed 
    // ... 
} 

pero ninguno es lo que quiere.

Respuesta n. ° 1: No, creo que no es válida. El objeto al que hace referencia el this está completamente construido (y por lo tanto comienza a existir) cuando el constructor finaliza. Estás jugando un juego peligroso aquí.

Respuesta # 2 & # 3: Este enfoque no es nuevo, es una consulta simple de google para, p. Ej. "verificar la plantilla C++ de invariantes" dará muchos resultados sobre este tema. En particular, esta solución se puede mejorar aún más si no le importa la sobrecarga del operador ->, así:

template <typename T> 
class invariants_checker { 
public: 
    class ProxyObject { 
    public: 
    ProxyObject(T* x) : m(x) { m->check_invariants(); } 
    ~ProxyObject() { m->check_invariants(); } 
    T* operator->() { return m; } 
    const T* operator->() const { return m; } 
    private: 
    T* m; 
    }; 

invariants_checker(T* x) : m(x) { } 

ProxyObject operator->() { return m; } 
const ProxyObject operator->() const { return m; } 

private: 
    T* m; 
}; 

La idea es que durante la duración de una llamada de función miembro, se crea un objeto proxy anónimo el cual realiza el control en su constructor y destructor. Puede usar la plantilla por encima de la siguiente manera:

void f() { 
    Foo f; 
    invariants_checker<Foo> g(&f); 
    g->bar(); // this constructs and destructs the ProxyObject, which does the checking 
} 
+1

+1 para introducir la idea de 'ProxyObject'. ¡muy agradable! – Nawaz

+1

0) no es una variable sin nombre, sino más bien un objeto temporal. No hay una variable declarada, pero una declaración única que crea un temporal y no hace nada con él. +1 para el último bloque y la sugerencia de sobrecargar 'operator->' ... pequeña bestia de fantasía allí. (Otro uso similar se muestra en "Diseño moderno de C++" para proporcionar un puntero inteligente de bloqueo. Las sugerencias para agregar un puntero inteligente de bloqueo al estándar fueron rechazadas sobre la base de que la granularidad de las cerraduras en la mayoría de los casos no sería la mejor opción) –

+1

@David: Entonces, ¿cuál es la diferencia entre una variable sin nombre (también conocido como un valor anónimo) y un objeto temporal? –

0

unidad de pruebas es mejor alternativa que conduce al código más pequeño con un mejor rendimiento

+0

Entonces, ¿cómo es que D tiene pruebas de unidad de diseño por contrato * y * integradas en el lenguaje? :) – fredoverflow

+0

@Fred: sí, tienes razón, a veces revisar invariantes es una buena idea, pero para un enfoque tan sistemático prefiero usar la prueba unitaria –

1

Pregunta # 0: supongo que no hay manera de declarar una variable local sin nombre? :)

Puede suele preparar algo usando macros y __LINE__, pero si se acaba de elegir un nombre bastante extraño, ya que debe hacer, ya que no debe tener más de un (directamente) en el mismo ámbito . Este

class invariants_checker {}; 

template<class T> 
class invariants_checker_impl : public invariants_checker { 
public: 
    invariants_checker_impl(T* that) : that_(that) {that_->check_invariants();} 
    ~invariants_checker_impl()      {that_->check_invariants();} 
private: 
    T* that_; 
}; 

template<class T> 
inline invariants_checker_impl<T> get_invariant_checker(T* that) 
{return invariants_checker_impl<T>(that);} 

#define CHECK_INVARIANTS const invariants_checker& 
    my_fancy_invariants_checker_object_ = get_invariant_checker(this) 

funciona para mí.

Pregunta # 1: ¿Es válido para pasar al constructor thisinvariants_checker el que llama inmediatamente check_invariants a través de ese puntero, aunque el objeto Foo está todavía en construcción?

No estoy seguro de si invoca UB técnico. En la práctica, sería seguro hacerlo, cuando no sea por el hecho de que, en la práctica, un miembro de la clase que deba ser declarado en un puesto específico en relación con otros miembros de la clase va a ser un problema tarde o temprano.

Pregunta # 2: ¿Ve algún otro problema con este enfoque? ¿Puedes mejorarlo?

See # 2. Tome una clase de tamaño moderado, agregue media década de extensión y corrección de errores por dos docenas de desarrolladores, y considero las posibilidades de estropear esto al menos una vez en aproximadamente el 98%.
Puede mitigar un poco esto agregando un comentario de gritos al miembro de datos. Todavía.

Pregunta # 3: ¿Es este enfoque nuevo o bien conocido? ¿Hay mejores soluciones disponibles?

que no había visto este enfoque, pero teniendo en cuenta su descripción de before() y after() pensé inmediatamente en la misma solución.

Creo que Stroustrup tenía un artículo de hace muchos (~ 15?) Años, donde describió una clase de control sobrecargando operator->() para devolver un proxy. Esto podría entonces, en su código y dtor, realizar antes y después de las acciones sin tener en cuenta los métodos invocados a través de él.

Editar: veo que Frerich ha añadido an answer fleshing this out. Por supuesto, a menos que su clase ya necesite ser utilizada a través de dicho identificador, esto es una carga para los usuarios de su clase. (IOW: No funcionará)

2

Idealmente, los invariantes se comprueban automáticamente al principio y al final de cada función miembro pública

creo que esto es una exageración; Yo en cambio reviso invariantes juiciosamente. Los miembros de datos de su clase son private (¿no?), Por lo que solo sus funciones miembro pueden cambiar los datos y, por lo tanto, invalidar invariantes.De modo que puede salirse con la suya comprobando un invariante justo después de un cambio en un miembro de datos que participe en ese invariante.

1

# 0: No, pero las cosas podrían ser un poco mejor con una macro (si estás bien con eso)

# 1: No, pero depende. No se puede hacer nada que haga que esto sea desreferenciado antes del cuerpo (lo que haría el tuyo, pero justo antes, para que funcione). Esto significa que puede almacenar esto, pero no acceder a campos o funciones virtuales. Llamar a check_invariants() no está bien si es virtual. Creo que funcionaría para la mayoría de las implementaciones, pero no se garantiza que funcione.

# 2: Creo que será tedioso, y no vale la pena. Esta ha sido mi experiencia con el control invariante. Prefiero las pruebas unitarias.

# 3: Lo he visto. Me parece que es el camino correcto si lo vas a hacer.

0

Veo claramente el problema de que su destructor llama a una función que a menudo arrojará, eso es un no-no en C++, ¿no es así?

+0

Absolutamente, porque se invocan destructores mientras se desenrolla la pila durante una excepción. No hay forma de saber la diferencia. Sin embargo, espero que la verificación invariante solo esté allí durante las pruebas, no en el código de producción, por lo que podría estar bien. –

+1

@Lou: si la verificación invariante se activa, significa que hay un error de programación en el código y la clase está en un estado en el que nunca debería estar. En este caso, todas las apuestas están desactivadas y el programa simplemente debe finalizar. Si esto se descubre en un dtor __ no importa__ en absoluto, cuando el programa va a terminar de todos modos – sbi

Cuestiones relacionadas