smart programming pointer c++ smart-pointers c++17

programming - smart pointers c++



Uso de observer_ptr (6)

¿Cuál es exactamente el sentido de la construcción std::observer_ptr en la especificación técnica de los fundamentos de la biblioteca V2?

Me parece que todo lo que hace es envolver una T* desnuda, lo que parece un paso superfluo si no agrega seguridad de memoria dinámica.

En todo mi código utilizo std::unique_ptr donde necesito tomar la propiedad explícita de un objeto y std::shared_ptr donde puedo compartir la propiedad de un objeto.

Esto funciona muy bien y evita la desreferencia accidental de un objeto ya destruido.

std::observer_ptr no garantiza el tiempo de vida del objeto observado, por supuesto.

Si fuera construido a partir de std::unique_ptr o std::shared_ptr , vería un uso en dicha estructura, pero cualquier código que simplemente use T* probablemente continuará haciéndolo y si planean mudarse. para cualquier cosa sería std::shared_ptr y / o std::unique_ptr (dependiendo del uso).

Dada una función de ejemplo simple:

template<typename T> auto func(std::observer_ptr<T> ptr){}

Donde sería útil si impidiera que los punteros inteligentes destruyan su objeto almacenado mientras se observan.

Pero si quiero observar un std::shared_ptr o std::unique_ptr tengo que escribir:

auto main() -> int{ auto uptr = std::make_unique<int>(5); auto sptr = std::make_shared<int>(6); func(uptr.get()); func(sptr.get()); }

Lo que hace que no sea más seguro que:

template<typename T> auto func(T *ptr){}

Entonces, ¿cuál es el uso de esta nueva estructura?

¿Es solo para fuente de auto documentación?


¿Es solo por la auto documentación de la fuente?

Sí.


La proposal deja bastante claro que es solo para auto-documentación:

Este documento propone observer_ptr , un tipo de puntero (no muy) inteligente que no asume la responsabilidad de propiedad de sus puntos, es decir, de los objetos que observa. Como tal, está pensado como un reemplazo casi inmediato para los tipos de punteros sin procesar, con la ventaja de que, como un tipo de vocabulario, indica su uso previsto sin necesidad de un análisis detallado por parte de los lectores de códigos.


Parece de la proposal que std::observer_ptr es en gran medida para documentar que un puntero es una referencia no propietaria de un objeto, en lugar de una referencia propietaria , matriz , cadena o iterador .

Sin embargo, hay un par de otros beneficios de usar observer_ptr<T> sobre T* :

  1. Un valor predeterminado observer_ptr construido siempre se inicializará en nullptr ; un puntero regular puede o no inicializarse, dependiendo del contexto.
  2. observer_ptr solo admite operaciones que tienen sentido para una referencia ; esto impone el uso correcto:
    • operator[] no está implementado para observer_ptr , ya que esta es una operación de matriz .
    • La aritmética del puntero no es posible con observer_ptr , ya que estas son operaciones del iterador .
  3. Dos observer_ptr s tienen un ordenamiento débil estricto en todas las implementaciones, lo que no está garantizado para dos punteros arbitrarios. Esto se debe a que operator< está implementado en términos de std::less para observer_ptr (como con std::unique_ptr y std::shared_ptr ).
  4. observer_ptr<void> parece no ser compatible, lo que puede alentar el uso de soluciones más seguras (por ejemplo, std::any y std::variant )

Sí, el objetivo de std::observer_ptr es en gran parte simplemente "auto-documentación" y ese es un fin válido en sí mismo. Pero debe señalarse que podría decirse que no hace un gran trabajo, ya que no es exactamente lo que es un puntero "observador". En primer lugar, como señala Galik, para algunos el nombre parece implicar un compromiso de no modificar el objetivo, que no es la intención, por lo que un nombre como access_ptr sería mejor. Y en segundo lugar, sin ningún calificador, el nombre implicaría un respaldo de su comportamiento "no funcional". Por ejemplo, uno podría considerar un std::weak_ptr como un tipo de puntero "observador". Pero std::weak_ptr el caso donde el puntero sobrevive al objeto objetivo al proporcionar un mecanismo que permite que los intentos de acceso al objeto (desasignado) fallen de manera segura. La implementación de std::observer_ptr no satisface este caso. Entonces, quizás raw_access_ptr sería un nombre mejor, ya que indicaría mejor su defecto funcional.

Entonces, como usted justificadamente pregunta, ¿cuál es el objetivo de este puntero "no propietario" funcionalmente desafiado? La razón principal es probablemente el rendimiento. Muchos programadores de C ++ perciben que la sobrecarga de std::share_ptr es demasiado alta y, por lo tanto, solo usan punteros sin formato cuando necesitan punteros "observadores". El std::observer_ptr propuesto intenta proporcionar una pequeña mejora de la claridad del código a un costo de rendimiento aceptable. Específicamente, costo de rendimiento cero.

