La mayoría de las respuestas aquí no abordan la ambigüedad inherente de tener un puntero en bruto en una firma de función, en términos de expresar intención. Los problemas son los siguientes:
La persona que llama no sabe si el puntero apunta a un solo objeto o al inicio de una "matriz" de objetos.
La persona que llama no sabe si el puntero "posee" la memoria a la que apunta. IE, si la función debe liberar memoria o no. ( foo(new int)
- ¿Es esto una pérdida de memoria?).
La persona que llama no sabe si se nullptr
puede pasar de forma segura a la función.
Todos estos problemas se resuelven mediante referencias:
Las referencias siempre se refieren a un solo objeto.
Las referencias nunca poseen la memoria a la que se refieren, son simplemente una vista a la memoria.
Las referencias no pueden ser nulas.
Esto hace que las referencias sean un candidato mucho mejor para uso general. Sin embargo, las referencias no son perfectas, hay un par de problemas importantes a considerar.
- Sin indirección explícita. Esto no es un problema con un puntero sin formato, ya que tenemos que usar el
&
operador para mostrar que de hecho estamos pasando un puntero. Por ejemplo, int a = 5; foo(a);
aquí no está nada claro que se esté pasando a por referencia y se pueda modificar.
- Nulabilidad Esta debilidad de los punteros también puede ser una fortaleza, cuando realmente queremos que nuestras referencias sean anulables. Al ver que
std::optional<T&>
no es válido (por buenas razones), los punteros nos dan esa nulabilidad que desea.
Entonces, parece que cuando queremos una referencia anulable con indirección explícita, ¿debemos alcanzar un T*
derecho? ¡Incorrecto!
Abstracciones
En nuestra desesperación por la nulabilidad, podemos alcanzar T*
y simplemente ignorar todas las deficiencias y la ambigüedad semántica enumeradas anteriormente. En cambio, deberíamos buscar lo que C ++ hace mejor: una abstracción. Si simplemente escribimos una clase que envuelve un puntero, obtenemos la expresividad, así como la nulabilidad y la indirección explícita.
template <typename T>
struct optional_ref {
optional_ref() : ptr(nullptr) {}
optional_ref(T* t) : ptr(t) {}
optional_ref(std::nullptr_t) : ptr(nullptr) {}
T& get() const {
return *ptr;
}
explicit operator bool() const {
return bool(ptr);
}
private:
T* ptr;
};
Esta es la interfaz más simple que se me ocurrió, pero hace el trabajo de manera efectiva. Permite inicializar la referencia, verificar si existe un valor y acceder al valor. Podemos usarlo así:
void foo(optional_ref<int> x) {
if (x) {
auto y = x.get();
// use y here
}
}
int x = 5;
foo(&x); // explicit indirection here
foo(nullptr); // nullability
¡Hemos logrado nuestros objetivos! Veamos ahora los beneficios, en comparación con el puntero sin formato.
- La interfaz muestra claramente que la referencia solo debe referirse a un objeto.
- Claramente, no posee la memoria a la que se refiere, ya que no tiene un destructor definido por el usuario y ningún método para eliminar la memoria.
- La persona que llama sabe que
nullptr
se puede transmitir, ya que el autor de la función solicita explícitamente unoptional_ref
Podríamos hacer que la interfaz sea más compleja desde aquí, como agregar operadores de igualdad, una monádica get_or
y una map
interfaz, un método que obtiene el valor o arroja una excepción, constexpr
soporte. Eso lo puedes hacer tú.
En conclusión, en lugar de usar punteros sin procesar, razone sobre lo que esos punteros realmente significan en su código, y aproveche una abstracción de biblioteca estándar o escriba la suya propia. Esto mejorará su código significativamente.
new
de crear un puntero y los problemas de propiedad resultantes.