...
Parte 4. Clases
...
Este artículo es una traducción de una parte de la guía de estilo C ++ de Google al ruso.
Artículo original (bifurcación en github), traducción actualizada .
Clases
Las clases son el bloque de construcción principal en C ++. Y, por supuesto, se utilizan con frecuencia. Esta sección describe las reglas básicas y las prohibiciones a seguir al usar clases.
Código en el constructor
No llame a métodos virtuales en el constructor. Evite la inicialización que puede fallar (y no hay forma de señalar un error. Nota: Tenga en cuenta que a Google no le gustan las excepciones).
Definición
En general, cualquier inicialización se puede realizar en un constructor (es decir, toda la inicialización se puede realizar en un constructor).
Por
- No hay necesidad de preocuparse por la clase no inicializada.
- Los objetos que están completamente inicializados en el constructor pueden ser constantes y también son más fáciles de usar en contenedores y algoritmos estándar.
Vs
- Si se llaman funciones virtuales en el constructor, no se llaman las implementaciones de la clase derivada. Incluso si la clase ahora no tiene descendientes, esto puede convertirse en un problema en el futuro.
- ( ) ( ).
- , ( — ) . : bool IsValid(). .
- . , , .
Los
constructores de veredicto no deben llamar a funciones virtuales. En algunos casos (si está permitido), los errores de diseño se pueden manejar mediante la terminación del programa. De lo contrario, considere el patrón del Método de fábrica o use Init () (más detalles aquí: TotW # 42 ). Use Init () solo si el objeto tiene indicadores de estado que permiten llamar a ciertas funciones públicas (ya que es difícil trabajar completamente con un objeto parcialmente construido).
Conversiones implícitas
No declare conversiones implícitas. Utilice la palabra clave explícita para operadores de conversión de tipos y constructores de un solo argumento.
Definición
Las conversiones implícitas permiten utilizar un objeto de un tipo de origen cuando se espera otro tipo (tipo de destino), como pasar un argumento de tipo int a una función que espera un doble .
Además de las conversiones implícitas especificadas por el lenguaje de programación, también puede definir sus propias conversiones personalizadas agregando los miembros apropiados a la declaración de clase (tanto de origen como de destino). La conversión implícita del lado de la fuente se declara como operador + tipo de receptor (por ejemplo, operador bool () ). La conversión implícita en el lado del receptor es implementada por un constructor que toma el tipo fuente como único argumento (además de los argumentos predeterminados).
La palabra clave explícita se puede aplicar a un constructor o un operador de conversión para indicar explícitamente que una función solo se puede usar cuando hay una coincidencia de tipo explícita (por ejemplo, después de una conversión). Esto se aplica no solo a las conversiones implícitas, sino también a las listas de inicialización en C ++ 11:
class Foo {
explicit Foo(int x, double y);
...
};
void Func(Foo f);
Func({42, 3.14}); //
Este ejemplo de código técnicamente no es una conversión implícita, pero el lenguaje lo trata como si estuviera destinado a ser explícito .
Por
- , .
- , string_view std::string const char*.
- .
- , ( ).
- , : , .
- , .
- explicit : , .
- , . — , .
- , , , .
Los
operadores de conversión de veredicto y los constructores de un solo argumento deben declararse con la palabra clave explícita . También hay una excepción: los constructores de copiar y mover pueden declararse sin explícito , ya que no realizan conversión de tipo. Además, las conversiones implícitas pueden ser necesarias en el caso de clases contenedoras para otros tipos (en este caso, asegúrese de pedirle permiso a su administración ascendente para ignorar esta importante regla).
Los constructores que no se pueden llamar con un solo argumento se pueden declarar sin explícito . Constructores que aceptan un único estándar :: initializer_listtambién debe declararse sin explícito para admitir la inicialización de copia (por ejemplo, MyType m = {1, 2}; ).
Tipos copiables y reubicables
La interfaz pública de la clase debe indicar explícitamente la capacidad de copiar y / o mover, o viceversa, prohibir todo. Admite copiar y / o mover solo si estas operaciones tienen sentido para tu tipo.
Definición Un
tipo reubicable es aquel que se puede inicializar o asignar a partir de valores temporales.
Tipo copiable: se puede inicializar o asignar desde otro objeto del mismo tipo (es decir, el mismo que el reubicable), siempre que el objeto original permanezca sin cambios. Por ejemplo , std :: unique_ptr <int> es reubicable, pero no el tipo que se va a copiar (porque el valor del objeto std :: unique_ptr <int> de origen debe cambiar cuando se asigna al objeto de destino). En ty std :: string son ejemplos de tipos reubicables que también se pueden copiar: para int las operaciones de mover y copiar son iguales, para std :: string la operación de mover requiere menos recursos que copiar.
Para los tipos definidos por el usuario, la copia es especificada por el constructor de copia y el operador de copia El movimiento lo especifica el constructor de movimiento con el operador de movimiento o (si no está presente) por las funciones de copia correspondientes.
El compilador puede llamar implícitamente a los constructores de copiar y mover, por ejemplo, al pasar objetos por valor.
Por
Los objetos de tipos copiables y reubicables se pueden pasar y recibir por valor, lo que hace que la API sea más simple, más segura y más versátil. En este caso, no hay problemas con la propiedad del objeto, su ciclo de vida, cambio de valor, etc., y tampoco es necesario especificarlos en el "contrato" (todo esto, a diferencia de pasar objetos por puntero o referencia). También se evita la comunicación perezosa entre el cliente y la implementación, lo que hace que el código sea mucho más fácil de entender, mantener y optimizar por parte del compilador. Tales objetos se pueden usar como argumentos para otras clases que requieren pasar por valor (por ejemplo, la mayoría de los contenedores) y, en general, son más flexibles (por ejemplo, cuando se usan en patrones de diseño).
Los constructores de copiar / mover y los operadores de asignación asociados suelen ser más fáciles de definir que las alternativas como Clone () , CopyFrom () o Swap () porque el compilador puede generar las funciones requeridas (implícitamente o con = predeterminado ). Estas (funciones) son fáciles de declarar y puede estar seguro de que se copiarán todos los miembros de la clase. Los constructores (copiar y mover) son generalmente más eficientes porque no requieren asignación de memoria, inicialización separada, asignaciones adicionales, están bien optimizados (ver elisión de copia ).
Los operadores de movimiento le permiten manipular de manera eficiente (e implícita) los recursos rvalue de los objetos. A veces, esto facilita la codificación.
En contra
No es necesario que algunos tipos se puedan copiar y la compatibilidad con las operaciones de copia puede ser contraria a la intuición o provocar un comportamiento incorrecto. Los tipos de singletones ( Registrador ), los objetos para limpiar (por ejemplo, cuando se sale del alcance) ( Limpieza ) o que contienen datos únicos ( Mutex ) son, en su significado, no copiables. Además, las operaciones de copia para clases base que tienen descendientes pueden llevar a la división de objetos.... Las operaciones de copia predeterminadas (o mal escritas) pueden dar lugar a errores que son difíciles de detectar.
Los constructores de copia se llaman implícitamente y esto es fácil de pasar por alto (especialmente para los programadores que previamente escribieron en lenguajes donde los objetos se pasan por referencia). También puede reducir el rendimiento haciendo copias innecesarias.
Veredicto La
interfaz pública de cada clase debe indicar explícitamente qué operaciones de copia y / o movimiento admite. Esto generalmente se hace en la sección pública en forma de declaraciones explícitas de las funciones requeridas o declarándolas como eliminar.
En particular, la clase copiada debe declarar explícitamente operaciones de copia; solo una clase reubicable debe declarar explícitamente operaciones de movimiento; una clase que no se puede copiar o mover debe denegar explícitamente ( = eliminar ) las operaciones de copia. También se permite declarar o eliminar explícitamente las cuatro funciones de copiar y mover, aunque no es obligatorio. Si implementa el operador copiar y / o mover, también debe crear el constructor correspondiente.
class Copyable {
public:
Copyable(const Copyable& other) = default;
Copyable& operator=(const Copyable& other) = default;
// (.. )
};
class MoveOnly {
public:
MoveOnly(MoveOnly&& other);
MoveOnly& operator=(MoveOnly&& other);
// . ( ) :
MoveOnly(const MoveOnly&) = delete;
MoveOnly& operator=(const MoveOnly&) = delete;
};
class NotCopyableOrMovable {
public:
//
NotCopyableOrMovable(const NotCopyableOrMovable&) = delete;
NotCopyableOrMovable& operator=(const NotCopyableOrMovable&)
= delete;
// (), :
NotCopyableOrMovable(NotCopyableOrMovable&&) = delete;
NotCopyableOrMovable& operator=(NotCopyableOrMovable&&)
= delete;
};
Las declaraciones o eliminaciones de funciones descritas se pueden omitir en casos obvios:
- Si la clase no contiene una sección privada (por ejemplo, una estructura o una clase de interfaz), la copia y la reubicación pueden declararse a través de una propiedad similar de cualquier miembro público.
- , . , , .
- , () /, / (.. ). / . .
Un tipo no debe declararse copiable / reubicable a menos que un programador ordinario comprenda la necesidad de estas operaciones, o si las operaciones requieren muchos recursos y rendimiento. Las operaciones de movimiento para tipos copiados son siempre una optimización del rendimiento, pero por otro lado son una fuente potencial de errores y complicaciones. Por lo tanto, no declare operaciones de movimiento a menos que proporcionen ganancias de rendimiento significativas con respecto a la copia. En general, es deseable (si se declaran operaciones de copia para la clase) diseñar todo para que se utilicen las funciones de copia predeterminadas. Y asegúrese de verificar la corrección de cualquier operación por defecto.
Debido al riesgo de "cortar", es preferible evitar las declaraciones públicas de copia y movimiento para las clases que planea usar como clases base (y preferiblemente no heredar de una clase con tales funciones). Si necesita hacer que la clase base se pueda copiar, haga una función virtual pública Clone () y un constructor de copia protegido para que la clase derivada pueda usarlos para implementar operaciones de copia.
Estructuras vs clases
Utilice estructuras solo para objetos pasivos que almacenan datos. En otros casos, use las clases ( clase ).
Las palabras clave de estructura y clase son casi idénticas en C ++. Sin embargo, tenemos nuestra propia comprensión de cada palabra clave, así que use la que se adapte a su propósito y significado.
Las estructuras deben usarse para objetos pasivos, solo para transferencia de datos. Pueden tener constantes propias, pero no debería haber ninguna funcionalidad (con la posible excepción de las funciones get / set). Todos los campos deben ser públicos, disponibles para acceso directo, y esto es preferible a usar las funciones get / set. Las estructuras no deben contener invariantes (por ejemplo, valores calculados) que se basan en dependencias entre diferentes campos de la estructura: la capacidad de modificar directamente los campos puede invalidar el invariante. Los métodos no deben restringir el uso de la estructura, pero pueden asignar valores a los campos: p. Ej. como constructor, destructor o funciones Initialize () , Reset () .
Si se requiere funcionalidad adicional en el procesamiento de datos o invariantes, es preferible utilizar las clases ( clase ). Además, si tienes dudas sobre qué elegir, usa clases.
En algunos casos ( metafunciones de plantilla , rasgos, algunos functores) por coherencia con el STL, se permite utilizar estructuras en lugar de clases.
Recuerde que las variables en estructuras y clases se nombran con diferentes estilos.
Estructuras vs pares y tuplas
Si los elementos individuales en un bloque de datos se pueden nombrar de manera significativa, entonces es deseable usar estructuras en lugar de pares o tuplas.
Mientras que utilizando pares y tuplas evita reinventar la rueda con su propio tipo y le ahorrará mucho tiempo la escritura de código, los campos con nombres significativos (en lugar de .First , .SECOND, o std :: get <X> ) será más fácil de leer cuando la lectura del código. Y aunque C ++ 14 agrega acceso de tipo ( std :: get <Type> , y el tipo debe ser único) además del acceso al índice para tuplas , el nombre del campo es mucho más informativo que el tipo.
Los pares y las tuplas están bien en el código donde no hay una distinción especial entre los elementos de un par o tupla. También deben trabajar con código o API existentes.
Herencia
La composición de clases suele ser más apropiada que la herencia. Cuando use herencia, hágalo público .
Definición
Cuando una clase secundaria hereda de una clase base, incluye las definiciones de todos los datos y operaciones de la base. La herencia de la interfaz es la herencia de una clase base abstracta pura (no se definen estados ni métodos en ella). Todo lo demás es "herencia de implementación".
Por
La herencia de implementación reduce el tamaño del código al reutilizar partes de la clase base (que se convierte en parte de la nueva clase). Porque La herencia es una declaración en tiempo de compilación que permite al compilador comprender la estructura y encontrar errores. La herencia de la interfaz se puede usar para que la clase admita la API requerida. Y también, el compilador puede encontrar errores si la clase no define el método requerido de la API heredada.
Contras
En el caso de la herencia de la implementación, el código comienza a difuminarse entre las clases base y secundaria y esto puede complicar la comprensión del código. Además, la clase secundaria no puede anular el código de funciones no virtuales (no puede cambiar su implementación).
La herencia múltiple es aún más problemática y, a veces, conduce a una degradación del rendimiento. A menudo, la penalización del rendimiento al pasar de una herencia única a una herencia múltiple puede ser mayor que la transición de las funciones normales a las virtuales. También es un paso de la herencia múltiple a la herencia rómbica, y esto ya genera ambigüedad, confusión y, por supuesto, errores.
Veredicto
Cualquier herencia debe ser pública . Si desea que sea privado , es mejor agregar un nuevo miembro con una instancia de la clase base.
No abuse de la herencia de implementación. A menudo se prefiere la composición de clase. Trate de limitar el uso de la semántica de herencia "is»: Bar , puede heredar del Foo , si puedo decir que el Bar "es» el Foo (es decir, donde se usó el Foo , también puede usar el Bar ).
Protegido ( protegido ) realiza solo aquellas funciones que deberían estar disponibles para las clases secundarias. Tenga en cuenta que los datos deben ser privados.
Declare explícitamente anulaciones de funciones / destructores virtuales utilizando especificadores: anulación o (si es necesario) final . No utilice el especificador virtual al anular funciones. Explicación: Una función o destructor que está marcado como anulado o final, pero no es virtual, simplemente no se compilará (lo que ayuda a detectar errores comunes). También los especificadores funcionan como documentación; y si no hay especificadores, el programador se verá obligado a verificar toda la jerarquía para aclarar la virtualidad de la función.
Se permite la herencia múltiple, sin embargo, la herencia múltiple la implementación no se recomienda de la palabra en absoluto.
Sobrecarga del operador
Sobrecargue a los operadores lo más razonablemente posible. No use literales personalizados.
La determinación del
código C ++ permite al usuario anular los operadores incorporados utilizando la palabra clave operador y el tipo de usuario como uno de los parámetros; también operador le permite definir nuevos literales usando el operador "" ; también puede crear funciones de conversión como operator bool () .
Por
El uso de la sobrecarga del operador para tipos definidos por el usuario (similar a los tipos integrados) puede hacer que su código sea más conciso e intuitivo. Los operadores sobrecargados corresponden a ciertas operaciones (por ejemplo, == , < , = y << ) y si el código sigue la lógica de aplicar estas operaciones, entonces los tipos definidos por el usuario se pueden aclarar y utilizar cuando se trabaja con bibliotecas externas que dependen de estas operaciones.
Los literales personalizados son una forma muy eficaz de crear objetos personalizados.
Vs
- (, ) — , , .
- , .
- , , .
- , , .
- , .
- / ( ), «» . , foo < bar &foo < &bar; .
- . & , . &&, || , () ( ) .
- , . , .
- (UDL) , C++ . : «Hello World»sv std::string_view(«Hello World»). , .
- Porque no se especifica ningún espacio de nombres para el UDL, necesitará usar la directiva using (que está prohibida ) o la declaración using (que también está prohibida (en los archivos de encabezado) , a menos que los nombres importados sean parte de la interfaz que se muestra en el archivo de encabezado). Para tales archivos de encabezado, es mejor evitar los sufijos UDL y es deseable evitar dependencias entre literales que son diferentes en el encabezado y el archivo de origen.
Veredicto
Defina operadores sobrecargados solo si su significado es claro, comprensible y consistente con la lógica general. Por ejemplo, utilice | en el sentido de la operación OR; implementar lógica de tubería en su lugar no es una buena idea.
Defina operadores solo para sus propios tipos, hágalo en el mismo encabezado y archivo de origen, y en el mismo espacio de nombres. Como resultado, los operadores estarán disponibles en el mismo lugar que los tipos mismos, y el riesgo de múltiples definiciones es mínimo. Siempre que sea posible, evite definir operadores como plantillas. tienes que hacer coincidir cualquier conjunto de argumentos de plantilla. Si define un operador, defina también "hermanos". Y cuida la consistencia de los resultados que devuelven. Por ejemplo, si define el operador < , defina todos los operadores de comparación y asegúrese de que los operadores < y > nunca devuelvan verdadero para los mismos argumentos.
Es deseable definir operadores binarios inmutables como funciones externas (no miembros). Si el operador binario se declara miembro de la clase, la conversión implícita se puede aplicar al argumento derecho, pero no al izquierdo. Esto puede ser un poco frustrante para los programadores si (por ejemplo) el código a <b se compilará, pero b <a no.
No es necesario intentar omitir las anulaciones del operador. Si se requiere comparación (o función de asignación y salida), entonces es mejor definir == (o = y << ) en lugar de Equals () , CopyFrom () y PrintTo () . Por el contrario, no es necesario volver a definir un operador solo porque las bibliotecas externas lo esperan. Por ejemplo, si el tipo de datos no se puede ordenar y desea almacenarlo en std :: set , entonces es mejor hacer una función de comparación personalizada y no usar el operador < .
No anule && , || , , (Coma) o unario y . No anule el operador "" , es decir no introduzca sus propios literales. No utilice literales previamente definidos (incluida la biblioteca estándar y otros).
Información adicional: la
conversión de tipos se describe en la sección sobre conversiones implícitas . El operador = está escrito en el constructor de copias . El tema de la sobrecarga << para trabajar con flujos se trata en flujos . También puede familiarizarse con las reglas de la sección sobre sobrecarga de funciones , que también son adecuadas para operadores.
Accediendo a los miembros de la clase
Haga siempre privados los datos de la clase , excepto las constantes . Esto simplifica el uso de invariantes al agregar las funciones de acceso más simples (a menudo constantes).
Está permitido declarar los datos de la clase como protegidos para su uso en clases de prueba (por ejemplo, cuando se usa Google Test ) u otros casos similares.
Procedimiento de anuncio
Coloque anuncios similares en un solo lugar, muestre partes comunes.
La definición de clase por lo general comienza con una sección de la opinión pública: , va más protegida: y luego lo privado: . No especifique secciones vacías.
Dentro de cada sección, agrupe declaraciones similares. El orden preferido son los tipos (incluyendo typedef , using , clases y estructuras anidadas), constantes, métodos de fábrica, constructores, operadores de asignación, destructores, otros métodos, miembros de datos.
No coloque definiciones de métodos voluminosos en la definición de clase. Por lo general, solo los métodos triviales, muy cortos o críticos para el rendimiento están "integrados" en la definición de clase. Consulte también Funciones en línea .