La noexcept
palabra clave se puede aplicar adecuadamente a muchas firmas de funciones, pero no estoy seguro de cuándo debería considerar usarla en la práctica. Según lo que he leído hasta ahora, la adición de última hora de noexcept
parece abordar algunos problemas importantes que surgen cuando los constructores de movimientos lanzan. Sin embargo, todavía no puedo proporcionar respuestas satisfactorias a algunas preguntas prácticas que me llevaron a leer más sobre el tema noexcept
en primer lugar.
Hay muchos ejemplos de funciones que sé que nunca lanzarán, pero que el compilador no puede determinar por sí solo. ¿Debo adjuntar
noexcept
a la declaración de función en todos estos casos?Tener que pensar si necesito agregar o no
noexcept
después de cada declaración de función reduciría en gran medida la productividad del programador (y, francamente, sería un fastidio). ¿Para qué situaciones debo tener más cuidado con el usonoexcept
y para qué situaciones puedo evitar lo implícitonoexcept(false)
?¿Cuándo puedo esperar de manera realista observar una mejora en el rendimiento después de usar
noexcept
? En particular, dé un ejemplo de código para el que un compilador de C ++ pueda generar un mejor código de máquina después de la adición denoexcept
.Personalmente, me preocupo
noexcept
por la mayor libertad brindada al compilador para aplicar de forma segura ciertos tipos de optimizaciones. ¿Se aprovechan los compiladores modernos denoexcept
esta manera? Si no, ¿puedo esperar que algunos de ellos lo hagan en el futuro cercano?
move_if_nothrow
(o whatchamacallit) verá una mejora en el rendimiento si hay un movimiento de excepción.move_if_noexcept
.Respuestas:
Creo que es demasiado pronto para dar una respuesta a las "mejores prácticas", ya que no ha habido tiempo suficiente para usarlo en la práctica. Si se le preguntara sobre los especificadores de lanzamiento justo después de que salieran, las respuestas serían muy diferentes a las actuales.
Bueno, entonces úsalo cuando sea obvio que la función nunca se lanzará.
Parece que las mayores ganancias de optimización provienen de las optimizaciones de los usuarios, no de las compiladoras debido a la posibilidad de verificar
noexcept
y sobrecargarlas. La mayoría de los compiladores siguen un método de manejo de excepciones sin penalización si no lanza, por lo que dudo que cambie mucho (o algo) en el nivel de código de máquina de su código, aunque tal vez reduzca el tamaño binario al eliminar el código de manejoEl uso
noexcept
en los cuatro grandes (constructores, asignación, no destructores como ya estánnoexcept
) probablemente causará las mejores mejoras ya que lasnoexcept
comprobaciones son 'comunes' en el código de plantilla, como en losstd
contenedores. Por ejemplo,std::vector
no usará el movimiento de su clase a menos que esté marcadonoexcept
(o el compilador puede deducirlo de otra manera).fuente
std::terminate
truco todavía obedece al modelo de costo cero. Es decir, da la casualidad de que el rango de instrucciones dentro de lasnoexcept
funciones se asigna a llamarstd::terminate
sithrow
se usa en lugar del desbobinador de pila. Por lo tanto, dudo que tenga más gastos generales que el seguimiento regular de excepciones.noexcept
esto lo garantiza.noexcept
lanza una función, entoncesstd::terminate
parece que implicaría una pequeña cantidad de sobrecarga" ... No, esto debería implementarse al no generar tablas de excepción para dicha función, que el despachador de excepciones debe detectar y luego rescatar.noexcept
es parte de la interfaz de la función ; no debe agregarlo solo porque su implementación actual no se produce. No estoy seguro de la respuesta correcta a esta pregunta, pero estoy bastante seguro de que la forma en que se comporta hoy su función no tiene nada que ver con eso ...Como sigo repitiendo estos días: la semántica primero .
Sumar
noexcept
,noexcept(true)
ynoexcept(false)
es ante todo sobre semántica. Solo incidentalmente condiciona varias optimizaciones posibles.Como programador de lectura de códigos, la presencia de
noexcept
es similar a la deconst
: me ayuda a entender mejor lo que puede o no suceder. Por lo tanto, vale la pena pasar un tiempo pensando si sabe o no si la función se lanzará. Como recordatorio, cualquier tipo de asignación de memoria dinámica puede arrojar.Bien, ahora a las posibles optimizaciones.
Las optimizaciones más obvias se realizan realmente en las bibliotecas. C ++ 11 proporciona una serie de rasgos que permiten saber si una función es
noexcept
o no, y la implementación de la Biblioteca estándar utilizará esos rasgos para favorecer lasnoexcept
operaciones en los objetos definidos por el usuario que manipulan, si es posible. Tal como mover la semántica .El compilador solo puede eliminar un poco de grasa (tal vez) de los datos de manejo de excepciones, porque tiene que tener en cuenta el hecho de que puede haber mentido. Si una función marcada
noexcept
lanza, entoncesstd::terminate
se llama.Estas semánticas fueron elegidas por dos razones:
noexcept
incluso cuando las dependencias ya no lo usan (compatibilidad con versiones anteriores)noexcept
cuando se llaman funciones que teóricamente pueden arrojar, pero que no se espera para los argumentos dadosfuente
noexcept
funciones no necesitaría hacer nada especial, porque cualquier excepción que pueda surgir se disparaterminate
antes de llegar a este nivel. Esto difiere mucho de tener que lidiar y propagar unabad_alloc
excepción.noexcept
es claramente útil / una buena idea. Estoy empezando a pensar que la construcción de movimientos, la asignación de movimientos y el intercambio son los únicos casos que existen ... ¿Conoces otros?noexcept
, alguien puede usarlo con confianza en un dato al que se pueda acceder después. Podría ver que esta idea se usa en otros lugares, pero la biblioteca estándar es bastante delgada en C ++ y creo que solo se usa para optimizar copias de elementos.Esto realmente hace una diferencia (potencialmente) enorme para el optimizador en el compilador. Los compiladores han tenido esta característica durante años a través de la declaración throw () vacía después de una definición de función, así como extensiones de propiedad. Les puedo asegurar que los compiladores modernos aprovechan este conocimiento para generar un mejor código.
Casi todas las optimizaciones en el compilador usan algo llamado "diagrama de flujo" de una función para razonar sobre lo que es legal. Un gráfico de flujo consta de lo que generalmente se llaman "bloques" de la función (áreas de código que tienen una sola entrada y una única salida) y bordes entre los bloques para indicar hacia dónde puede saltar el flujo. Noexcept altera el diagrama de flujo.
Solicitaste un ejemplo específico. Considera este código:
El gráfico de flujo para esta función es diferente si
bar
está etiquetadonoexcept
(no hay forma de que la ejecución salte entre el final debar
y la instrucción catch). Cuando se etiqueta comonoexcept
, el compilador está seguro de que el valor de x es 5 durante la función baz: se dice que el bloque x = 5 "domina" el bloque baz (x) sin el borde debar()
la instrucción catch.Luego puede hacer algo llamado "propagación constante" para generar un código más eficiente. Aquí, si baz está en línea, las declaraciones que usan x también pueden contener constantes y luego lo que solía ser una evaluación en tiempo de ejecución se puede convertir en una evaluación en tiempo de compilación, etc.
De todos modos, la respuesta corta:
noexcept
permite que el compilador genere un gráfico de flujo más ajustado, y el gráfico de flujo se usa para razonar sobre todo tipo de optimizaciones comunes del compilador. Para un compilador, las anotaciones de usuario de esta naturaleza son impresionantes. El compilador intentará resolver esto, pero por lo general no puede (la función en cuestión podría estar en otro archivo de objetos no visible para el compilador o usar transitivamente alguna función que no es visible), o cuando lo hace, hay alguna excepción trivial que podría producirse de la que ni siquiera es consciente, por lo que no puede etiquetarla implícitamente comonoexcept
(asignar memoria podría arrojar bad_alloc, por ejemplo).fuente
x = 5
puede lanzar. Si esa parte deltry
bloque sirviera para algo, el razonamiento no sería válido.throw
declaraciones explícitas u otras cosas comonew
esas. Si el compilador no puede ver el cuerpo, entonces debe confiar en la presencia o ausencia denoexcept
. El acceso a una matriz simple no genera excepciones (C ++ no tiene verificación de límites), por lo que no, el acceso a la matriz por sí solo no haría que el compilador piense que una función arroja excepciones. (El acceso fuera del límite es UB, no es una excepción garantizada.)throw()
en C ++ pre-noexceptnoexcept
puede mejorar dramáticamente el rendimiento de algunas operaciones. Esto no sucede en el nivel de generación de código de máquina por parte del compilador, sino seleccionando el algoritmo más efectivo: como otros mencionaron, usted hace esta selección usando la funciónstd::move_if_noexcept
. Por ejemplo, el crecimiento destd::vector
(por ejemplo, cuando llamamosreserve
) debe proporcionar una fuerte garantía de seguridad de excepción. Si sabe queT
el constructor de movimientos no arroja, simplemente puede mover cada elemento. De lo contrario, debe copiar todos losT
s. Esto se ha descrito en detalle en esta publicación .fuente
noexcept
(si corresponde). Las funciones de miembro de movimiento definidas implícitamente se hannoexcept
agregado automáticamente (si corresponde).Um, nunca? Nunca es un tiempo? Nunca.
noexcept
es para las optimizaciones de rendimiento del compilador de la misma manera queconst
para las optimizaciones de rendimiento del compilador. Es decir, casi nunca.noexcept
se utiliza principalmente para permitir que "usted" detecte en tiempo de compilación si una función puede generar una excepción. Recuerde: la mayoría de los compiladores no emiten código especial para excepciones a menos que realmente arroje algo. Así quenoexcept
no es una cuestión de dar los consejos del compilador acerca de cómo optimizar una función tanto como dar usted consejos sobre cómo utilizar una función.Las plantillas como
move_if_noexcept
detectarán si el constructor de movimiento está definidonoexcept
y devolverá un enconst&
lugar de un&&
tipo si no lo está. Es una forma de decir que se mueva si es muy seguro hacerlo.En general, debe usarlo
noexcept
cuando crea que realmente será útil hacerlo. Algún código tomará diferentes caminos siis_nothrow_constructible
es cierto para ese tipo. Si está utilizando un código que lo hará, no dude en utilizarnoexcept
los constructores adecuados.En resumen: úselo para mover constructores y construcciones similares, pero no sienta que tiene que volverse loco con él.
fuente
move_if_noexcept
no devolverá una copia, devolverá una referencia de valor constante en lugar de una referencia de valor. En general, eso hará que la persona que llama haga una copia en lugar de un movimiento, peromove_if_noexcept
no está haciendo la copia. De lo contrario, gran explicación.noexcept
. De modo que "nunca" no es cierto.std::vector
está escrito para forzar al compilador para compilar diferente código. No se trata de que el compilador detecte algo; se trata de que el código de usuario detecte algo.En palabras de Bjarne ( The C ++ Programming Language, 4th Edition , página 366):
fuente
noexcept
cada vez, excepto que explícitamente quiero ocuparme de las excepciones. Seamos realistas, la mayoría de las excepciones son tan improbables y / o tan fatales que el rescate es apenas razonable o posible. Por ejemplo, en el ejemplo citado, si la asignación falla, la aplicación difícilmente podrá continuar funcionando correctamente.noexcept
es complicado, ya que es parte de la interfaz de funciones. Especialmente, si está escribiendo una biblioteca, su código de cliente puede depender de lanoexcept
propiedad. Puede ser difícil cambiarlo más tarde, ya que podría romper el código existente. Eso podría ser menos preocupante cuando implementa código que solo usa su aplicación.Si tiene una función que no puede lanzar, pregúntese si le gustaría quedarse
noexcept
o eso restringiría futuras implementaciones? Por ejemplo, es posible que desee introducir la comprobación de errores de argumentos ilegales lanzando excepciones (por ejemplo, para pruebas unitarias), o puede depender de otro código de biblioteca que podría cambiar su especificación de excepción. En ese caso, es más seguro ser conservador y omitirnoexcept
.Por otro lado, si está seguro de que la función nunca debería lanzarse y es correcto que sea parte de la especificación, debe declararla
noexcept
. Sin embargo, tenga en cuenta que el compilador no podrá detectar violacionesnoexcept
si su implementación cambia.Hay cuatro clases de funciones en las que debería concentrarse porque probablemente tendrán el mayor impacto:
noexcept(true)
menos que los haganoexcept(false)
)Estas funciones generalmente deberían serlo
noexcept
, y lo más probable es que las implementaciones de la biblioteca puedan hacer uso de lanoexcept
propiedad. Por ejemplo,std::vector
puede usar operaciones de movimiento sin lanzamiento sin sacrificar fuertes garantías de excepción. De lo contrario, tendrá que recurrir a la copia de elementos (como lo hizo en C ++ 98).Este tipo de optimización está en el nivel algorítmico y no se basa en optimizaciones del compilador. Puede tener un impacto significativo, especialmente si los elementos son caros de copiar.
La ventaja de
noexcept
no tener en cuenta ninguna especificación de excepción othrow()
es que el estándar permite a los compiladores más libertad a la hora de desenrollar la pila. Incluso en elthrow()
caso, el compilador tiene que desenrollar completamente la pila (y tiene que hacerlo en el orden inverso exacto de las construcciones de objetos).En el
noexcept
caso, por otro lado, no es necesario hacer eso. No hay ningún requisito de que la pila tenga que desenrollarse (pero el compilador aún puede hacerlo). Esa libertad permite una mayor optimización del código, ya que reduce la sobrecarga de poder siempre desenrollar la pila.La pregunta relacionada sobre noexcept, desbobinado y rendimiento de la pila entra en más detalles sobre la sobrecarga cuando se requiere el desbobinado de la pila.
También recomiendo el libro de Scott Meyers "Effective Modern C ++", "Item 14: Declarar funciones sin excepción si no emiten excepciones" para leer más.
fuente
throws
palabra clave en lugar denoexcept
negativo. Yo simplemente no puede conseguir un poco de C ++ opciones de diseño ...noexcept
porquethrow
ya estaba ocupado. En pocas palabras,throw
se puede usar casi de la manera que mencionas, excepto que estropearon el diseño, por lo que se volvió casi inútil, incluso perjudicial. Pero estamos atascados con eso ahora, ya que eliminarlo sería un cambio radical con pocos beneficios. Asínoexcept
es básicamentethrow_v2
.throw
no es útil?throw()
especificador de excepción no proporcionó la misma garantía quenothrow
?Cuando dice "Sé que [nunca] lanzarán", quiere decir que al examinar la implementación de la función, sabe que la función no lanzará. Creo que ese enfoque es de adentro hacia afuera.
Es mejor considerar si una función puede generar excepciones para formar parte del diseño de la función: tan importante como la lista de argumentos y si un método es un mutador (...
const
). Declarar que "esta función nunca arroja excepciones" es una restricción para la implementación. Omitirlo no significa que la función pueda generar excepciones; significa que la versión actual de la función y todas las versiones futuras pueden arrojar excepciones. Es una restricción que dificulta la implementación. Pero algunos métodos deben tener la restricción para ser prácticamente útiles; lo más importante, para que puedan llamarse desde los destructores, pero también para la implementación del código de "reversión" en los métodos que proporcionan la garantía de excepción fuerte.fuente