Una vez fui entrevistado para el puesto de desarrollador de C ++ en una oficina decente e incluso conocida. Ya tenía algo de experiencia, incluso en ese momento me llamaron desarrollador líder de mi empleador. Pero cuando me preguntaron si sabía cosas como DRY, KISS, YAGNI, NIH, tuve que responder "No" una y otra vez.
Fallé miserablemente, por supuesto. Pero luego las abreviaturas anteriores fueron buscadas en Google y recordadas. Mientras leía artículos y libros temáticos, me preparaba para entrevistas y solo hablaba con colegas, aprendí más cosas nuevas, las olvidé, busqué en Google nuevamente y entendí. Hace un par de meses, uno de mis colegas mencionó casualmente en el chat de trabajo de IIFE en el contexto de C ++. Como ese abuelo en una broma, casi me caigo de la estufa y nuevamente me metí en Google.

Fue entonces cuando decidí componer (principalmente para mí) una hoja de trucos para abreviaturas que son útiles para que un desarrollador de C ++ conozca. Esto no significa que se apliquen solo a C ++, o que sean conceptos generales de C ++ (puede escribir volúmenes sobre expresiones idiomáticas). No, estos son solo conceptos que realmente encontré en el trabajo y las entrevistas, generalmente expresados en forma de abreviaturas. Bueno, me perdí cosas absolutamente triviales como LIFO, FIFO, CRUD, OOP, GCC y MSVC.
Sin embargo, las abreviaturas aparecieron decentemente, por lo que dividí la hoja de trucos en 2 partes: fuertemente característica de C ++ y más común. Cuando era apropiado, agrupé los conceptos, de lo contrario simplemente los enumeré alfabéticamente. En general, no tiene mucho sentido en su orden.
Cosas básicas:•
ODR•
POD•
POF•
PIMPL•
RAII•
RTTI•
STL•
UBSutilezas del lenguaje:•
ADL•
CRTP•
CTAD•
EBO•
IIFE•
NVI•
RVO y NRVO•
SFINAE•
SBO, SOO, SSOACTUALIZACIÓN•
CV•
LTO•
PCH•
PGO•
SEH / VEH•
TMP•
VLACosas basicas
ODR
Regla de una definición. La regla de una definición. Simplificado significa lo siguiente:
- Dentro de una sola unidad de traducción, cada variable, función, clase, etc., no puede tener más de una definición. Hay tantos anuncios como sea posible (excepto transferencias sin un tipo base dado, que simplemente no se pueden declarar sin definir), pero no más de una definición. Menos posible si la entidad no se utiliza.
- En todo el programa, cada función y variable no en línea utilizada debe tener exactamente una definición. Cada función y variable en línea utilizada debe tener una definición en cada unidad de traducción.
- Algunas entidades, por ejemplo, clases, funciones en línea y una variable, plantillas, enumeraciones, etc., pueden tener varias definiciones en un programa (pero no más de una en una unidad de traducción). En realidad, esto sucede cuando el mismo encabezado que contiene una clase completamente implementada, por ejemplo, está conectado a varios archivos .cpp. Pero estas definiciones deberían coincidir (simplifico enormemente, pero la esencia es esta). De lo contrario, será UB .
El compilador detectará fácilmente una violación de
ODR dentro de una unidad de traducción. Pero no podrá hacer nada si se viola la regla a escala de programa, aunque solo sea porque el compilador procesa una unidad de traducción a la vez.
El vinculador puede encontrar muchas más violaciones, pero, estrictamente hablando, no está obligado a hacer esto (porque, según el Estándar,
UB está aquí) y puede pasar por alto algo. Además, el proceso de búsqueda de violaciones de
ODR en la etapa de vinculación tiene una complejidad cuadrática, y el ensamblaje del código C ++ no es tan rápido.
Como resultado, la responsabilidad principal del cumplimiento de esta regla (especialmente a nivel de programa) es el propio desarrollador. Y sí, solo las entidades con un enlace externo pueden violar la
ODR a escala de programa; los del interior (es decir, definidos en espacios de nombres anónimos) no participan en este carnaval.
Leer más: una
vez (inglés) ,
dos (inglés)Vaina
Datos antiguos simples. Estructura de datos simple. La definición más simple: esta es una estructura tal que puede, como es, en forma binaria enviar / recibir desde la biblioteca C. O, que es lo mismo, copie correctamente con
memcpy
simple.
De estándar a estándar, la definición completa ha cambiado en detalle. El último C ++ 17
POD actualmente define cómo
- tipo escalar
- o una clase / estructura / unión que:
- hay una clase trivial
- hay una clase con un dispositivo estándar
- no contiene campos no estáticos POD
- o una variedad de estos tipos
Clase trivial:
- tiene al menos uno no eliminado:
- constructor predeterminado
- constructor de copia
- constructor en movimiento
- operador de asignación de copia
- operador de asignación de movimiento
- Todos los constructores predeterminados que copian y mueven constructores y operadores de asignación son triviales (simplificados, generados por el compilador) o remotos
- tiene un destructor no remoto trivial
- Todos los tipos base y todos los campos de los tipos de clase tienen destructores triviales
- sin métodos virtuales (incluido el destructor)
- sin tipos de base virtual
Clase con un dispositivo estándar (clase de diseño estándar):
Sin embargo, en C ++ 20 ya no habrá el concepto de tipo
POD , solo permanecerá el tipo trivial y el tipo con el dispositivo estándar.
Leer más:
uno (ruso) ,
dos (inglés) ,
tres (inglés)POF
Función antigua simple. Una función simple de estilo C. Mencionada en el Estándar anterior a C ++ 14 inclusive solo en el contexto de los manejadores de señal. Los requisitos para ello son:
- usa solo cosas comunes a C y C ++ (es decir, sin excepciones y
try-catch
, por ejemplo) - no causa directa o indirectamente funciones que no sean POF , con la excepción de operaciones atómicas sin bloques (
std::atomic_init
, std::atomic_fetch_add
, etc.)
El Estándar solo permite que tales funciones, que también tienen un enlace C (
extern "C"
), se usen como manejadores de señal. El soporte para otras funciones depende del compilador.
En C ++ 17, el concepto de
POF desaparece, en lugar de aparecer una evaluación segura de señal. En tales cálculos están prohibidos:
- llamadas a todas las funciones de la biblioteca estándar, excepto atómica, sin bloqueo
new
y delete
llamadas- usando
dynamic_cast
- llamar a la entidad
thread_local
- cualquier trabajo con excepciones
- inicialización de una variable estática local
- esperando que se complete la inicialización de la variable estática
Si el manejador de señal hace algo de lo anterior, el Estándar promete
UB .
Leer más:
tiempo (inglés)PIMPL
Puntero a la implementación. Puntero a la implementación. El modismo clásico en C ++, también conocido como puntero d, puntero opaco, firewall de compilación. Consiste en el hecho de que todos los métodos privados, campos y otros detalles de implementación de una determinada clase se asignan a una clase separada, y solo los métodos públicos (es decir, una interfaz) y un puntero a una instancia de esta nueva clase separada permanecen en la clase original. Por ejemplo:
foo.hpp class Foo { public: Foo(); ~Foo(); void doThis(); int doThat(); private: class Impl; std::unique_ptr<Impl> pImpl_; };
foo.cpp #include "foo.hpp" class Foo::Impl {
Por qué es esto necesario, es decir, ventajas:
- Encapsulación: los usuarios de la clase a través de la conexión de encabezado obtienen solo lo que necesitan: una interfaz pública. Si los detalles de implementación cambian, no es necesario volver a compilar el código del cliente (ver ABI ).
- Tiempo de compilación: dado que el encabezado público no sabe nada sobre la implementación, no incluye los muchos encabezados que necesita. En consecuencia, se reduce el número de encabezados conectados implícitamente en el código del cliente. La búsqueda de nombres y la resolución de sobrecargas también se simplifica, porque el encabezado público no contiene miembros privados (aunque son privados, participan en estos procesos).
Precio, es decir, desventajas:
Algunas de estas deficiencias son removibles, pero el precio complica aún más el código e introduce niveles adicionales de abstracción (ver
FTSE ).
Leer más:
uno (ruso) ,
dos (ruso) ,
tres (inglés)RAII
La adquisición de recursos es la inicialización. Capturar un recurso es la inicialización. El significado de este idioma es que la retención de un determinado recurso dura toda la vida del objeto correspondiente. La captura del recurso ocurre en el momento de la creación / inicialización del objeto, la liberación, en el momento de la destrucción / finalización del mismo objeto.
Curiosamente (principalmente para programadores de C ++), este idioma también se usa en otros lenguajes, incluso aquellos con un recolector de basura. En Java es
try--
, en Python la instrucción
with
, en C # la directiva
using
, en Go the
defer
. Pero es en C ++ con su vida de objetos absolutamente predecible que
RAII encaja especialmente orgánicamente.
En C ++, un recurso generalmente se captura en el constructor y se libera en el destructor. Por ejemplo, los punteros inteligentes controlan la memoria de esta manera, las secuencias de archivos administran los archivos, mutex bloquea los mutexes. Lo bueno es que no importa cómo se salga el bloque (alcance), es normal a través de cualquiera de los puntos de salida, o se lanzó una excepción, el objeto de control de recursos creado en este bloque se destruirá y el recurso se liberará. Es decir Además de encapsular
RAII en C ++, también ayuda a garantizar la seguridad en el sentido de excepciones.
Limitaciones, donde sin ellas. Los destructores en C ++ no devuelven valores y categóricamente no deberían arrojar excepciones. En consecuencia, si la liberación del recurso va acompañada de uno u otro, será necesario implementar una lógica adicional en el destructor del objeto de control.
Leer más:
una vez (ruso) ,
dos (inglés)RTTI
Información de tipo de tiempo de ejecución. Identificación de tipo en tiempo de ejecución. Este es un mecanismo para obtener información sobre el tipo de un objeto o expresión en tiempo de ejecución. Existe en otros lenguajes, pero en C ++ se usa para:
dynamic_cast
typeid
y type_info
- capturar excepción
Una limitación importante:
RTTI utiliza una tabla de funciones virtuales y, por lo tanto, solo funciona para tipos polimórficos (un destructor virtual es suficiente). Una explicación importante:
dynamic_cast
y
typeid
no siempre usan
RTTI , y por lo tanto funcionan para tipos no polimórficos. Por ejemplo, para emitir dinámicamente un enlace a un descendiente a un enlace a un antepasado,
RTTI no
es necesario; toda la información está disponible en tiempo de compilación.
RTTI no
es gratuito, aunque sea un poco, pero afecta negativamente el rendimiento y el tamaño de la memoria consumida (de ahí el consejo frecuente de no utilizar
dynamic_cast
debido a su lentitud). Por lo tanto, los compiladores, como regla, le permiten deshabilitar
RTTI . GCC y MSVC prometen que esto no afectará la corrección de las excepciones de captura.
Leer más:
una vez (ruso) ,
dos (inglés)STL
Biblioteca de plantillas estándar. Biblioteca de plantillas estándar. Parte de la biblioteca estándar de C ++ que proporciona contenedores genéricos, iteradores, algoritmos y funciones auxiliares.
A pesar de su nombre conocido,
STL nunca se ha llamado así en el Estándar. De las secciones de la Norma, el
STL se puede atribuir claramente a la biblioteca de Contenedores, la biblioteca de Iteradores, la biblioteca de Algoritmo, y parcialmente a la biblioteca de Servicios generales.
En las descripciones de trabajo, a menudo puede encontrar 2 requisitos separados: conocimiento de C ++ y familiaridad con
STL . Nunca entendí esto, porque
STL es una parte integral del lenguaje desde el primer Estándar de 1998.
Leer más:
una vez (ruso) ,
dos (inglés)UB
Comportamiento indefinido. Comportamiento indefinido. Este comportamiento es en aquellos casos de error para los cuales el Estándar no tiene requisitos. Muchos de estos se enumeran explícitamente en el Estándar como conducentes a
UB . Estos incluyen, por ejemplo:
- violación de los límites de una matriz o contenedor STL
- uso de variable no inicializada
- desreferenciar un puntero nulo
- desbordamiento de entero firmado
El resultado de
UB depende de todo en una fila, tanto en la versión del compilador como en el clima en Marte. Además, este resultado puede ser cualquier cosa: un error de compilación, una ejecución correcta y un bloqueo. El comportamiento indefinido es malo, es necesario deshacerse de él.
El comportamiento indefinido, por otro lado, no debe confundirse con el
comportamiento no especificado . El comportamiento no especificado es el comportamiento correcto del programa correcto, pero que, con el permiso del Estándar, depende del compilador. Y el compilador no está obligado a documentarlo. Por ejemplo, este es el orden en que se evalúan los argumentos de una función o los detalles de implementación de
std::map
.
Bueno, aquí puede recordar el comportamiento definido por la implementación. De no especificado difiere en la disponibilidad de documentación. Ejemplo: el compilador es libre de hacer que el tipo
std::size_t
cualquier tamaño, pero debe indicar cuál.
Leer más:
uno (ruso) ,
dos (ruso) ,
tres (inglés)Las sutilezas de la lengua
ADL
Búsqueda dependiente de argumentos. Búsqueda dependiente de argumentos. Él es la búsqueda de Koenig, en honor de Andrew Koenig. Este es un conjunto de reglas para resolver nombres de funciones no calificadas (es decir, nombres sin el operador
::
:), además de la resolución de nombres habitual. En pocas palabras: el nombre de una función se busca en espacios de nombres relacionados con sus argumentos (este es el espacio que contiene el tipo del argumento, el tipo en sí mismo, si es una clase, todos sus antepasados, etc.).
Ejemplo más simple #include <iostream> namespace N { struct S {}; void f(S) { std::cout << "f(S)" << std::endl; }; } int main() { N::S s; f(s); }
La función
f
encuentra en el espacio de nombres
N
solo porque su argumento pertenece a este espacio.
Incluso el trivial
std::cout << "Hello World!\n"
usa
ADL ,
std::basic_stream::operator<<
no
std::basic_stream::operator<<
sobrecargado para
const char*
. Pero el primer argumento de esta declaración es
std::basic_stream
, y el compilador busca y encuentra una sobrecarga adecuada en el
std
.
Algunos detalles:
ADL no
es aplicable si una búsqueda regular encontró una declaración de un miembro de la clase, o una declaración de función en el bloque actual sin usar el
using
, o una declaración de una función ni una plantilla de función. O si el nombre de la función se indica entre paréntesis (el ejemplo anterior no se compila con
(f)(s)
; tendrá que escribir
(N::f)(s);
).
A veces,
ADL lo obliga a usar nombres de funciones totalmente calificados donde parecería innecesario.
Por ejemplo, este código no compila namespace N1 { struct S {}; void foo(S) {}; } namespace N2 { void foo(N1::S) {}; void bar(N1::S s) { foo(s); } }
Leer más:
uno (inglés) ,
dos (inglés) ,
tres (inglés)CRTP
Patrón de plantilla curiosamente recurrente. Extraño patrón recursivo. La esencia de la plantilla es la siguiente:
- alguna clase hereda de la clase de plantilla
- la clase descendiente se usa como parámetro de plantilla de su clase base
Es más fácil dar un ejemplo:
template <class T> struct Base {}; struct Derived : Base<Derived> {};
CRTP es un excelente ejemplo de polimorfismo estático. La clase base proporciona una interfaz; las clases derivadas proporcionan una implementación. Pero a diferencia del polimorfismo ordinario, no hay gastos generales para crear y usar una tabla de funciones virtuales.
Ejemplo template <typename T> struct Base { void action() const { static_cast<T*>(this)->actionImpl(); } }; struct Derived : Base<Derived> { void actionImpl() const { ... } }; template <class Arg> void staticPolymorphicHandler(const Arg& arg) { arg.action(); }
Cuando se usa correctamente,
T
siempre
T
un descendiente de
Base
, por lo que
static_cast
es suficiente para
static_cast
. Sí, en este caso, la clase base conoce la interfaz descendiente.
Otra área común de uso para
CRTP es la extensión (o estrechamiento) de la funcionalidad de las clases heredadas (algo llamado mixin en algunos idiomas). Quizás los ejemplos más famosos:
struct Derived : singleton<Derived> { … }
struct Derived : private boost::noncopyable<Derived> { … }
struct Derived : std::enable_shared_from_this<Derived> { … }
struct Derived : counter<Derived> { … }
- cuenta el número de objetos creados y / o existentes
Desventajas, o más bien, momentos que requieren atención:
- No existe una clase base común, no puede crear una colección de descendientes diferentes y acceder a ellos mediante un puntero al tipo base. Pero si lo desea, puede heredar Base del tipo polimórfico habitual.
- Hay una oportunidad adicional para tirar el pie por descuido:
Ejemplo template <typename T> struct Base {}; struct Derived1 : Base<Derived1> {}; struct Derived2 : Base<Derived1> {};
Pero puedes agregar protección:
private: Base() = default; friend T;
- Porque Como todos los métodos no son virtuales, los métodos del descendiente ocultan los métodos de la clase base con los mismos nombres. Por lo tanto, es mejor llamarlos de manera diferente.
- En general, los descendientes tienen métodos públicos que no deben usarse en ningún otro lugar, excepto en la clase base. Esto no es bueno, pero se corrige mediante un nivel adicional de abstracción (ver FTSE ).
Leer más:
una vez (ruso) ,
dos (inglés)CTAD
Deducción de argumento de plantilla de clase. Inferir automáticamente el tipo del parámetro de plantilla de clase. Esta es una nueva característica de C ++ 17. Anteriormente, solo se mostraban automáticamente los tipos de variables (
auto
) y los parámetros de la plantilla de función, por lo que
std::make_tuple
funciones auxiliares como
std::make_pair
,
std::make_tuple
, etc. Ahora, en su mayor parte, no son necesarios, porque el compilador capaz de mostrar automáticamente los parámetros de las plantillas de clase:
std::pair p{1, 2.0};
CTAD es una nueva oportunidad, aún
necesita evolucionar y evolucionar (C ++ 20 ya promete mejoras). Mientras tanto, las restricciones son las siguientes:
La inferencia parcial de los tipos de parámetros no es compatible. std::pair<double> p{1, 2};
Alias de plantilla no admitidos template <class T, class U> using MyPair = std::pair<T, U>; MyPair p{1, 2};
Los constructores disponibles solo en especializaciones de plantillas no son compatibles. template <class T> struct Wrapper {}; template <> struct Wrapper<int> { Wrapper(int) {}; }; Wrapper w{5};
Las plantillas anidadas no son compatibles template <class T> struct Foo { template <class U> struct Bar { Bar(T, U) {}; }; }; Foo::Bar x{ 1, 2.0 };
Obviamente, CTAD no funcionará si el tipo del parámetro de plantilla no está relacionado con los argumentos del constructor. template <class T> struct Collection { Collection(std::size_t size) {}; }; Collection c{5};
En algunos casos, ayudarán las reglas de inferencia explícitas que deben declararse en el mismo bloque que la plantilla de clase.
Ejemplo template <class T> struct Collection { template <class It> Collection(It from, It to) {}; }; Collection c{v.begin(), v.end()};
Leer más:
una vez (ruso) ,
dos (inglés)EBO
Optimización de base vacía. Optimización de una clase base vacía. También se llama Optimización de clase base vacía (EBCO).
Como sabes, en C ++, el tamaño de un objeto de cualquier clase no puede ser cero. De lo contrario, toda la aritmética de los punteros se romperá, porque en una dirección será posible marcar tantos objetos diferentes como desee. Por lo tanto, incluso los objetos de clases vacías (es decir, clases sin un solo campo no estático) tienen un tamaño distinto de cero, que depende del compilador y el sistema operativo y, por lo general, es igual a 1.
Por lo tanto, la memoria se desperdicia en vano en todos los objetos de clases vacías. Pero no los objetos de sus descendientes, porque en este caso el Estándar hace una excepción explícitamente. Se permite al compilador no asignar memoria para una clase base vacía y, por lo tanto, guardar no solo 1 byte de la clase vacía, sino los 4 (dependiendo de la plataforma), ya que también hay alineación.
Ejemplo struct Empty {}; struct Foo : Empty { int i; }; std::cout << sizeof(Empty) << std::endl;
Pero
como no se pueden colocar diferentes objetos del mismo tipo en la misma dirección, el
EBO no funcionará si:
Una clase vacía se encuentra dos veces entre los antepasados. struct Empty {}; struct Empty2 : Empty {}; struct Foo : Empty, Empty2 { int i; }; std::cout << sizeof(Empty) << std::endl;
El primer campo no estático es un objeto de la misma clase vacía o su descendiente. struct Empty {}; struct Foo : Empty { Empty e; int i; }; std::cout << sizeof(Empty) << std::endl;
En los casos en que los objetos de las clases vacías son campos no estáticos, no se proporcionan optimizaciones (por ahora, el atributo
[[no_unique_address]]
aparecerá en C ++ 20). Pero gastar 4 bytes (o cuánto necesita el compilador) para cada uno de estos campos es una pena, por lo que puede "colapsar" los objetos de las clases vacías con el primer campo no estático no vacío por su cuenta.
Ejemplo struct Empty1 {}; struct Empty2 {}; template <class Member, class ... Empty> struct EmptyOptimization : Empty ... { Member member; }; struct Foo { EmptyOptimization<int, Empty1, Empty2> data; };
Es extraño, pero en este caso, el tamaño de Foo es diferente para diferentes compiladores, para MSVC 2019 es 8, para GCC 8.3.0 es 4. Pero en cualquier caso, aumentar el número de clases vacías no afecta el tamaño de
Foo
.
Leer más: una
vez (inglés) ,
dos (inglés)IIFE
Expresión de función invocada inmediatamente. Expresión funcional llamada de inmediato. En general, este es un idioma en JavaScript, de donde Jason Turner lo tomó prestado junto con el nombre. De hecho, solo está creando e inmediatamente llamando a una lambda:
const auto myVar = [&] { if (condition1()) { return computeSomeComplexStuff(); } return condition2() ? computeSonethingElse() : DEFAULT_VALUE; } ();
¿Por qué es esto necesario? Bueno, por ejemplo, como en el código anterior para inicializar una constante por el resultado de un cálculo no trivial y no obstruir el alcance con variables y funciones innecesarias.
Leer más: una
vez (inglés) ,
dos (inglés)NVI
Interfaz no virtual. Interfaz no virtual. Según este modismo, una interfaz de clase abierta no debe contener funciones virtuales. Todas las funciones virtuales se hacen privadas (máxima protección) y se llaman dentro de abierto no virtual.
Ejemplo class Base { public: virtual ~Base() = default; void foo() {
¿Por qué es esto necesario?
- Cada función virtual abierta hace 2 cosas: define la interfaz pública de la clase y participa en la anulación del comportamiento en las clases descendientes. El uso de NVI elimina tales funciones con una doble carga: la interfaz está definida por algunas funciones, el cambio de comportamiento por otras. Puede cambiar ambos independientemente uno del otro.
- Si hay algunos requisitos generales para todas las opciones para implementar una función virtual (comprobaciones previas y posteriores, captura de mutex, etc.), entonces es muy conveniente recopilarlas en un solo lugar (ver SECO ), en la clase base, y evitar que los herederos se anulen. Este comportamiento. Es decir Resulta un caso especial del método de plantilla de patrón.
La tarifa por usar NVI es una cierta expansión del código, una posible disminución en el rendimiento (debido a una llamada de método adicional) y una mayor susceptibilidad al problema de una clase base frágil (ver FBC ).Leer más: una vez (inglés) , dos (inglés)RVO y NRVO
(Nombre) Optimización del valor de retorno. Optimizando el valor de retorno (nombrado). Este es un caso especial de elisión de copia permitida por el Estándar: el compilador puede omitir copias innecesarias de objetos temporales, incluso si sus constructores y destructores tienen efectos secundarios obvios. Dicha optimización es permisible cuando la función devuelve un objeto por valor (los otros dos casos permitidos de copia de elisión son el lanzamiento y la captura de excepciones).Ejemplo Foo bar() { return Foo(); } int main() { auto f = bar(); }
Sin RVO , aquí se crearía un objeto temporal Foo
en la función bar
, luego a través del constructor de copias se crearía otro objeto temporal en la función main
(para obtener el resultado bar
), y solo entonces se crearía el objeto f
y se le asignaría el valor del segundo objeto temporal. RVO se deshace de todas estas copias y asignaciones, y la función bar
crea directamente f
.Sucede así: una función main
asigna un espacio para un objeto en su marco de pila f
. Una función bar
(que ya funciona en su marco) obtiene acceso a esta memoria asignada en el marco anterior y crea allí el objeto deseado.NRVO es diferente deRVO realiza la misma optimización, pero no cuando se crea el objeto en la expresión return
, sino cuando se devuelve el objeto creado previamente en la función.Ejemplo Foo bar() { Foo result; return result; }
A pesar de la diferencia aparentemente pequeña, NRVO es mucho más difícil de implementar y, por lo tanto, no funciona en muchos casos. Por ejemplo, si una función devuelve un objeto global o uno de sus argumentos, o si una función tiene varios puntos de salida y se devuelven diferentes objetos a través de ellos, NRVO no se aplicará.NRVO no funciona aquí Foo bar(bool condition) { if (condition) { Foo f1; return f1; } Foo f2; return f2; }
Casi todos los compiladores han apoyado durante mucho tiempo a RVO . El grado de soporte para NRVO puede variar de un compilador a otro y de una versión a otra.RVO y NRVO son solo optimizaciones. Y aunque no se llama la copia del constructor y el operador de asignación, deberían estar en la clase del objeto. Las reglas han cambiado un poco en C ++ 17: ahora RVO no se considera copia de elisión, se ha convertido en obligatorio, y el constructor correspondiente y el operador de asignación no son necesarios.Nota: (N) RVO en términos constantes es un tema resbaladizo. Hasta C ++ 14 inclusive, no se dijo nada al respecto, C ++ 17 requiere RVO en tales expresiones, y el próximo C ++ 20 - prohíbe.Algunas palabras sobre la conexión con la semántica del desplazamiento. En primer lugar, (N) RVO es aún más efectivo, porque no es necesario llamar al constructor y destructor de movimientos. En segundo lugar, si en lugar result
de regresar de la misma función std::move(result)
, se garantiza que NRVO no funcionará. Parafraseando Estándar: RVO se aplica a prvalue, NRVO se aplica a lvalue, a std::move(result)
es xvalue.Leer más: uno (inglés) , dos (inglés) , tres (inglés)SFINAE
La falla de sustitución no es un error. La sustitución fallida no es un error. SFINAE es una característica del proceso de creación de instancias de plantillas (funciones y clases) en C ++. La conclusión es que si una determinada plantilla no puede ser instanciada, esto no se considera un error si hay otras opciones. Por ejemplo, un algoritmo simplificado para elegir la sobrecarga de funciones más adecuada funciona así:- Se resuelve el nombre de la función: el compilador busca todas las funciones con el nombre dado en todos los espacios de nombres considerados (ver ADL ).
- Se descartan las funciones inapropiadas: no se trata del número de argumentos, no es necesaria la conversión de los tipos de argumentos, no fue posible derivar tipos para la plantilla de funciones, etc.
- (viable functions), . — .
Entonces SFINAE ocurre en el segundo paso: si la sobrecarga se obtiene al crear una instancia de la plantilla de función, pero el compilador no pudo inferir los tipos de firma de la función, entonces esta sobrecarga no se considera un error, sino que se descarta silenciosamente (incluso sin advertencia). Y de manera similar para las clases.SFINAE puede usarse para muchas cosas, por ejemplo, para contar la longitud de una lista de inicialización o para contar bits en un número. Pero la mayoría de las veces, con su ayuda, la reflexión se emula como mínimo, es decir, se determina si la clase tiene un método con una firma determinada.Ejemplo #include <iostream> #include <type_traits> #include <utility> template <class, class = void> struct HasToString : std::false_type {}; // , // - , // — , , template <class T> struct HasToString<T, std::void_t<decltype(&T::toString)>> : std::is_same<std::string, decltype(std::declval<T>().toString())> {}; struct Foo { std::string toString() { return {}; } }; int main() { std::cout << HasToString<Foo>::value << std::endl; // 1 std::cout << HasToString<int>::value << std::endl; // 0 }
Lo que apareció en C ++ 17 static if
puede en algunos casos reemplazar a SFINAE , y los conceptos esperados en C ++ 20 casi lo harán innecesario. A verLeer más: uno (ruso) , dos (inglés) , tres (inglés)SBO, SOO, SSO
Búfer pequeño / Objeto / Optimización de cadena. Optimización de pequeños buffers / objetos / líneas. A veces, SSO se usa en el sentido de la optimización de tamaño pequeño, pero muy raramente, por lo que suponemos que SSO se trata de cadenas. SBO y SOO son simplemente sinónimos, y SSO es el caso especial más famoso.Todas las estructuras de datos que usan memoria dinámica ciertamente también ocupan un lugar en la pila. Al menos para almacenar un puntero a un grupo. Y la esencia de estas optimizaciones no es solicitar memoria del montón para objetos relativamente pequeños (lo cual es relativamente costoso), sino colocarlos en el espacio de pila ya asignado.Por ejemplo, std :: string podría implementarse así:Ejemplo class string { char* begin_; size_t size_; size_t capacity_; };
El tamaño de esta clase, obtengo 24 bytes (depende del compilador y la plataforma). Es decir
No se pueden colocar cadenas de más de 24 caracteres en la pila. En realidad, no hasta las 24, por supuesto, ya que es necesario distinguir de alguna manera entre la colocación en la pila y en el montón. Pero aquí está la forma más simple para líneas cortas de hasta 8 caracteres (el mismo tamaño - 24 bytes):Ejemplo class string { union Buffer { char* begin_; char local_[8]; }; Buffer buffer_; size_t _size; size_t _capacity; };
Además de la falta de asignaciones en el montón, existe otra ventaja: un alto grado de localidad de datos. Una matriz o vector de tales objetos optimizados realmente ocupará solo una pieza continua de memoria.Casi todas las implementaciones std::string
usan SSO y al menos algunas implementaciones std::function
. Pero std::vector
nunca se optimiza de esta manera, ya que el estándar requiere que std::swap
para dos vectores no cause copia o asignación de sus elementos, y que todos los iteradores válidos sigan siendo válidos. SBO no permitirá cumplir estos requisitos (porque std::string
no lo son). Pero boost::container::small_vector
, como puede suponer, usa SBO .Leer más: tiempo (inglés) ,dosUDPATE
Gracias a PyerK por esta lista adicional de abreviaturas.CV
Calificadores como const y volátiles. const
significa que el objeto / variable no se puede modificar, un intento de hacerlo resultará en un error en el momento de la compilación o en UB en el tiempo de ejecución. volatile
significa que el objeto / variable puede cambiar independientemente de las acciones del programa (por ejemplo, algunos rellenos de microcontroladores escriben algo en la memoria) y el compilador no debe optimizar el acceso al mismo. El acceso a un volatile
objeto no a través de un volatile
enlace o puntero también da como resultado UB .Leer más: uno (ruso) , dos (inglés) , tres (ruso)LTO
Optimización del tiempo de enlace. Optimización de enlaces. Como su nombre lo indica, esta optimización ocurre durante el enlace, es decir, después de la compilación. El enlazador puede hacer algo que el compilador no se atrevió a hacer: hacer que algunas funciones estén en línea, desechar el código y los datos no utilizados. Aumenta el tiempo de enlace, por supuesto.Leer más: tiempo (inglés)PCH
Encabezados precompilados. Encabezados precompilados. A menudo se usan, pero rara vez los archivos de encabezado modificados se compilan una vez y se guardan en el formato interno del compilador. Por lo tanto, volver a armar el proyecto llevará menos tiempo, a veces mucho menos.Leer más: tiempo (rus.)Pgo
Optimización guiada por perfil. Optimización basada en resultados de perfiles. Este es un método de optimización del programa, pero no a través del análisis de código estático, sino a través del lanzamiento de programas de prueba y la recopilación de estadísticas reales. Por ejemplo, la ramificación y la llamada a funciones virtuales de esta manera se pueden optimizar.Leer más: tiempo (rus.)Seh / veh
Manejo de excepciones estructuradas / vectorizadas. Esta es una extensión MSVC para manejo de excepciones y errores. A diferencia norma try-catch
SEH utiliza sus propias palabras clave: __try
, __except
, __finally
, las capturas y los mangos no se lanzan excepciones explícitamente, y tales cosas como el acceso a un desbordamiento de pila de memoria no válida debido a la recursividad infinita, llamar a una función virtual pura, etc ... VEH No detecta todos los errores explícitamente, pero crea una cadena global de controladores de errores.Leer más: tiempo (inglés)Tmp
Metaprogramación de plantillas. Metaprogramación de plantilla. La metaprogramación es cuando un programa crea otro como resultado de su trabajo. Las plantillas en C ++ implementan dicha metaprogramación. El compilador de plantillas genera el número requerido de clases o funciones. Se sabe que TMP en C ++ es Turing completo, es decir, cualquier función se puede implementar en él.Leer más: tiempo (rus.)Vla
Matrices de longitud variable. Matrices de longitud variable. Es decir
matrices cuya longitud es desconocida en el momento de la compilación: void foo(int n) { int array[n]; }
El estándar C ++ no permite esto. Lo cual es algo extraño, ya que existen en C puro desde el estándar C99. Y son compatibles con algunos compiladores de C ++ como una extensión.Leer más: tiempo (rus.)PS
Si me perdí algo o me equivoqué en alguna parte, escriba los comentarios. Solo recuerde, por favor, que aquí solo se enumeran las abreviaturas directamente relacionadas con C ++. Para otros, pero no menos útil, habrá una publicación separada.Segunda parte