
En los últimos años, C ++ ha avanzado a pasos agigantados, y mantenerse al día con todas las sutilezas y complejidades del lenguaje puede ser muy, muy difícil. Un nuevo estándar no está lejos, sin embargo, la introducción de nuevas tendencias no es el proceso más rápido y fácil, por lo tanto, si bien hay un poco de tiempo antes de C ++ 20, sugiero actualizar o descubrir algunos lugares especialmente "resbaladizos" del estándar actual idioma
Hoy les diré por qué si constexpr no es un reemplazo para las macros, cuáles son las "partes internas" del enlace estructurado y sus "escollos" y es cierto que la elisión de copia siempre funciona ahora y puede escribir cualquier devolución sin dudarlo.
Si no tienes miedo de ensuciarte un poco las manos, profundizando en el "interior" de tu lengua, bienvenido a Cat.
si constexpr
Comencemos con el más simple:
if constexpr
permite descartar la rama de expresión condicional para la que no se cumple la condición deseada, incluso en la etapa de compilación.
Parece que este es un reemplazo para la macro
#if
para desactivar la lógica "extra"? No En absoluto
En primer lugar, un
if
tiene propiedades que no están disponibles para macros: en el interior puede contar cualquier expresión
constexpr
que se pueda
constexpr
a
bool
. Bueno, y en segundo lugar, el contenido de la rama descartada debe ser sintáctica y semánticamente correcta.
Debido al segundo requisito,
if constexpr
no se puede utilizar, por ejemplo, funciones inexistentes (el código dependiente de la plataforma no se puede separar explícitamente de esta manera) o incorrecto desde el punto de vista del lenguaje de construcción (por ejemplo, "
void T = 0;
").
¿Cuál es el punto de usar
if constexpr
? El punto principal está en las plantillas. Hay una regla especial para ellos: la rama descartada no se instancia cuando se instancia la plantilla. Esto facilita la escritura de código que de alguna manera depende de las propiedades de los tipos de plantilla.
Sin embargo, en las plantillas, no se debe olvidar que el código dentro de las ramas debe ser correcto al menos para alguna variante de instanciación (incluso puramente potencial), por lo tanto, es simplemente
static_assert(false)
escribir, por ejemplo,
static_assert(false)
dentro de una de las ramas (es necesario que esto
static_assert
dependía de algún parámetro dependiente de la plantilla).
Ejemplos:
void foo() {
template<class T> void foo() {
template<class T> void foo() { if constexpr (condition1) {
Cosas para recordar
- El código en todas las ramas debe ser correcto.
- Dentro de las plantillas, el contenido de las ramas descartadas no se instancia.
- El código dentro de cualquier rama debe ser correcto para al menos una variante puramente potencial de instanciación de la plantilla.
Enlace estructurado

En C ++ 17, apareció un mecanismo bastante conveniente para descomponer varios objetos tipo tupla, lo que le permite vincular de manera conveniente y concisa sus elementos internos a variables con nombre:
Por un objeto similar a una tupla, me referiré a un objeto para el que se conoce el número de elementos internos disponibles en el momento de la compilación (de "tupla" - una lista ordenada con un número fijo de elementos (vector)).
Dichas definiciones se incluyen en esta definición como:
std::pair
,
std::tuple
,
std::array
, matrices de la forma "
T a[N]
", así como varias estructuras y clases auto escritas.
Parar ... ¿Puedes usar tus propias estructuras en la unión estructural? Spoiler: puedes (aunque a veces tienes que trabajar duro (pero más sobre eso a continuación)).
Como funciona
El trabajo de vinculación estructural merece un artículo separado, pero como estamos hablando específicamente de lugares "resbaladizos", intentaré explicar brevemente cómo funciona todo.
El estándar proporciona la siguiente sintaxis para definir el enlace:
attr (opcional)
cv-auto ref-operator (opcional) [
identifier-list ]
expresión ;
attr
- lista de atributos opcionales;
cv-auto
- auto con posibles modificadores constantes / volátiles;
ref-operator
referencia: especificador de referencia opcional (& o &&);
identifier-list
- una lista de nombres de nuevas variables;
expression
es una expresión que da como resultado un objeto tipo tupla que se usa para la unión (la expresión puede tener la forma " = expr
", " {expr}
" o " (expr)
").
Es importante tener en cuenta que el número de nombres en la
identifier-list
debe coincidir con el número de elementos en el objeto resultante de la
expression
.
Todo esto le permite escribir construcciones de la forma:
const volatile auto && [a,b,c] = Foo{};
Y aquí llegamos al primer lugar "resbaladizo": encontrar una expresión de la forma "
auto a = expr;
", Generalmente quiere decir que el tipo"
a
"se calculará con la expresión"
expr
", y espera que en la expresión"
const auto& [a,b,c] = expr;
"Se hará lo mismo, solo los tipos para"
a,b,c
"serán los tipos
const&
elementos correspondientes de"
expr
"...
La verdad es diferente: el especificador
cv-auto ref-operator
se usa para calcular el tipo de una variable invisible, a la que se asigna el resultado del cálculo de expr (es decir, el compilador reemplaza "
const auto& [a,b,c] = expr
" con "
const auto& e = expr
").
Por lo tanto, aparece una nueva entidad invisible (en adelante la llamaré {e}), sin embargo, la entidad es muy útil: por ejemplo, puede materializar objetos temporales (por lo tanto, puede conectarlos con seguridad "
const auto& [a,b,c] = Foo {};
").
El segundo lugar resbaladizo se deduce inmediatamente del reemplazo que hace el compilador: si el tipo deducido para {e} no es una referencia, entonces el resultado de
expr
se copiará en {e}.
¿Qué tipos tendrán las variables en
identifier-list
? Para empezar, estos no serán exactamente variables. Sí, se comportan como variables reales, ordinarias, pero solo con la diferencia de que en su interior se refieren a una entidad asociada con ellas, y el
decltype
de
decltype
de una variable de "referencia" de este tipo producirá el tipo de entidad a la que se refiere esta variable:
std::tuple<int, float> t(1, 2.f); auto& [a, b] = t;
Los tipos mismos se definen de la siguiente manera:
- Si {e} es una matriz (
T a[N]
), entonces el tipo será uno: T, los modificadores cv coincidirán con los de la matriz.
- Si {e} es de tipo E y admite la interfaz de tupla, las estructuras se definen:
std::tuple_size<E>
std::tuple_element<i, E>
y función:
get<i>({e}); // {e}.get<i>()
entonces el tipo de cada variable será el tipo std::tuple_element_t<i, E>
- En otros casos, el tipo de la variable corresponderá al tipo de elemento de estructura al que se realiza el enlace.
Entonces, si es muy breve, se toman los siguientes pasos con el enlace estructural:
- Cálculo del tipo e inicialización de la entidad invisible {e} en función de los modificadores de tipo
expr
y cv-ref
.
- Crea pseudo-variables y únelas a elementos {e}.
Vinculando estructuralmente sus clases / estructuras
El principal obstáculo para vincular sus estructuras es la falta de reflexión en C ++. Incluso el compilador, que, al parecer, debe saber con certeza cómo se organiza esta o aquella estructura en su interior, tiene dificultades: los modificadores de acceso (públicos / privados / protegidos) y la herencia complican mucho las cosas.
Debido a tales dificultades, las restricciones en el uso de sus clases son muy estrictas (al menos por ahora:
P1061 ,
P1096 ):
- Todos los campos internos no estáticos de una clase deben ser de la misma clase base y deben estar disponibles en el momento del uso.
- O la clase debe implementar "reflexión" (admite la interfaz de tupla).
La implementación de la interfaz de tupla le permite usar cualquiera de sus clases para el enlace, pero se ve un poco engorroso y conlleva otra trampa. Usemos de inmediato un ejemplo:
Ahora nos unimos:
Foo foo; const auto& [f1] = foo; const auto [f2] = foo; auto& [f3] = foo; auto [f4] = foo;
¿Y es hora de pensar en qué tipos tenemos? (Quien pueda responder de inmediato merece un delicioso cariño).
decltype(f1); decltype(f2); decltype(f3); decltype(f4);
¿Por qué sucedió esto? La respuesta se encuentra en la especialización predeterminada para
std::tuple_element
:
template<std::size_t i, class T> struct std::tuple_element<i, const T> { using type = std::add_const_t<std::tuple_element_t<i, T>>; };
std::add_const
no agrega
const
a los tipos de referencia, por lo que el tipo para
Foo
siempre será
int&
.
¿Cómo ganar esto? Solo agrega especialización para
const Foo
:
template<> struct std::tuple_element<0, const Foo> { using type = const int&; };
Entonces se esperarán todos los tipos:
decltype(f1);
Por cierto, el mismo comportamiento es cierto para, por ejemplo,
std::tuple<T&>
- puede obtener una referencia no constante al elemento interno, aunque el objeto en sí sea constante.
Cosas para recordar
- "
cv-auto ref
" en " cv-auto ref [a1..an] = expr
" se refiere a la variable invisible {e}.
- Si no se hace referencia al tipo inferido {e}, {e} se inicializará copiando (cuidadosamente con las clases "pesadas").
- Las variables
decltype
son enlaces "implícitos" (se comportan como enlaces, aunque decltype
devuelve un tipo de no referencia (a menos que la variable se refiera a un enlace)).
- Se debe tener cuidado al usar tipos de referencia para la unión.
Optimización del valor de retorno (rvo, copia de elisión)

Quizás esta fue una de las características más discutidas del estándar C ++ 17 (al menos en mi círculo de amigos). Y de hecho: C ++ 11 trajo la semántica del movimiento, que simplificó enormemente la transferencia de lo "interno" del objeto y la creación de varias fábricas, y C ++ 17 en general, al parecer, hizo posible no pensar en cómo devolver el objeto de algún método de fábrica , - ahora todo debería ser sin copiar y, en general, "pronto todo florecerá en Marte" ...
Pero seamos un poco realistas: optimizar el valor de retorno no es lo más fácil de implementar. Recomiendo ver esta presentación de cppcon2018: Arthur O'Dwyer "
Optimización del valor de retorno: más difícil de lo que parece ", en la que el autor explica por qué es difícil.
Spoiler corto:
Existe una "ranura para el valor de retorno". Esta ranura es esencialmente solo un lugar en la pila que es asignado por quien llama y pasa a la llamada. Si el código llamado sabe exactamente qué objeto individual se devolverá, simplemente puede crearlo inmediatamente en este espacio directamente (siempre que el tamaño y el tipo del objeto y el espacio sean los mismos).
¿Qué se sigue de esto? Vamos a desarmarlo con ejemplos.
Todo estará bien aquí: NRVO funcionará, el objeto se construirá inmediatamente en la "ranura":
Base foo1() { Base a; return a; }
Aquí ya no es posible determinar inequívocamente qué objeto debería ser el resultado, por lo que el
constructor de movimiento (c ++ 11) se
llamará implícitamente :
Base foo2(bool c) { Base a,b; if (c) { return a; } return b; }
Aquí es un poco más complicado ... Dado que el tipo del valor de retorno es diferente del tipo declarado, no puede invocar implícitamente
move
, por lo que se llama al constructor de copia de forma predeterminada. Para evitar que esto suceda, debe llamar explícitamente a
move
:
Base foo3(bool c) { Derived a,b; if (c) { return std::move(a); } return std::move(b); }
Parece que esto es lo mismo que
foo2
, pero el operador ternario es algo muy
peculiar ...
Base foo4(bool c) { Base a, b; return std::move(c ? a : b); }
Similar a
foo4
, pero también de un tipo diferente, por
move
necesita
move
exactamente:
Base foo5(bool c) { Derived a, b; return std::move(c ? a : b); }
Como puede ver en los ejemplos, uno todavía tiene que pensar en cómo devolver el significado incluso en casos aparentemente triviales ... ¿Hay alguna forma de simplificar un poco su vida? Hay: clang desde hace algún tiempo ahora apoya el
diagnóstico de la necesidad de llamar explícitamente
move
, y hay varias propuestas (
P1155 ,
P0527 ) en el nuevo estándar que harán que el
move
explícito sea menos necesario.
Cosas para recordar
- RVO / NRVO solo funcionará si:
- se sabe inequívocamente qué objeto único se debe crear en el "espacio de valor de retorno";
- Los objetos de retorno y los tipos de función son iguales.
- Si hay ambigüedad en el valor de retorno, entonces:
- si los tipos del objeto y la función devueltos coinciden, se llamará a move implícitamente;
- de lo contrario, debe llamar explícitamente a move.
- Precaución con el operador ternario: es conciso, pero puede requerir un movimiento explícito.
- Es mejor usar compiladores con diagnósticos útiles (o al menos analizadores estáticos).
Conclusión
Y sin embargo, amo C ++;)