Desafortunadamente, parece haber un optimismo generalizado pero, en mi opinión, poco realista acerca de cuán seguro es usar punteros crudos como punteros "observadores". En particular, aunque es fácil establecer un requisito de que el objeto objetivo debe sobrevivir al std::observer_ptr , no siempre es fácil estar absolutamente seguro de que se está cumpliendo. Considera este ejemplo:

struct employee_t { employee_t(const std::string& first_name, const std::string& last_name) : m_first_name(first_name), m_last_name(last_name) {} std::string m_first_name; std::string m_last_name; }; void replace_last_employee_with(const std::observer_ptr<employee_t> p_new_employee, std::list<employee_t>& employee_list) { if (1 <= employee_list.size()) { employee_list.pop_back(); } employee_list.push_back(*p_new_employee); } void main(int argc, char* argv[]) { std::list<employee_t> current_employee_list; current_employee_list.push_back(employee_t("Julie", "Jones")); current_employee_list.push_back(employee_t("John", "Smith")); std::observer_ptr<employee_t> p_person_who_convinces_boss_to_rehire_him(&(current_employee_list.back())); replace_last_employee_with(p_person_who_convinces_boss_to_rehire_him, current_employee_list); }

Es posible que nunca se le haya ocurrido al autor de la función replace_last_employee_with() que la referencia a la nueva contratación también podría ser una referencia al empleado existente que se reemplazará, en cuyo caso la función puede causar inadvertidamente el objetivo de su std::observer_ptr<employee_t> parámetro std::observer_ptr<employee_t> será desasignado antes de que termine de usarlo.

Es un ejemplo artificial, pero este tipo de cosas puede suceder fácilmente en situaciones más complejas. Por supuesto, el uso de punteros crudos es perfectamente seguro en la gran mayoría de los casos. El problema es que hay una minoría de casos en los que es fácil suponer que es seguro cuando en realidad no lo es.

Si reemplazar el parámetro std::observer_ptr<employee_t> con std::shared_ptr o std::weak_ptr es por cualquier razón no aceptable, ahora hay otra opción segura, y esta es la parte del complemento descarado de la respuesta, " apuntadores registrados ". "punteros registrados" son punteros inteligentes que se comportan como punteros sin procesar, excepto que se establecen (automáticamente) en null_ptr cuando se destruye el objeto de destino, y de forma predeterminada arrojarán una excepción si intenta acceder a un objeto que ya ha sido borrado En general son faster que std :: shared_ptrs, pero si sus demandas de rendimiento son realmente estrictas, los punteros registrados pueden ser "deshabilitados" (reemplazados automáticamente con su contraparte de puntero sin procesar) con una directiva en tiempo de compilación, lo que les permite ser utilizados (y incurrir en overhead) en modos de depuración / prueba / beta solamente.

Entonces, si va a haber un puntero de "observador" basado en punteros sin procesar, podría decirse que debería haber uno basado en apuntadores registrados y tal vez como lo sugirió OP, uno basado en std :: shared_ptr también.


Una buena consecuencia de utilizar std::observer_ptr sobre punteros sin procesar es que proporciona una mejor alternativa a la sintaxis de instanciación de puntero múltiple, confusa y propensa a errores, heredada de C.

std::observer_ptr<int> a, b, c;

es una mejora en

int *a, *b, *c;

que es un poco extraño desde una perspectiva de C ++ y puede ser mal escrita fácilmente

int* a, b, c;


Cuando necesita acceso compartido pero no comparte propiedad .

El problema es que los punteros crudos siguen siendo muy útiles y tienen escenarios de casos de uso perfectamente respetables.

Cuando un puntero sin formato es administrado por un puntero inteligente, su limpieza está garantizada y, por lo tanto, dentro de la vida útil del puntero inteligente , tiene sentido acceder a los datos reales a través del puntero sin procesar que maneja el puntero inteligente .

Entonces, cuando creamos funciones, eso normalmente tomaría un puntero sin formato, una buena forma de prometer que la función no eliminará ese puntero es usar una clase fuertemente tipada como std::observer_ptr .

Al pasar un puntero sin formato administrado como argumento a un parámetro de función std::observer_ptr , sabemos que la función no lo delete .

Es una forma para que una función diga "dame tu puntero, no interferiré con su asignación, solo lo usaré para observar".

Por cierto, no estoy interesado en el nombre std::observer_ptr porque eso implica que puedes mirar pero no tocar. Pero eso no es realmente cierto. Me hubiera ido con algo más como access_ptr .

Nota adicional:

Este es un caso de uso diferente de std::shared_ptr . std::shared_ptr se trata de compartir la propiedad y solo debe usarse cuando no se puede determinar qué objeto propietario saldrá primero del alcance.

std::observer_ptr , por otro lado, es para cuando quiere compartir el acceso pero no la propiedad .

No es realmente apropiado usar std::shared_ptr simplemente para compartir el acceso porque podría ser muy ineficiente.

Por lo tanto, ya sea que administre su puntero de destino utilizando std::unique_ptr o std::shared_ptr todavía hay un caso de uso para los punteros crudos y, por lo tanto, es racional para un std::observer_ptr .