¿Por qué std::vector copia-construye en lugar de mueve-construye cuando el destructor puede lanzar?

6 minutos de lectura

avatar de usuario de einpoklum
einpoklum

Considere el siguiente programa:

#include <vector>
#include <iostream>

class A {
    int x;
public:
    A(int n)          noexcept : x(n)       { std::cout << "ctor with value\n"; }
    A(const A& other) noexcept : x(other.x) { std::cout << "copy ctor\n"; }
    A(A&& other)      noexcept : x(other.x) { std::cout << "move ctor\n"; }
    ~A()                                    { std::cout << "dtor\n"; } // (*)
};

int main()
{
    std::vector<A> v;
    v.emplace_back(123);
    v.emplace_back(456);
}

Si ejecuto el programa, obtener (GodBolt):

ctor with value
ctor with value
move ctor
dtor
dtor
dtor

… que está en línea con lo que esperaría. Sin embargo, si en línea (*) Marco el destructor como potencialmente arrojadizo, entonces obtener :

ctor with value
ctor with value
copy ctor
dtor
dtor
dtor

… es decir, se utiliza el ctor de copia en lugar del ctor de movimiento. ¿Por qué es este el caso? No parece que la copia prevenga las destrucciones que necesitaría la mudanza.

Preguntas relacionadas:

  • ¿Se requiere std::vector para usar mover en lugar de copiar?
  • ¿Cómo hacer cumplir la semántica de movimiento cuando un vector crece?
  • La reasignación de vectores usa la copia en lugar del constructor de movimiento

  • Relacionado/Duplicado: la reasignación de vectores utiliza el constructor de copia en lugar del de movimiento.

    – Jason Liam

    10 oct a las 9:37


  • @JasonLiam mientras está relacionado, definitivamente no es un tonto. El quid de esa respuesta es que se elige el constructor de copia porque el destructor no está marcado noexcept. Esta pregunta pregunta por qué se elige el constructor de copias si el destructor puede generar excepciones.

    – Revólver_Ocelote

    10 oct a las 9:44

  • El duplicado vinculado se trata de un error en una versión anterior de GCC relacionado con el caso en el que el destructor no tiene noexcept El especificador se comporta así. Aquí la pregunta es sobre el caso. con a noexcept especificador Así que reabriré.

    – usuario17732522

    10 oct a las 9:45


  • Dos publicaciones recientes del blog de O’Dwyer son relevantes y buenas lecturas: ¿Qué es la “pesimización vectorial”? y seguimiento Un triángulo de “elige dos” para std::vector.

    – davidbak

    10 oct a las 20:19


Avatar de usuario de Caleth
Caleth

Esto es LWG2116. La elección entre mover y copiar los elementos a menudo se expresa como std::is_nothrow_move_constructiblees decir noexcept(T(T&&))que también comprueba erróneamente el destructor.

  • ¿No debería ser así? noexcept(T(T&&)) || !noexcept(T(T)) ¿después?

    – einpoklum

    10 oct a las 9:33

  • @einpoklum no, el problema es que no debería verificar el destructor en absoluto, porque si eso falla, no puede volver a lo que comenzó.

    – Caleth

    10 oct a las 9:40

  • Supongo que eso tiene sentido. Usar un tipo con un destructor potencialmente arrojadizo en un contenedor de biblioteca estándar ya es algo inusual. Tampoco permiten lanzar desde el destructor.

    – usuario17732522

    10 oct a las 10:03

  • @benrg A la implementación no tiene que importarle si el destructor se lanza porque es una condición previa para el usuario de la biblioteca que esto no suceda. Sin embargo, la implementación debe asegurar que si std::is_nothrow_move_constructible es falso, pero el tipo sigue siendo CopyInsertable, que cualquier excepción lanzada desde los constructores no causará que se violen las garantías de excepción, lo que significa que el vector debe dejarse en el estado original. Por lo general, es imposible asegurar si se usó un movimiento antes/mientras se lanza la excepción. Entonces, si el movimiento puede arrojar, entonces se debe usar una copia.

    – usuario17732522

    10 oct a las 17:52


  • @MartinYork, el constructor de movimientos no es excepto, y el destructor no lo es. La elección de copiar o mover no importa para un destructor arrojadizo, porque en ese momento habrá terminado la vida útil de algunos elementos.

    – Caleth

    11 oct a las 20:14

tl;dr: Porque std::vector prefiere ofrecerle una “fuerte garantía de excepción”.

(Gracias a Jonathan Wakely, @davidbak, @Caleth por los enlaces y explicaciones)

Suponer std::vector fuera a utilizar la construcción de movimiento en su caso; y supongamos que se lanza una excepción durante el cambio de tamaño de vector, por uno de los A::~A llamadas En ese caso, usted tendría un inutilizable std::vectorparcialmente movido.

Por otro lado, si std::vector realiza la construcción de la copia y se produce una excepción en uno de los destructores: simplemente puede deshacerse de la nueva copia y su vector será en el mismo estado que estaba antes el cambio de tamaño. Esa es la “fuerte garantía de excepción” para el std::vector objeto.

Los diseñadores de bibliotecas estándar optaron por preferir esta garantía a la optimización del rendimiento del cambio de tamaño de vectores.

Esto había sido informado como un problema/defecto con la biblioteca estándar (LWG 2116) – pero después de algunas discusiones, se decidió mantener el comportamiento actual según la consideración anterior.

Ver también la publicación de Arthur O’Dwyr: Un triángulo “Elige dos” para std::vector.

  • ¿La fuerte garantía de excepción es realmente relevante aquí? Todas las destrucciones (pueden) ocurrir después los movimientos, por lo que los movimientos presumiblemente se han completado y el nuevo contenido de la vector se puede bloquear (simplemente obtiene la excepción al limpiar los datos antiguos). Incluso si realiza copias, se pueden generar las mismas excepciones cuando va a limpiar los datos antiguos de los que copió. No existe una distinción moral entre “copió todo el material y se produjo una excepción limpiando el material antiguo sin modificar” y “movió todo el material y se produjo una excepción limpiando el material vaciado”.

    – ShadowRanger

    11 oct a las 0:52


  • @MartinYork: La pregunta en cuestión es sobre destructores tirar, no mover ni copiar constructores. Todas las llamadas al destructor se pueden agrupar después de la finalización del movimiento/copia, por lo que la fuerte garantía de excepción parece irrelevante allí: la “transacción” se ha completado en el momento en que se produce la limpieza.

    – Matthieu M.

    11 oct a las 7:33

  • @ShadowRanger: Ese es un punto interesante. Supongo que, para los diseñadores de bibliotecas, las destrucciones son parte de la operación. Pero veo lo que quieres decir.

    – einpoklum

    11 oct a las 13:25

  • Tener una llamada a A::~A en realidad tirar no está permitido. Los contenedores de biblioteca estándar no admiten tipos que hagan eso. También puede ver las implementaciones. No vi las implementaciones de libstdc++, libc++ o las llamadas al destructor de guardia de MS con controladores de excepción. Si un destructor lanza una excepción, es probable que la excepción se propague al usuario y deje el vector en un estado inconsistente. Así que esto realmente no puede ser relevante.

    – usuario17732522

    14 oct a las 23:47


  • La garantía de excepción fuerte solo es relevante cuando se lanza un constructor de movimiento (que está permitido para los tipos utilizados en contenedores estándar). Es solo que la descripción de la garantía en el estándar también depende de la especificación de excepción del destructor, lo que realmente no tiene sentido ya que se supone que el destructor no lanzará de todos modos.

    – usuario17732522

    14 oct a las 23:49


¿Ha sido útil esta solución?