¿Por qué los operadores de asignación de movimiento de contenedores no son noexcept?

8 minutos de lectura

Me di cuenta que std::string‘s (realmente std::basic_string‘s) mover el operador de asignación es noexcept. Eso tiene sentido para mí. Pero luego me di cuenta de que ninguno de los contenedores estándar (por ejemplo, std::vector, std::deque, std::list, std::map) declara su operador de asignación de movimiento noexcept. Eso tiene menos sentido para mí. A std::vector, por ejemplo, normalmente se implementa como tres punteros, y los punteros ciertamente se pueden mover sin generar una excepción. Entonces pensé que tal vez el problema es mover el asignador del contenedor, pero std::stringtambién tienen asignadores, por lo que si ese fuera el problema, esperaría que afectara std::string.

Entonces, ¿por qué es std::stringOperador de asignación de movimiento de noexceptpero los operadores de asignación de movimiento para los contenedores estándar no lo son?

  • ¿Dónde exactamente estás viendo esto?

    – Martín York

    08/09/2012 a las 17:25

  • @LokiAstari en el estándar C ++, creo.

    – Siempre

    08/09/2012 a las 17:31

  • @Mr.Anubis: Sí. El OP pregunta por qué no es noexcept.

    – Nicolás Bolas

    8 sep 2012 a las 17:39

  • @NicolBolas me idiota :), tomé la pregunta al revés

    – Sr. Anubis

    08/09/2012 a las 17:40


  • open-std.org/JTC1/SC22/WG21/docs/lwg-active.html#2063 sobre operador = para cadena&&.

    – Siempre

    8 sep 2012 a las 17:56

avatar de usuario
Howard Hinant

Creo que estamos viendo un defecto de estándares. los noexcept la especificación, si se va a aplicar al operador de asignación de movimiento, es algo complicada. Y creo que esta afirmación es cierta ya sea que estemos hablando de basic_string o vector.

Residencia en [container.requirements.general]/p7 mi traducción al inglés de lo que se supone que debe hacer un operador de asignación de movimiento de contenedor es:

C& operator=(C&& c)

Si alloc_traits::propagate_on_container_move_assignment::value es
truevuelca recursos, mueve, asigna asignadores y transfiere recursos de c.

Si
alloc_traits::propagate_on_container_move_assignment::value es false
y get_allocator() == c.get_allocator()vuelca recursos y transfiere recursos de c.

Si
alloc_traits::propagate_on_container_move_assignment::value es false
y get_allocator() != c.get_allocator()mover asigna cada c[i].

Notas:

  1. alloc_traits se refiere a allocator_traits<allocator_type>.

  2. Cuando alloc_traits::propagate_on_container_move_assignment::value es true se puede especificar el operador de asignación de movimiento noexcept porque todo lo que va a hacer es desasignar los recursos actuales y luego robar recursos de la fuente. También en este caso, el asignador también debe tener una asignación de movimiento, y esa asignación de movimiento debe ser noexcept para que la asignación de movimiento del contenedor sea noexcept.

  3. Cuando alloc_traits::propagate_on_container_move_assignment::value es false, y si los dos asignadores son iguales, entonces hará lo mismo que #2. Sin embargo, uno no sabe si los asignadores son iguales hasta el tiempo de ejecución, por lo que no puede basar noexcept sobre esta posibilidad.

  4. Cuando alloc_traits::propagate_on_container_move_assignment::value es falsey si los dos asignadores son no igual, entonces uno tiene que mover asignar cada elemento individual. Esto puede implicar agregar capacidad o nodos al destino y, por lo tanto, es intrínsecamente noexcept(false).

Así que en resumen:

C& operator=(C&& c)
        noexcept(
             alloc_traits::propagate_on_container_move_assignment::value &&
             is_nothrow_move_assignable<allocator_type>::value);

Y no veo ninguna dependencia de C::value_type en la especificación anterior, por lo que creo que debería aplicarse igualmente bien a std::basic_string a pesar de que C++ 11 especifique lo contrario.

Actualizar

En los comentarios a continuación, Columbo señala correctamente que las cosas han ido cambiando gradualmente todo el tiempo. Mis comentarios anteriores son relativos a C++ 11.

Para el borrador de C ++ 17 (que parece estable en este momento), las cosas han cambiado un poco:

  1. Si alloc_traits::propagate_on_container_move_assignment::value es truela especificación ahora requiere la asignación de movimiento del allocator_type para no lanzar excepciones (17.6.3.5 [allocator.requirements]/p4). Entonces uno ya no necesita verificar is_nothrow_move_assignable<allocator_type>::value.

  2. alloc_traits::is_always_equal ha sido añadido. Si esto es cierto, entonces uno puede determinar en el momento de la compilación que el punto 3 anterior no puede arrojar porque los recursos se pueden transferir.

