2010-12-11 12 views
6

En mi código, me acabo de dar cuenta de que a menudo necesito comprobar nullptr, aunque nullptr no debería ser posible (de acuerdo con los requisitos especificados).Evitar punteros nulos y mantener el polimorfismo

Sin embargo, nullptr aún puede ocurrir ya que otras personas podrían enviar un nullptr creyendo que esto está bien (lamentablemente no todo el mundo lee/escribe la especificación), y este defecto no puede detectarse a menos que el problema se desencadene durante la prueba (y la cobertura de prueba alta es costosa). Por lo tanto, podría dar lugar a muchos errores posteriores a la publicación informados por los clientes.

p. Ej.

class data 
{ 
    virtual void foo() = 0; 
}; 

class data_a : public data 
{ 
public: 
    virtual void foo(){} 
}; 

class data_b : public data 
{ 
public: 
    virtual void foo(){} 
}; 

void foo(const std::shared_ptr<data>& data) 
{ 
    if(data == nullptr) // good idea to check before use, performance and forgetting check might be a problem? 
     return; 
    data->foo(); 
} 

Normalmente, simplemente usaría value-types y lo pasaría por referencia y copia. Sin embargo, en algunos casos necesito polimorfismo que requiere punteros o referencias.

Así que he comenzado a utilizar el siguiente "polimorfismo de tiempo de compilación".

class data_a 
{ 
public: 
    void foo(){} 
private: 
    struct implementation; 
    std::shared_ptr<implementation> impl_; // pimpl-idiom, cheap shallow copy 
}; 

class data_b 
{ 
public: 
    void foo(){} 
private: 
    struct implementation; 
    std::shared_ptr<implementation> impl_; // pimpl-idiom, cheap shallow copy 
}; 

class data 
{ 
public: 
    data(const data_a& x) : data_(x){} // implicit conversion 
    data(const data_b& x) : data_(x){} // implicit conversion 
    void foo() 
    { 
      boost::apply(foo_visitor(), data_); 
    } 
private: 
    struct foo_visitor : public boost::static_visitor<void> 
    { 
      template<typename T> 
      void operator()(T& x){ x.foo(); }  
    }; 

    boost::variant<data_a, data_b> data_; 
} 

void foo(const data& data) 
{ 
    data.foo(); 
} 

¿Alguien más piensa que es una buena idea, cuando es práctico? ¿O me estoy perdiendo algo? ¿Hay algún problema potencial con esta práctica?

EDIT:

El "problema" con el uso de las referencias es que no puede mover la propiedad de una referencia (por ejemplo, devolver un objeto).

data& create_data() { data_a temp; return temp; } // ouch... cannot return temp; 

El problema con referencias rvalue (polimorfismo funciona con referencias rvalue?) Se convierte entonces en que no se puede compartir la propiedad.

data&& create_data() { return std::move(my_data_); } // bye bye data   

Un puntero "seguro" basado en shared_ptr hace sonar como una buena idea, pero todavía me gustaría una solución en la que no se hace cumplir nullness en tiempo de compilación, tal vez no sea posible.

+0

Debe volver a escribir el código en la publicación. En algunos lugares está mezclando punteros y referencias, los punteros pueden ser nulos, y eliminarlos de referencias utiliza el operador '->'; las referencias no pueden ser nulas, y su desreferenciación utiliza el operador '.'. –

+0

Hm ... No puedo ver el problema. ¿Te importaría señalar dónde está? – ronag

+0

Otras cosas pequeñas: una vez que define * any * constructor, el compilador no generará el constructor predeterminado (no es necesario definirlo como privado), y si realmente desea deshabilitar un constructor --de nuevo, no requerido para el constructor predeterminado-- - es mejor declararlo privado pero * no * definirlo. Ser privado atrapará el uso de otras clases en el momento de la compilación, no definirlas atrapará usos dentro de su propia clase (o amigos) en el momento del enlace. –

Respuesta

3

Yo personalmente prefiero codificar la posibilidad de nulo en el tipo, y así usar boost::optional.

  • crear una clase data_holder, que siempre posee una data (pero permite el polimorfismo)
  • definir sus interfaces de en términos de data_holder (no nulo) o boost::optional<data_holdder>

De esta manera es perfectamente clara si puede ser nulo o no.

Ahora, la parte más difícil es obtener data_holder para no tener nunca un puntero nulo. Si lo define con un constructor del formulario data_holder(data*), el constructor puede lanzar.

Por otro lado, podría simplemente tomar algunos argumentos, y diferir la construcción real a una fábrica (utilizando el lenguaje constructor virtual). Todavía verifica el resultado de la fábrica (y arroja si es necesario), pero solo tiene un lugar para verificar (la fábrica) en lugar de cada punto de construcción.

Es posible que desee comprobar boost::make_shared también, para ver el reenvío de argumentos en acción. Si usted tiene C++ 0x, entonces se puede reenviar el argumento de manera eficiente y obtener:

template <typename Derived> 
data_holder(): impl(new Derived()) {} 

// Other constructors for forwarding 

No olvide declarar el constructor por defecto (sin plantilla) como privado (y no definirlo) para evitar un choque accidental llamada.

4

Siempre puede usar el null object pattern y referencias solamente. No se puede pasar (bueno, se puede, pero ese es el error del usuario) una referencia nula.

+0

El jurado todavía no me conoce en el patrón de objeto nulo. Parece que si se usa incorrectamente, el programa puede continuar la ejecución aunque esté dentro de un estado no válido. Sin embargo, tengo +1 debido a la segunda parte de tu respuesta. Si no quiere un nullptr, entonces tome una referencia. Estoy de acuerdo en que el usuario aún puede romper esto por ignorancia o engaño, pero en todos los otros casos esta es la apuesta más segura. –

2

A non-null pointer es un concepto ampliamente conocido, y se usa en subconjuntos seguros de C, por ejemplo. Sí, puede ser ventajoso.

Y, debe usar un puntero inteligente para esto. Dependiendo de su caso de uso, es posible que desee comenzar con algo similar a boost::shared_ptr o tr1::unique_ptr. Pero en lugar de solo assert ing null-ness en operator*, operator-> y get(), ejecute una excepción.

ETA: forget this. Aunque creo que este enfoque general es útil (sin comportamiento indefinido, etc., etc.), no le proporciona comprobaciones en tiempo de compilación para la ausencia de nulidad, que probablemente desee. Para esto, habría utilizado punteros no nulos de forma generalizada en todo su código, y sin el soporte de idiomas, esto seguiría teniendo fugas.

1

En general, cuando se utilizan funciones virtuales como se ha descrito anteriormente, es porque no quiere saber acerca de todas las clases que implementan la interfaz deseada. En su código de ejemplo, enumera todas las clases que implementan la interfaz (conceptual). Esto se vuelve frustrante cuando tiene que agregar o eliminar implementaciones a lo largo del tiempo.

Su enfoque también interfiere con la administración de dependencias ya que los datos de su clase dependen de todas las clases, mientras que el uso del polimorfismo hace que sea más fácil limitar la dependencia de clases particulares. Usar el modismo de los pimpl mitiga esto, pero siempre consideré que el idioma de los pimpl es algo molesto (ya que tienes dos clases que deben estar sincronizadas para representar un concepto).

El uso de referencias o punteros inteligentes comprobados parece una solución más simple y más limpia. Otras personas ya están comentando sobre eso, así que no elaboraré en este momento.