Cuando aprendí C ++ hace mucho tiempo, me enfatizaba mucho que parte del punto de C ++ es que al igual que los bucles tienen "invariantes de bucle", las clases también tienen invariantes asociados a la vida útil del objeto, cosas que deberían ser ciertas mientras el objeto esté vivo. Cosas que deberían ser establecidas por los constructores y preservadas por los métodos. El control de acceso / encapsulación está ahí para ayudarlo a hacer cumplir a los invariantes. RAII es una cosa que puedes hacer con esta idea.
Desde C ++ 11 ahora tenemos semántica de movimiento. Para una clase que admite el movimiento, moverse de un objeto no termina formalmente su vida útil: se supone que el movimiento lo deja en un estado "válido".
Al diseñar una clase, ¿es una mala práctica si la diseñas para que los invariantes de la clase solo se conserven hasta el punto desde el que se mueve? ¿O está bien si te permitirá hacerlo más rápido?
Para hacerlo concreto, supongamos que tengo un tipo de recurso no copiable pero movible como este:
class opaque {
opaque(const opaque &) = delete;
public:
opaque(opaque &&);
...
void mysterious();
void mysterious(int);
void mysterious(std::vector<std::string>);
};
Y por alguna razón, necesito hacer un contenedor copiable para este objeto, para que pueda usarse, tal vez en algún sistema de envío existente.
class copyable_opaque {
std::shared_ptr<opaque> o_;
copyable_opaque() = delete;
public:
explicit copyable_opaque(opaque _o)
: o_(std::make_shared<opaque>(std::move(_o)))
{}
void operator()() { o_->mysterious(); }
void operator()(int i) { o_->mysterious(i); }
void operator()(std::vector<std::string> v) { o_->mysterious(v); }
};
En este copyable_opaque
objeto, una invariante de la clase establecida en la construcción es que el miembro o_
siempre apunta a un objeto válido, ya que no existe un ctor predeterminado, y el único ctor que no es un copiador lo garantiza. Todos los operator()
métodos suponen que esta invariante se mantiene y la conservan después.
Sin embargo, si se mueve el objeto, entonces no o_
apuntará a nada. Y después de ese punto, llamar a cualquiera de los métodos operator()
provocará un bloqueo UB / a.
Si el objeto nunca se mueve, entonces la invariante se conservará hasta la llamada dtor.
Supongamos que, hipotéticamente, escribí esta clase, y meses después, mi compañero de trabajo imaginario experimentó UB porque, en alguna función complicada donde muchos de estos objetos se barajaban por alguna razón, se movió de una de estas cosas y luego llamó a una de sus métodos Claramente es su culpa al final del día, pero ¿esta clase está "mal diseñada"?
Pensamientos:
Por lo general, es una mala forma en C ++ crear objetos zombies que explotan si los tocas.
Si no puede construir algún objeto, no puede establecer los invariantes, entonces arroje una excepción desde el ctor. Si no puede preservar los invariantes en algún método, entonces señale un error de alguna manera y retroceda. ¿Debería ser diferente para los objetos movidos?¿Es suficiente simplemente documentar "después de que se haya movido este objeto, es ilegal (UB) hacer algo con él que no sea destruirlo" en el encabezado?
¿Es mejor afirmar continuamente que es válido en cada llamada al método?
Al igual que:
class copyable_opaque {
std::shared_ptr<opaque> o_;
copyable_opaque() = delete;
public:
explicit copyable_opaque(opaque _o)
: o_(std::make_shared<opaque>(std::move(_o)))
{}
void operator()() { assert(o_); o_->mysterious(); }
void operator()(int i) { assert(o_); o_->mysterious(i); }
void operator()(std::vector<std::string> v) { assert(o_); o_->mysterious(v); }
};
Las afirmaciones no mejoran sustancialmente el comportamiento y provocan una desaceleración. Si su proyecto utiliza el esquema de "versión de compilación / depuración de compilación", en lugar de simplemente ejecutarse con aserciones, supongo que esto es más atractivo, ya que no paga las verificaciones en la versión de compilación. Si en realidad no tiene compilaciones de depuración, esto parece bastante poco atractivo.
- ¿Es mejor hacer que la clase sea copiable, pero no móvil?
Esto también parece malo y causa un impacto en el rendimiento, pero resuelve el problema "invariante" de una manera directa.
¿Cuáles consideraría que son las "mejores prácticas" relevantes aquí?