c++ - ¿Es CppCoreGuidelines C.21 correcto?
oop c++11 (2)
Mientras leía las CoreCppGuidelines de Bjarne Stroustrup, he encontrado una guía que contradice mi experiencia.
El C.21 requiere lo siguiente:
Si define o
=delete
cualquier operación predeterminada, defina o=delete
todos
Con el siguiente motivo:
La semántica de las funciones especiales está estrechamente relacionada, por lo que, si uno necesita ser no predeterminado, las probabilidades son que otros también necesiten modificaciones.
Desde mi experiencia, las dos situaciones más comunes de redefinición de las operaciones predeterminadas son las siguientes:
# 1: Definición de destructor virtual con cuerpo predeterminado para permitir la herencia:
class C1
{
...
virtual ~C1() = default;
}
# 2: Definición del constructor predeterminado que realiza una inicialización de miembros con tipo de RAII:
class C2
{
public:
int a; float b; std::string c; std::unique_ptr<int> x;
C2() : a(0), b(1), c("2"), x(std::make_unique<int>(5))
{}
}
Todas las demás situaciones fueron raras en mi experiencia.
¿Qué piensas de estos ejemplos? ¿Son excepciones de la regla C.21 o es mejor definir aquí todas las operaciones predeterminadas? ¿Hay alguna otra excepción frecuente?
Tengo reservas significativas con esta guía. Incluso sabiendo que es una guía , y no una regla , todavía tengo reservas.
Supongamos que tiene una clase escrita por el usuario similar a std::complex<double>
, o std::chrono::seconds
. Es solo un tipo de valor. No posee ningún recurso, está destinado a ser simple. Digamos que tiene un constructor de miembro no especial.
class SimpleValue
{
int value_;
public:
explicit SimpleValue(int value);
};
Bueno, también quiero que SimpleValue
sea construible por defecto, y he inhibido el constructor predeterminado al proporcionar otro constructor, así que necesito agregar ese miembro especial :
class SimpleValue
{
int value_;
public:
SimpleValue();
explicit SimpleValue(int value);
};
Me temo que la gente memorizará esta guía y razonará: Bueno, dado que he proporcionado un miembro especial, debería definir o eliminar el resto, así que aquí va ...
class SimpleValue
{
int value_;
public:
~SimpleValue() = default;
SimpleValue();
SimpleValue(const SimpleValue&) = default;
SimpleValue& operator=(const SimpleValue&) = default;
explicit SimpleValue(int value);
};
Hmm ... No necesito mover miembros, pero necesito seguir sin pensar lo que los sabios me han dicho, así que simplemente los eliminaré:
class SimpleValue
{
int value_;
public:
~SimpleValue() = default;
SimpleValue();
SimpleValue(const SimpleValue&) = default;
SimpleValue& operator=(const SimpleValue&) = default;
SimpleValue(SimpleValue&&) = delete;
SimpleValue& operator=(SimpleValue&&) = delete;
explicit SimpleValue(int value);
};
Me temo que CoreCppGuidelines C.21 dará lugar a una tonelada de código que se ve así. ¿Por qué es eso malo? Un par de razones:
1.
Esto es mucho más difícil de leer que esta versión correcta:
class SimpleValue
{
int value_;
public:
SimpleValue();
explicit SimpleValue(int value);
};
2.
Está roto . Descubrirá la primera vez que intenta devolver un SimpleValue
de una función por valor:
SimpleValue
make_SimpleValue(int i)
{
// do some computations with i
SimpleValue x{i};
// do some more computations
return x;
}
Esto no compilará El mensaje de error dirá algo sobre el acceso a un miembro eliminado de SimpleValue
.
Tengo algunas mejores pautas:
1.
Sepa cuándo el compilador está incumpliendo o eliminando miembros especiales para usted, y qué harán los miembros incumplidos.
Este cuadro puede ayudar con eso:
Si este cuadro es demasiado complejo, lo entiendo. Es complejo. Pero cuando se te explica un poco a la vez, es mucho más fácil lidiar con eso. Espero estar actualizando esta respuesta dentro de una semana con un enlace a un video donde explico esta tabla. Aquí está el enlace a la explicación, después de un retraso más largo de lo que me hubiera gustado (mis disculpas): https://www.youtube.com/watch?v=vLinb2fgkHk
2.
Siempre defina o elimine un miembro especial cuando la acción implícita del compilador no sea correcta.
3.
No dependa del comportamiento obsoleto (los recuadros rojos en el cuadro de arriba). Si declara el destructor, el constructor de copia o el operador de asignación de copia, declare el constructor de copia y el operador de asignación de copia.
4.
Nunca elimine los miembros de movimiento. Si lo haces, en el mejor de los casos será redundante. En el peor, romperá su clase (como en el ejemplo de SimpleValue
arriba). Si eliminas los miembros del movimiento, y es el caso redundante, obligarás a tus lectores a revisar constantemente tu clase para asegurarte de que no sea el caso roto.
5.
Bríndele cariño y cariño a cada uno de los 6 miembros especiales, incluso si el resultado es dejar que el compilador lo maneje por usted (quizás inhibiéndolo o eliminándolo implícitamente).
6.
Ponga a sus miembros especiales en un orden consistente en la parte superior de su clase (solo aquellos que desea declarar explícitamente) para que sus lectores no tengan que ir a buscarlos. Tengo mi pedido favorito, si su pedido preferido es diferente, está bien. Mi orden preferida es la que utilicé en el ejemplo de SimpleValue
.
Creo que tal vez su segundo ejemplo sea una excepción razonable, y, después de todo, la directriz dice "las probabilidades son ...", por lo que habrá algunas excepciones.
Me pregunto si esta diapositiva podría ayudar con su primer ejemplo:
Estas son las diapositivas: https://accu.org/content/conf2014/Howard_Hinnant_Accu_2014.pdf
EDITAR: Para obtener más información sobre el primer caso, he descubierto esto: Destructores virtuales C ++ 11 y generación automática de funciones especiales de movimiento