Nota: el siguiente es el código C ++ 03, pero esperamos pasar a C ++ 11 en los próximos dos años, por lo que debemos tenerlo en cuenta.
Estoy escribiendo una guía (para novatos, entre otros) sobre cómo escribir una interfaz abstracta en C ++. Leí los dos artículos de Sutter sobre el tema, busqué en Internet ejemplos y respuestas e hice algunas pruebas.
¡Este código NO debe compilarse!
void foo(SomeInterface & a, SomeInterface & b)
{
SomeInterface c ; // must not be default-constructible
SomeInterface d(a); // must not be copy-constructible
a = b ; // must not be assignable
}
Todos los comportamientos anteriores encuentran la fuente de su problema en el corte : la interfaz abstracta (o la clase no hoja en la jerarquía) no debe ser construible ni copiable / asignable, INCLUSO si la clase derivada puede serlo.
0ª Solución: la interfaz básica
class VirtuallyDestructible
{
public :
virtual ~VirtuallyDestructible() {}
} ;
Esta solución es simple y algo ingenua: falla todas nuestras restricciones: puede ser construida por defecto, construida por copia y asignada por copia (ni siquiera estoy seguro sobre los constructores de movimientos y la asignación, pero todavía tengo 2 años para calcular fuera).
- No podemos declarar el destructor virtual puro porque necesitamos mantenerlo en línea, y algunos de nuestros compiladores no digerirán métodos virtuales puros con el cuerpo vacío en línea.
- Sí, el único punto de esta clase es hacer que los implementadores sean prácticamente destructibles, lo cual es un caso raro.
- Incluso si tuviéramos un método virtual puro adicional (que es la mayoría de los casos), esta clase aún sería asignable por copia.
Entonces no ...
1ra Solución: boost :: no copiable
class VirtuallyDestructible : boost::noncopyable
{
public :
virtual ~VirtuallyDestructible() {}
} ;
Esta solución es la mejor, porque es simple, clara y C ++ (sin macros)
El problema es que todavía no funciona para esa interfaz específica porque VirtuallyConstructible todavía se puede construir por defecto .
- No podemos declarar el destructor puro virtual porque necesitamos mantenerlo en línea, y algunos de nuestros compiladores no lo digerirán.
- Sí, el único punto de esta clase es hacer que los implementadores sean prácticamente destructibles, lo cual es un caso raro.
Otro problema es que las clases que implementan la interfaz no copiable deben declarar / definir explícitamente el constructor de copia y el operador de asignación si necesitan tener esos métodos (y en nuestro código, tenemos clases de valores a las que nuestro cliente todavía puede acceder a través de interfaces).
Esto va en contra de la Regla de Cero, que es a donde queremos ir: si la implementación predeterminada es correcta, entonces deberíamos poder usarla.
Segunda solución: ¡protégelos!
class MyInterface
{
public :
virtual ~MyInterface() {}
protected :
// With C++11, these methods would be "= default"
MyInterface() {}
MyInterface(const MyInterface & ) {}
MyInterface & operator = (const MyInterface & ) { return *this ; }
} ;
Este patrón sigue las restricciones técnicas que teníamos (al menos en el código de usuario): MyInterface no se puede construir por defecto, no se puede construir con copia y no se puede asignar con copia.
Además, no impone restricciones artificiales para implementar clases , que luego son libres de seguir la Regla de Cero, o incluso declarar algunos constructores / operadores como "= predeterminado" en C ++ 11/14 sin problema.
Ahora, esto es bastante detallado, y una alternativa sería usar una macro, algo así como:
class MyInterface
{
public :
virtual ~MyInterface() {}
protected :
DECLARE_AS_NON_SLICEABLE(MyInterface) ;
} ;
El protegido debe permanecer fuera de la macro (porque no tiene alcance).
Correctamente "espacio de nombres" (es decir, prefijado con el nombre de su empresa o producto), la macro debe ser inofensiva.
Y la ventaja es que el código se factoriza en una fuente, en lugar de ser copiado y pegado en todas las interfaces. Si el constructor de movimiento y la asignación de movimiento se desactivan explícitamente de la misma manera en el futuro, este sería un cambio muy leve en el código.
Conclusión
- ¿Estoy paranoico de querer que el código esté protegido contra cortes en las interfaces? (Creo que no, pero nunca se sabe ...)
- ¿Cuál es la mejor solución entre las anteriores?
- ¿Hay otra solución mejor?
Recuerde que este es un patrón que servirá como guía para los novatos (entre otros), por lo que una solución como: "Cada caso debe tener su implementación" no es una solución viable.
Recompensa y resultados
Le otorgé la recompensa a Coredump por el tiempo dedicado a responder las preguntas y la relevancia de las respuestas.
Mi solución al problema probablemente irá a algo así:
class MyInterface
{
DECLARE_CLASS_AS_INTERFACE(MyInterface) ;
public :
// the virtual methods
} ;
... con la siguiente macro:
#define DECLARE_CLASS_AS_INTERFACE(ClassName) \
public : \
virtual ~ClassName() {} \
protected : \
ClassName() {} \
ClassName(const ClassName & ) {} \
ClassName & operator = (const ClassName & ) { return *this ; } \
private :
Esta es una solución viable para mi problema por las siguientes razones:
- Esta clase no se puede instanciar (los constructores están protegidos)
- Esta clase puede ser prácticamente destruida.
- Esta clase se puede heredar sin imponer restricciones indebidas a las clases heredadas (por ejemplo, la clase heredada podría ser copiable por defecto)
- El uso de la macro significa que la "declaración" de la interfaz es fácilmente reconocible (y buscable), y su código se factoriza en un solo lugar, lo que facilita su modificación (un nombre con el prefijo adecuado eliminará los conflictos de nombres indeseables)
Tenga en cuenta que las otras respuestas dieron información valiosa. Gracias a todos los que lo intentaron.
Tenga en cuenta que supongo que todavía puedo poner otra recompensa por esta pregunta, y valoro las respuestas de iluminación lo suficiente como para que si vea una, abra una recompensa solo para asignarla a esa respuesta.
virtual ~VirtuallyDestructible() = 0
una herencia virtual de clases de interfaz (solo con miembros abstractos). Puede omitir eso VirtuallyDestructible, probablemente.
virtual void bar() = 0;
¿por ejemplo? Eso evitaría que su interfaz sea instanciada.