Noexcept y copiar, mover constructores

6 minutos de lectura

Avatar de usuario de nombre genérico
Nombre generico

Dondequiera que miro, parece ser el acuerdo de que la biblioteca estándar debe llamar a los constructores de copia en lugar de a los constructores de movimiento cuando el constructor de movimiento es noexcept (falso).

Ahora no entiendo por qué este es el caso. Y además, Visual Studio VC v140 y gcc v 4.9.2 parecen hacer esto de manera diferente.

No entiendo por qué no, excepto que esto es una preocupación de, por ejemplo, vector. Me refiero a cómo debería vector::resize() ser capaz de dar una fuerte garantía de excepción si T no lo hace. Tal como lo veo, el nivel de excepción del vector dependerá de T. Independientemente de si se usa copiar o mover. Entiendo que no, excepto que solo es un guiño al compilador para optimizar el manejo de excepciones.

Este pequeño programa llama al constructor de copia cuando se compila con gcc y al constructor de movimiento cuando se compila con Visual Studio.

include <iostream>
#include <vector>

struct foo {
  foo() {}
  //    foo( const foo & ) noexcept { std::cout << "copy\n"; }
  //    foo( foo && ) noexcept { std::cout << "move\n"; }
  foo( const foo & )  { std::cout << "copy\n"; }
  foo( foo && )  { std::cout << "move\n"; }

  ~foo() noexcept {}
};

int main() {
    std::vector< foo > v;
    for ( int i = 0; i < 3; ++i ) v.emplace_back();
}

  • al copiar los elementos del vector durante la reasignación, deja intacto el almacenamiento anterior; al moverlos se modifica. si se lanza una excepción, esto no se puede deshacer, ya que se puede lanzar otra excepción

    –Piotr Skotnicki

    20 de febrero de 2015 a las 11:17

Esta es una pregunta multifacética, así que tenga paciencia conmigo mientras analizo los diversos aspectos.

La biblioteca estándar espera que todos los tipos de usuarios proporcionen siempre la garantía de excepción básica. Esta garantía dice que cuando se lanza una excepción, los objetos involucrados todavía están en un estado válido, aunque desconocido, que no se filtran recursos, que no se violan invariantes fundamentales del lenguaje y que no ocurre ninguna acción espeluznante a distancia (esa última uno no es parte de la definición formal, pero es una suposición implícita que realmente se hizo).

Considere un constructor de copias para una clase Foo:

Foo(const Foo& o);

Si este constructor arroja, la garantía de excepción básica le brinda el siguiente conocimiento:

  • No se creó ningún objeto nuevo. Si un constructor lanza, el objeto no se crea.
  • o no fue modificado. Su única participación aquí es a través de una referencia const, por lo que no debe modificarse. Otros casos caen bajo el encabezado de “acción espeluznante a distancia”, o posiblemente “lenguaje fundamental invariable”.
  • No se filtraron recursos, el programa en su conjunto sigue siendo coherente.

En un constructor de movimiento:

Foo(Foo&& o);

la garantía básica da menos seguridad. o se puede modificar, porque está involucrado a través de una referencia no constante, por lo que puede estar en cualquier estado.

A continuación, mira vector::resize. Su implementación seguirá generalmente el mismo esquema:

void vector<T, A>::resize(std::size_t newSize) {
  if (newSize == size()) return;
  if (newSize < size()) makeSmaller(newSize);
  else if (newSize <= capacity()) makeBiggerSimple(newSize);
  else makeBiggerComplicated(newSize);
}
void vector<T, A>::makeBiggerComplicated(std::size_t newSize) {
  auto newMemory = allocateNewMemory(newSize);
  constructAdditionalElements(newMemory, size(), newSize);
  transferExistingElements(newMemory);
  replaceInternalBuffer(newMemory, newSize);
}

La función clave aquí es transferExistingElements. Si sólo usamos la copia, tiene una garantía simple: es no puedo modificar el búfer de origen. Entonces, si en algún momento se lanza una operación, podemos simplemente destruir los objetos recién creados (tenga en cuenta que la biblioteca estándar no puede funcionar en absoluto con destructores de lanzamiento), tirar el nuevo búfer y volver a tirar. El vector se verá como si nunca hubiera sido modificado. Esto significa que tenemos la garantía fuerte, aunque el constructor de copia del elemento solo ofrece la garantía débil.

Pero si usamos mover en su lugar, esto no funciona. Una vez que se mueve un objeto, cualquier excepción posterior significa que el búfer de origen ha cambiado. Y debido a que no tenemos garantía de que mover los objetos hacia atrás no se arroje también, ni siquiera podemos recuperarnos. Por lo tanto, para mantener la garantía sólida, debemos exigir que la operación de movimiento no genere ninguna excepción. Si tenemos eso, estamos bien. Y por eso tenemos move_if_noexcept.

En cuanto a la diferencia entre MSVC y GCC: MSVC solo admite noexcept desde la versión 14, y dado que aún está en desarrollo, sospecho que la biblioteca estándar aún no se ha actualizado para aprovecharla.

  • Muchas gracias por esta respuesta. Creo que lo explica muy bien.

    – Nombre generico

    20 de febrero de 2015 a las 16:07

El problema central es que es imposible ofrecer una fuerte excepción de seguridad con un constructor de movimientos de lanzamiento. Imagínese si, en el cambio de tamaño del vector, a la mitad de mover los elementos al nuevo búfer, se lanza un constructor de movimiento. ¿Cómo podría restaurar el estado anterior? No puedes volver a usar el constructor de movimiento porque, bueno, eso podría seguir arrojando.

La copia funciona para una fuerte garantía de seguridad de excepción, independientemente de su naturaleza de lanzamiento porque el estado original no está dañado, por lo que si no puede construir el estado completamente nuevo, puede limpiar el estado parcialmente construido y luego ya está, porque el viejo estado todavía está aquí esperándote. Los constructores de mudanzas no ofrecen esta red de seguridad.

es fundamentalmente imposible para ofrecer un cambio de tamaño seguro de excepción fuerte () con un movimiento de lanzamiento, pero fácil con una copia de lanzamiento. Este hecho fundamental se refleja en todas partes en la biblioteca estándar.

GCC y VS tratan esto de manera diferente porque se encuentran en diferentes etapas de conformidad. VS se ha ido noexcept ser una de las últimas funciones que implementan, por lo que su comportamiento es una especie de punto medio entre el comportamiento de C++03 y el de C++11/14. En particular, dado que no tienen la capacidad de saber si su constructor de movimiento es realmente noexcept o no, básicamente solo tienen que adivinar. De memoria simplemente asumen que es noexcept porque lanzar constructores de movimientos no es común y no poder moverse sería un problema crítico.

  • Y muchas gracias por esta respuesta. También lo explica muy bien.

    – Nombre generico

    20 de febrero de 2015 a las 16:10

¿Ha sido útil esta solución?