10 cosas pequeñas que facilitan C ++
29/01/2019
Autor / Editor: Dominik Berner* / Sebastian Gerstl
C ++ ha modernizado notablemente la introducción de los estándares C ++ 14/11/17.Además de las características del lenguaje, como puntos inteligentes, mueven semántica y plantilla Varaidica, también hay muchas extensiones más pequeñas que a menudo vuelan debajo del radar.Pero son precisamente estas características las que pueden ayudar a simplificar notablemente el código C ++ y hacer que sea más espera.
Empresas sobre el tema
BBV Software Services AG
Los "grandes" cambios, como la plantilla variádica de, el automóvil, la semántica de movimiento, las expresiones de lambda y otros han proporcionado mucho material de discusión y, por lo tanto, se conocen mucho.Además de las características del idioma, la biblioteca estándar también ha experimentado una expansión notable y muchos conceptos de bibliotecas como Boost se han estandarizado.Además de estas características muy notables (y a veces también controvertidas), hay muchas extensiones pequeñas, pero finas del lenguaje, que a menudo son menos conocidas o se pasan por alto.
Precisamente, debido a que estas características son a menudo muy pequeñas y, a veces, casi invisibles, tienen un gran potencial para facilitar la vida en la vida de la programación cotidiana y modernizar suavemente el código sin intervenciones graves.A menudo es el caso de que cuando se trabaja con el código existente, no tiene la oportunidad de hacer grandes cambios estructurales o visibles desde el exterior, pero aquí es exactamente donde las "características pequeñas" pueden ayudar a mantener el código actualizado y esperar.
Código moderno de espera
El mantenimiento, la legibilidad y la calidad del código son temas que se han convertido en una parte integral del desarrollo de software actual. La ventaja del software en comparación con el hardware es que se puede adaptar y revisar relativamente fácilmente y, especialmente donde se trabaja AGIL, esto a menudo ocurre de manera muy consciente y una vez más. Con esta volatilidad, estas características de calidad tienen aún más peso, porque el código deficiente da la ventaja de un procesamiento simple más que la destrucción. Cosas como el código limpio, el príncipe sólido o los paradigmas, como el bajo acoplamiento, la cohesión fuerte son aspectos importantes de la calidad del código, pero la calidad comienza cuando se usa el lenguaje en sí. El uso de las características del idioma proporcionadas. Código para aclarar y, a menudo, también facilitarlo para verificar automáticamente estas intenciones. Además, la cantidad de código escrito a menudo se puede reducir, lo que reproduce el principio de "menos código significa menos errores".
Un ejemplo de ilustración
Un algoritmo simple puede ser muy complicado si la ortografía no cumple con las expectativas o al autor presentó un truco particularmente inteligente para la optimización.Por ejemplo, el intercambio de dos variables X e Y se puede escribir de la siguiente manera:
X = x ^ y; y = y ^ x; x = x ^ y;
Este intercambio XOR es eficiente en almacenamiento y tiene derecho a existir en casos muy específicos, pero la operación no es intuitiva.Incluso con un comentario de código, este simple ejemplo obliga al lector innecesario.El siguiente ejemplo se lee mucho más fácil:
Std :: swap (x, y);
Verifique la herencia con anulación y final
La herencia es igualmente maldición y bendición para muchos programadores.Por un lado, a menudo ayuda a evitar la duplicación del código, por otro lado, especialmente en C ++, hay muchos obstáculos que deben observarse.Especialmente con la refactorización en las clases básicas, siempre sucede que las clases dependientes se olvidan y solo lo nota en el término.La anulación de palabras clave ha sido proporcionar un remedio aquí desde C ++ 11.La anulación debe usarse siempre que una función se sobrescriba en un árbol de herencia.Esto se vuelve virtual automáticamente y el compilador tiene la posibilidad de verificar si un método está realmente sobrescribido y si el método sobrescribido es realmente virtual.
Struct base {virtual int func () {return 1;}};Struct Derived: Public Base {// compiler-Reror si base :: func no existe o no es virtual int func () anular {return 2;};};
Obtiene aún más control sobre el árbol de herencia si puede evitar completamente la herencia desde cierto punto.El especificador final indica que una clase o función virtual no se puede sobrescribir.Esto no reduce el esfuerzo de escritura, pero comunica claramente una intención detrás de un código, a saber, que no es deseable una mayor herencia.Incluso el compilador ayuda con el fallado de la compilación, debe intentarlo.
Base de clase final {};Clase Derived: Public Base {} // Compiler Error Class Base {:::: Virtual void f ();};Clase derivada: la base pública {// f no puede ser anulada por más clases base nulo f () anular final;};
Uso de declaraciones y herencia de constructores
El código de duplicación es un horror para el programador, incluso si este se genera código.El uso de declaraciones permite al programador "importar" un símbolo de una región declarativa, como salas de nombres, clases y estructuras en otro sin generar código adicional.En las clases, esto es particularmente útil para hacerse cargo de los constructores de las clases básicas directamente, sin que todas las variantes tengan que reescribirse.Otro ejemplo es diseñar explícitamente implementaciones en clases derivadas.Esto indica claramente al lector que se utiliza una implementación "extranjera" aquí que no ha experimentado ninguna modificación funcional.
Struct a {a () {} explícito a (char c {} int get_x (); int func ();} struct B: public A {usando a :: a; // Obtenga todos los constructores de A usando a :: func ; int func (int); // posiblemente podría enmascarar a :: func () privado: usando a :: get_x; //}
Esto ha estado trabajando para clases y estructuras durante mucho tiempo;Desde C ++ 17, hacerse cargo de símbolos también ha estado funcionando para salas de nombres (anidadas):
Void f () {} Namespace x {void x () {};void y () {};void z () {};} Espacio de nombres i :: k :: l} usando :: f;// f () está disponible en i :: k :: l ahora usando x :: x;// x () está disponible en i :: k :: l}
Reenvío de constructores
Otros lenguajes de programación de alto nivel han conocido la "cadena" de los constructores durante mucho tiempo y desde C ++ 11 esto también ha sido posible en C ++.Las ventajas del código menos duplicado y, por lo tanto, una legibilidad más fácil y, por lo tanto, una mejor mantenimiento son obvias.Especialmente con los constructores, las iniciales y/o cheques de los constructores internamente hacen esto ayuda mucho y promueve la implementación del paradigma RAII (asignación de recursos es inicialización).
Clase delegatingctor {int number_;Public: delegatingctor (int n): number_ (n) {} delegattingctor (): delegatingTor (42) {};}
En relación con el uso de la herencia del constructor con el uso de mencionado anteriormente, el código se puede comprimir más.
Clase base {public: base (int x): x_ {x} {};Privado: int x_;};Clase derivada: base pública {public: usando base :: base;// Importa base (int) como derivado (int) derivado (char): derivado (123) {} // delegando ctor:};
= Eliminar - eliminación de funciones
Menos código significa menos errores, incluso con el código generado.Por lo tanto, facilitamos que el compilador genere el trabajo de código que no queremos y necesitamos.La palabra clave eliminar la declaración de función, no para confundirse con la expresión correspondiente para eliminar objetos, es otra extensión muy fuerte en C ++ 11, con la cual un programador no solo puede indicar una intención, sino que también puede ser aplicada por el compilador.El uso de = Eliminar puede garantizar explícitamente que ciertas operaciones como copiar un objeto no sean y posibles.Por supuesto, la "regla de cinco" también debe observarse al eliminar las funciones.
struct NonCopyable { NonCopyable() = default; // disables copying the object through construction NonCopyable(const Dummy &) = delete; // disables copying the object through assignement Noncopyable &operator=(const Dummy &rhs) = delete; }; struct NonDefaultConstructible { // this struct can only be constructed through a move or copy NonDefaultConstructible() = delete; };
Garantizado prevenir copias
La prevención garantizada de copias (elisión de copia garantizada en inglés) generalmente es invisible para el programador, pero detrás de ella es un gran potencial para un código más pequeño y más limpio.Este reembolso previene copias innecesarias de objetos temporales si se asignan a un nuevo símbolo inmediatamente después de crear.Algunos compiladores, como GCC, han estado apoyando esto durante mucho tiempo, pero con C ++ 17, dejando las copias como un comportamiento garantizado en el estándar.Además del efecto de que se genera menos código, deja al programador para implementar su intención de que un objeto no debe copiarse o posponerse con una consecuencia aún mayor.Usando el anterior = Eliminar, esto se puede expresar muy claramente.
class A { public: A() = default; A(const A &) = delete; A(const A &&) = delete; A& operator=(const A&) = delete; A& operator=(A&&) = delete; ~A() = default; }; // Without elosion this is illegal, as it performs a copy/move //of A which has deleted copy/move ctors A f() { return A{}; } int main() { // OK, because of copy elision. Copy/move constructing // an anonymous A is not necessary A a = f(); }
Enlaces estructurados
Las clases y estructuras no son la única forma de estructurar el manejo de datos.La biblioteca estándar también proporciona una gran cantidad de contenedores de datos para exactamente estos fines.Con std :: tufle y std :: matriz, se introdujeron dos estructuras de datos en C ++ 11 con el tamaño del tamaño conocido.Mientras que STD :: Array es una modernización relativamente simple de las matrices C con std :: tuble, se creó una posibilidad genérica para entregar datos heterogéneos en el programa sin que la programación tenga que crear clases o estructuras de datos puros.
Desde C ++ 17, el acceso al contenido de estas estructuras de datos ha sido muy liviano debido a los enlaces estructurados:
auto tuple = std: :make_tuple<
1, 'a', 2.3>; const auto [a, b, c] = tuple; auto & [i, k, l] = tuple;
Zu beachten ist, dass alle Variablen hier dieselbe const-ness haben und entweder alle als Referenz oder By-Value gelesen werden. Die Enlaces estructurados funktionieren auch im Zusammenhang mit Klassen, allerdings ist dies etwas Problematisch, da die Semantik von Klassenmembers keine starke Reihenfolge der Member vorsieht. Es gibt Möglichkeiten diese Semantik zu reimplementieren, allerdings ist dies vergleichsweise aufwändig.
Enums fuertemente escritos
Una de las opciones más utilizadas para crear sus propios tipos de datos con valores claros ya estaba en C en los enums e incluso hoy en día todavía se usan a menudo.Una molestia a menudo citada es que el tipo de seguridad no se garantiza suficientemente cuando se usa enums.En el pasado, era posible asignar un valor de un tipo enum de una variable de otro tipo de enum.Con los nuevos estándares, esto es cosa del pasado cuando se usa el pasado.Si se agrega una definición de Enum a la clase o estructura de palabras clave, un tipo de datos fuertemente escrito y el uso con otro tipo de Enum conduce a una advertencia o error al compilar.Como un bono adicional, por así decirlo, el tipo de datos subyacente para un enum se ha especificado explícitamente desde C ++ 11, lo que beneficia la portabilidad del código.
Color enum: uint8_t {rojo, verde, azul};Enum Class Sound {Boing, Gloop, Crack};Auto s = sonido :: boing;
Literales de tiempo con
Un uso muy frecuente de datos con valores claros pero no siempre lineales es, por supuesto, el tiempo en sí mismo, especialmente para aplicaciones con requisitos de tiempo estrictos. El manejo de unidades de tiempo es una pesadilla para muchos programadores.Las razones son diversas, desde la división no lineal de segundos, minutos y horas hasta el hecho de que la confusión surge rápidamente, qué unidad de tiempo es una llamada como dormir (100).¿Son segundos?Milisegundos?Con la introducción de std :: crono en C ++ 11 y agregar literales de tiempo, el manejo se vuelve mucho más fácil.Con los litros, se pueden declarar tiempos con un sufijo simple en el código con una unidad fija o con una resolución fija.
Ofrece todo entre microsegundos y horas.Al usar las unidades de tiempo suministradas por std :: crono, los valores de tiempo ya se pueden convertir al tiempo de compilación y la conversión manual molesta en ese momento es parte del pasado.
using namespace std: :chrono_literals; auto seconds = 10s; auto very_small = 1us; // automatic, compile-time conversion if (very_small < seconds) { ... }
Revisiones con inicialización
A primera vista, la introducción de la inicialización directa en las declaraciones IF y Switch en C ++ 17 es una forma un poco más compacta.Otra ventaja algo oculta es que el programador solo puede expresar su intención de que un símbolo solo se use dentro de una rama.La inicialización de tener o en la condición desde o en la condición también evita el riesgo de separarse de la rama en la refactorización.
if (int i = std :: rim (); i % 2 == 0) {} switch (int i = std :: rim (); i = % 3) {caso 0: ... caso 1: .. . Caso 2: ...}
Im Zusammenhang mit den oben genannten Enlaces estructurados kann die direkte Initialisierung sehr elegant verwendet werden. Im folgenden Beispiel wird versucht ein bereits existierender Wert in einer std::map zu überschreiben. Der Rückgabewert von insert wird direkt in einen Iterator und das Flag, ob die Operation erfolgreich war entpackt und kann somit direkt innerhalb der Abfrage verwendet werden.
std: :map<
char, int> map; map[ 'a' ] = 123; if (auto [it, inserted] = map.insert ({ 'a', 1000}); !inserted) { std: :cout <<
"'a' already exists with value " << it->second <<
"\ n";}
Atributos estándar
Wann immer ein Programmierer eine Annahme trifft, sollte dies im Code dokumentiert sein. Mit den Atributos estándarn können einige solcher Annahmen mit wenig Aufwand dokumentiert werden. Attribute sind seit längerem für verschiedene Compiler bekannt, allerdings war die Notation für die verschiedenen Compiler oft unterschiedlich. Seit C++17 wurde diese als [[ attribute ]] standardisiert was portablen den Code lesbarer macht. Zudem wurden verschiedene von allen Compilern unterstützte Atributos estándar eingefügt, welche es dem Programmierer erlauben seine Absichten für gewisse Konstrukte explizit zu formulieren
[[Noreturn]] indica que una función no regresa, por ejemplo, porque siempre arroja una excepción
[[Depreciado]] [[Deprecido ("Razón")] indica que el uso de esta clase, función o variable está permitido, pero ya no se recomienda
[[Fallthrough]] Utilizado en la declaración Switch para indicar que un caso: Bloque no contiene ninguna ruptura a propósito
[[Nodiscard]] produce una advertencia de compilador si no se usa dicho valor de retorno tan marcado
[[maybe_unused]] Unterdrückt Compiler-Warnungen bei nicht verwendeten Variablen. z.B. in Debug-Code <
Conclusión
Estas 10 características y funciones pequeñas son, por supuesto, solo una pequeña parte de lo que hace que C ++ moderno.Pero a través de su aplicación constante, el código puede ser comprensible con relativamente poco esfuerzo y más fácil de entender sin la estructura completa de una base de código existente.
El autor
*Dominik Berner ist ein Senior Software-Ingenieur bei der BBV Software Services AG mit einer Leidenschaft für modernes C++. Die Wartbarkeit von Code ist für ihn kein Nebeneffekt, sondern ein primäres Qualitätsmerkmal, das für die Entwicklung von langlebiger Software unabdingbar ist.
(Esta publicación fue tomada del Congreso de Ingeniería de Software de Software 2018 del autor con el amable permiso del autor).
Archivos de artículo y enlaces de artículo
Enlace: Congreso de ingeniería de software integrado
(ID: 45706431)