Así que el nuevo noexcept Las especificaciones para contenedores podrían ser:

C& operator=(C&& c)
        noexcept(
             alloc_traits::propagate_on_container_move_assignment{} ||
             alloc_traits::is_always_equal{});

Y para std::allocator<T>, alloc_traits::propagate_on_container_move_assignment{} y alloc_traits::is_always_equal{} ambos son verdaderos.

También ahora en el borrador de C++17, ambos vector y string mover asignación llevar exactamente este noexcept especificación. Sin embargo, los otros contenedores llevan variaciones de este noexcept especificación.

Lo más seguro que puede hacer si le preocupa este problema es probar especializaciones explícitas de contenedores que le interesen. He hecho exactamente eso por container<T> para VS, libstdc++ y libc++ aquí:

http://howardhinnant.github.io/container_summary.html

Esta encuesta tiene aproximadamente un año, pero hasta donde yo sé, sigue siendo válida.

  • ¿Por qué debería aplicarse a basic_string?

    – Nicolás Bolas

    10 de septiembre de 2012 a las 0:10

  • @NicolBolas: [container.requirements.general]/p13: Todos los contenedores definidos en esta Cláusula y en (21.4), excepto array, cumplen los requisitos adicionales de un contenedor consciente del asignador, como se describe en la Tabla 99.

    – Howard Hinant

    10 de septiembre de 2012 a las 1:50

  • La condición se puede mejorar empleando if_always_equal (C++17). Por cierto, probablemente quieras usar la especialización de rasgo del asignador y no el tipo de asignador sin formato.

    – Colombo

    27 de noviembre de 2016 a las 22:49


avatar de usuario
Nicolás Bolas

Creo que el razonamiento para esto es así.

basic_string solo funciona con tipos de POD que no son de matriz. Como tales, sus destructores deben ser triviales. Esto significa que si haces un swap para la asignación de movimiento, no le importa tanto que el contenido original de la cadena a la que se movió aún no se haya destruido.

Mientras que los contenedores (basic_string no es técnicamente un contenedor según la especificación de C++) puede contener tipos arbitrarios. Tipos con destructores, o tipos que contienen objetos con destructores. Esto significa que es más importante para el usuario mantener el control sobre exactamente cuando un objeto es destruido. Específicamente establece que:

Todos los elementos existentes de a [the moved-to object] son asignados a movimiento o destruidos.

Así que la diferencia tiene sentido. No puedes hacer una asignación de movimiento noexcept una vez que comience a desasignar memoria (a través del asignador) porque eso puede fallar por excepción. Por lo tanto, una vez que comience a requerir que la memoria se desasigne al mover-asignar, dejará de poder hacer cumplir noexcept.

  • basic_string no es un contenedor por estándar. Y operator = para basic_string es realmente noexcept. Effects: If *this and str are not the same object, modifies *this as shown in Table 71. [ Note: A valid implementation is swap(str). —end note ]

    – Siempre

    08/09/2012 a las 17:35


  • La desasignación nunca debe fallar. Si la liberación de un recurso puede fallar con una excepción, es prácticamente imposible escribir código C++ tolerante a fallas. La desasignación también ocurre en el propio destructor del contenedor (que también llamará a los destructores del tipo definido por el usuario), pero los destructores en C++ 11 son noexcept por defecto, y por una buena razón.

    – Adam H. Peterson

    07/08/2013 a las 20:52

  • @AdamH.Peterson: Los destructores no son excepto por defecto, pero asignadores no son. Notablemente, std::allocator::deallocate no es noexcept.

    – Nicolás Bolas

    7 ago 2013 a las 23:05

  • @NicolBolas, eso puede ser cierto, pero los destructores de contenedores de la biblioteca estándar son noexcept y esos destructores invocan desasignaciones en los asignadores. Si la función de desasignación del asignador falla, eventualmente verá un programa abortado por una violación de noexcept. Eso me sugiere que la razón por la que un move() podria no ser noexcept no tiene nada que ver con la falla de desasignación.

    – Adam H. Peterson

    8 de agosto de 2013 a las 1:25

El operador de asignación de movimiento en las clases de contenedor se define como noexcept porque muchos contenedores están diseñados para implementar la garantía de seguridad de excepción fuerte. Los contenedores implementan la fuerte garantía de seguridad de excepción porque antes de que hubiera operadores de asignación de movimiento, el contenedor tenía que copiarse. Si algo salía mal con la copia, el nuevo almacenamiento se eliminaba y el contenedor permanecía sin cambios. Ahora estamos atascados con ese comportamiento. Si la operación de asignación de movimiento no es noexcept, en su lugar se invoca el operador de asignación de copia más lento.

¿Ha sido útil esta solución?