c++ - smart - ¿Por qué el operador std:: unique_ptr*throw y operator-> no arroja?
unique_ptr c++ (4)
Francamente, esto me parece un defecto. Conceptualmente, a-> b siempre debe ser equivalente a (* a) .b, y esto se aplica incluso si a es un puntero inteligente. Pero si * a no es noexcept, entonces (* a) .b no lo es, y por lo tanto a-> b no debería ser.
En el borrador estándar de C ++ (N3485), establece lo siguiente:
20.7.1.2.4 unique_ptr observers [unique.ptr.single.observers]
typename add_lvalue_reference<T>::type operator*() const;
1 Requires: get() != nullptr.
2 Returns: *get().
pointer operator->() const noexcept;
3 Requires: get() != nullptr.
4 Returns: get().
5 Note: use typically requires that T be a complete type.
Puede ver que el operator*
(desreferencia) no se especifica como noexcept
, probablemente porque puede causar una segfault, pero entonces operator->
en el mismo objeto se especifica como noexcept
. Los requisitos para ambos son los mismos, sin embargo, hay una diferencia en la especificación de la excepción.
Me di cuenta de que tienen diferentes tipos de devolución, uno devuelve un puntero y el otro una referencia. ¿Eso está diciendo que el operator->
realidad no desreferencia nada?
El hecho es que usar operator->
en un puntero de cualquier tipo que sea NULL, segfault (es UB). ¿Por qué entonces, uno de estos se especifica como noexcept
y el otro no?
Estoy seguro de que he pasado por alto algo.
EDITAR:
Mirando std::shared_ptr
tenemos esto:
20.7.2.2.5 shared_ptr observers [util.smartptr.shared.obs]
T& operator*() const noexcept;
T* operator->() const noexcept;
¿No es lo mismo? ¿Tiene algo que ver con la semántica de propiedad diferente?
Por lo que vale, aquí hay un poco de la historia, y cómo las cosas se pusieron como están ahora.
Antes de N3025, el operator *
no se había especificado con noexcept
, pero su descripción contenía un Throws: nothing
. Este requisito fue eliminado en N3025 :
Cambie [unique.ptr.single.observers] como se indica (834) [Para más detalles, consulte la sección de Comentarios]:
typename add_lvalue_reference<T>::type operator*() const;
1 - Requiere:get() !=
0nullptr
.
2 - Devuelve:*get().
3 - Lanzamientos: nada.
Aquí está el contenido de la sección "Comentarios" mencionada anteriormente:
Durante las revisiones de este documento, se volvió controvertido cómo especificar correctamente la semántica operacional del operador *, el operador [] y las funciones de comparación heterogéneas. [structure.specifications] / 3 no especifica si un elemento Returns (en ausencia del nuevo Equivalente a formula) especifica los efectos. Además, no está claro si esto permitiría que dicha expresión de retorno salga a través de una excepción, si además se proporciona un elemento Throws: -Nothing (¿se le exigiría al implementador capturar esos?). Para resolver este conflicto, se eliminó cualquier elemento Throws existente para estas operaciones, que al menos es coherente con [unique.ptr.special] y otras partes del estándar. El resultado de esto es que damos ahora soporte implícito para potencialmente lanzar funciones de comparación, pero no para homogéneos == y! =, Lo que podría ser un poco sorprendente.
El mismo documento también contiene una recomendación para editar la definición de operator ->
, pero se lee de la siguiente manera:
pointer operator->() const;
4 - Requiere:get() !=
0nullptr.
5 - Devoluciones: get ().
6 - Lanza: nada.
7 - Nota: el uso generalmente requiere que T sea un tipo completo.
En cuanto a la pregunta en sí misma, se trata de una diferencia básica entre el propio operador y la expresión en la que se utiliza el operador.
Cuando utiliza operator*
, el operador desreferencia el puntero, que puede tirar.
Cuando utiliza operator->
, el operador solo devuelve un puntero (que no puede lanzar). Ese puntero se desreferencia en la expresión que contenía el signo ->
. Cualquier excepción a la desreferenciación del puntero ocurre en la expresión circundante más que en el operador mismo.
Respecto a:
¿Eso está diciendo que el operador-> en realidad no desreferencia nada?
No, la evaluación estándar de ->
para un operator->
sobrecarga de operator->
es:
a->b; // (a.operator->())->b
Es decir, la evaluación se define recursivamente, cuando el código fuente contiene un ->
, operator->
se aplica dando otra expresión con un ->
que puede referirse a un operator->
...
En cuanto a la pregunta general, si el puntero es nulo, el comportamiento no está definido, y la falta de noexcept
permite que se implemente una implementación. Si la firma fuera no noexcept
, la implementación no podría throw
(un throw
sería una llamada a std::terminate
).
Un segfault está fuera del sistema de excepción de C ++. Si desreferencia un puntero nulo, no obtendrá ningún tipo de excepción (bueno, al menos si cumple con la cláusula Require:
vea más detalles a continuación).
Para operator->
, típicamente se implementa como simplemente return m_ptr;
(o return get();
for unique_ptr
). Como puede ver, el operador en sí no puede arrojar, solo devuelve el puntero. Sin desreferenciación, nada. El lenguaje tiene algunas reglas especiales para p->identifier
:
§13.5.6 [over.ref] p1
Una expresión
x->m
se interpreta como(x.operator->())->m
para un objeto de clasex
de tipoT
siT::operator->()
existe y si el operador se selecciona como la mejor función de coincidencia por el mecanismo de resolución de sobrecarga (13.3).
Lo anterior aplica de manera recursiva y al final debe generar un puntero, para lo cual se usa el operator->
incorporado- operator->
. Esto permite a los usuarios de punteros e iteradores smart->fun()
hacer simplemente smart->fun()
sin preocuparse por nada.
Una nota para el Require:
partes de la especificación: Estas denotan condiciones previas. Si no los encuentras, estás invocando a UB.
¿Por qué entonces, uno de estos se especifica como noexcept y el otro no?
Para ser honesto, no estoy seguro. Parece que desreferenciar un puntero siempre debe ser noexcept
, sin embargo, unique_ptr
permite cambiar completamente el tipo de puntero interno (a través del eliminador). Ahora, como usuario, puede definir una semántica completamente diferente para el operator*
en su tipo de pointer
. Tal vez computa cosas sobre la marcha? Todas esas cosas divertidas, que pueden arrojar.
Mirando std :: shared_ptr tenemos esto:
Esto es fácil de explicar: shared_ptr
no admite la personalización mencionada anteriormente para el tipo de puntero, lo que significa que la semántica incorporada siempre se aplica, y *p
donde p
es T*
simplemente no arroja.