¿Hay beneficios de pasar por puntero sobre pasar por referencia en C ++?

225

¿Cuáles son los beneficios de pasar por puntero sobre pasar por referencia en C ++?

Últimamente, he visto una serie de ejemplos que eligieron pasar argumentos de función por punteros en lugar de pasar por referencia. ¿Hay beneficios al hacer esto?

Ejemplo:

func(SPRITE *x);

con una llamada de

func(&mySprite);

vs.

func(SPRITE &x);

con una llamada de

func(mySprite);
Matt Pascoe
fuente
No se olvide newde crear un puntero y los problemas de propiedad resultantes.
Martin York

Respuestas:

216

Un puntero puede recibir un parámetro NULL, un parámetro de referencia no. Si alguna vez existe la posibilidad de que desee pasar "sin objeto", utilice un puntero en lugar de una referencia.

Además, pasar por puntero le permite ver explícitamente en el sitio de la llamada si el objeto se pasa por valor o por referencia:

// Is mySprite passed by value or by reference?  You can't tell 
// without looking at the definition of func()
func(mySprite);

// func2 passes "by pointer" - no need to look up function definition
func2(&mySprite);
Adam Rosenfield
fuente
18
Respuesta incompleta El uso de punteros no autorizará el uso de objetos temporales / promocionados, ni el uso de objetos puntiagudos como objetos de tipo pila. Y sugerirá que el argumento puede ser NULL cuando, la mayoría de las veces, se debe prohibir un valor NULL. Lea la respuesta de litb para obtener una respuesta completa.
paercebal
La segunda llamada de función solía ser anotada func2 passes by reference. Si bien agradezco que quisieras decir que pasa "por referencia" desde una perspectiva de alto nivel, implementado pasando un puntero en una perspectiva de nivel de código, esto fue muy confuso (ver stackoverflow.com/questions/13382356/… ).
ligereza corre en órbita el
Simplemente no compro esto. Sí, pasa un puntero, por lo que debe ser un parámetro de salida, porque lo que se señala no puede ser constante.
deworde
¿No tenemos esta referencia de paso en C? Estoy usando la última versión de codeblock (mingw) y en eso selecciono un proyecto C. Sigue pasando por referencia (func (int & a)) funciona. ¿O está disponible en C99 o C11 en adelante?
Jon Wheelock
1
@ JonWheelock: No, C no tiene referencia de paso en absoluto. func(int& a)no es válido C en ninguna versión del estándar. Probablemente esté compilando sus archivos como C ++ por accidente.
Adam Rosenfield
245

Pasando por puntero

  • La persona que llama tiene que tomar la dirección -> no transparente
  • Se puede proporcionar un valor 0 para significar nothing. Esto se puede usar para proporcionar argumentos opcionales.

Pase por referencia

  • La persona que llama solo pasa el objeto -> transparente. Tiene que usarse para la sobrecarga del operador, ya que la sobrecarga para los tipos de puntero no es posible (los punteros son tipos incorporados). Entonces no puedes string s = &str1 + &str2;usar punteros.
  • No hay valores 0 posibles -> La función llamada no tiene que verificarlos
  • La referencia a const también acepta temporales: los void f(const T& t); ... f(T(a, b, c));punteros no pueden usarse así, ya que no puede tomar la dirección de un temporal.
  • Por último, pero no menos importante, las referencias son más fáciles de usar -> menos posibilidades de errores.
Johannes Schaub - litb
fuente
77
Al pasar por el puntero también se plantea '¿Se transfiere la propiedad o no?' pregunta. Este no es el caso con las referencias.
Frerich Raabe
43
No estoy de acuerdo con "menos posibilidades de errores". Al inspeccionar el sitio de la llamada y el lector ve "foo (& s)", queda claro de inmediato que los s pueden modificarse. Cuando lee "foo (s)" no está nada claro si se pueden modificar los s. Esta es una fuente importante de errores. Tal vez haya menos posibilidades de una determinada clase de errores, pero en general, pasar por referencia es una gran fuente de errores.
William Pursell
25
¿Qué quieres decir con "transparente"?
Gbert90
2
@ Gbert90, si ve foo (& a) en un sitio de llamadas, sabe que foo () toma un tipo de puntero. Si ve foo (a), no sabe si se necesita una referencia.
Michael J. Davenport
3
@ MichaelJ.Davenport: en su explicación, sugiere "transparente" para significar algo como "obvio que la persona que llama está pasando un puntero, pero no es obvio que la persona que llama está pasando una referencia". En la publicación de Johannes, dice "Pasando por puntero - La persona que llama tiene que tomar la dirección -> no transparente" y "Pasar por referencia - La persona que llama simplemente pasa el objeto -> transparente" - que es casi opuesto a lo que dices . Creo que la pregunta de Gbert90 "¿Qué quieres decir con" transparente "" sigue siendo válida.
Happy Green Kid Naps el
66

Me gusta el razonamiento de un artículo de "cplusplus.com:"

  1. Pase por valor cuando la función no quiera modificar el parámetro y el valor sea fácil de copiar (ints, doubles, char, bool, etc ... tipos simples. Std :: string, std :: vector y todos los demás STL Los contenedores NO son tipos simples).

  2. Pase por el puntero constante cuando el valor es costoso de copiar Y la función no desea modificar el valor señalado Y NULO es un valor válido esperado que maneja la función.

  3. Pase por un puntero no constante cuando el valor es costoso de copiar Y la función quiere modificar el valor señalado Y NULL es un valor válido y esperado que maneja la función.

  4. Pase por referencia constante cuando el valor es costoso de copiar Y la función no quiere modificar el valor al que se hace referencia AND NULL no sería un valor válido si se utilizara un puntero.

  5. Pase por referencia no cont cuando el valor es costoso de copiar Y la función quiere modificar el valor al que se hace referencia AND NULL no sería un valor válido si se utilizara un puntero.

  6. Al escribir funciones de plantilla, no hay una respuesta clara porque hay algunas compensaciones a considerar que están más allá del alcance de esta discusión, pero es suficiente decir que la mayoría de las funciones de plantilla toman sus parámetros por valor o referencia (constante) , sin embargo, debido a que la sintaxis del iterador es similar a la de los punteros (asterisco para "desreferenciar"), cualquier función de plantilla que espere iteradores como argumentos también aceptará de manera predeterminada los punteros (y no comprobará NULL ya que el concepto de iterador NULL tiene una sintaxis diferente )

http://www.cplusplus.com/articles/z6vU7k9E/

Lo que deduzco de esto es que la principal diferencia entre elegir usar un puntero o un parámetro de referencia es si NULL es un valor aceptable. Eso es.

Si el valor es input, output, modificable, etc., debe estar en la documentación / comentarios sobre la función, después de todo.

R. Navega
fuente
Sí, para mí los términos relacionados con NULL son las principales preocupaciones aquí. Gracias por citar ..
binaryguy
62

"Suficiente cuerda para pegarse un tiro en el pie" de Allen Holub enumera las siguientes 2 reglas:

120. Reference arguments should always be `const`
121. Never use references as outputs, use pointers

Enumera varias razones por las que se agregaron referencias a C ++:

  • son necesarios para definir constructores de copia
  • son necesarios para sobrecargas del operador
  • const las referencias le permiten tener una semántica de paso por valor mientras evita una copia

Su punto principal es que las referencias no deben usarse como parámetros de 'salida' porque en el sitio de la llamada no hay indicación de si el parámetro es una referencia o un parámetro de valor. Entonces su regla es usar solo constreferencias como argumentos.

Personalmente, creo que esta es una buena regla general, ya que deja más claro cuándo un parámetro es un parámetro de salida o no. Sin embargo, aunque personalmente estoy de acuerdo con esto en general, me dejo influenciar por las opiniones de otros en mi equipo si ellos defienden los parámetros de salida como referencias (a algunos desarrolladores les gustan inmensamente).

Michael Burr
fuente
8
Mi postura en ese argumento es que si el nombre de la función lo hace totalmente obvio, sin verificar los documentos, que el parámetro será modificado, entonces una referencia no constante está bien. Entonces personalmente permitiría "getDetails (DetailStruct & result)". Un puntero allí plantea la posibilidad fea de una entrada NULL.
Steve Jessop
3
Esto es engañoso. Incluso si a algunos no les gustan las referencias, son una parte importante del lenguaje y deben usarse como tal. Esta línea de razonamiento es como decir que no use plantillas, siempre puede usar contenedores de vacío * para almacenar cualquier tipo. Lea la respuesta de litb.
David Rodríguez - dribeas
44
No veo cómo esto es engañoso: hay momentos en que se requieren referencias, y hay momentos en que las mejores prácticas podrían sugerir no usarlos, incluso si pudiera. Lo mismo puede decirse de cualquier característica del lenguaje: herencia, amigos que no son miembros, sobrecarga del operador, MI, etc ...
Michael Burr
Por cierto, estoy de acuerdo en que la respuesta de litb es muy buena y ciertamente es más completa que esta: simplemente elegí concentrarme en discutir una justificación para evitar el uso de referencias como parámetros de salida.
Michael Burr
1
Esta regla se usa en la guía de estilo de google c ++: google-styleguide.googlecode.com/svn/trunk/…
Anton Daneyko
9

Aclaraciones a las publicaciones anteriores:


Las referencias NO son una garantía de obtener un puntero no nulo. (Aunque a menudo los tratamos como tales).

Si bien es un código horriblemente malo, como en el caso de llevarlo detrás del código incorrecto de la leñera , lo siguiente se compilará y ejecutará: (al menos en mi compilador).

bool test( int & a)
{
  return (&a) == (int *) NULL;
}

int
main()
{
  int * i = (int *)NULL;
  cout << ( test(*i) ) << endl;
};

El verdadero problema que tengo con las referencias radica en otros programadores, en adelante denominados IDIOTS , que asignan en el constructor, desasignan en el destructor y no proporcionan un constructor de copia u operador = ().

De repente hay un mundo de diferencia entre foo (BAR bar) y foo (BAR & bar) . (Se invoca la operación automática de copia a nivel de bit. La desasignación en el destructor se invoca dos veces).

Afortunadamente, los compiladores modernos recogerán esta doble desasignación del mismo puntero. Hace 15 años, no lo hicieron. (En gcc / g ++, use setenv MALLOC_CHECK_ 0 para volver a visitar las formas anteriores). Como resultado de DEC UNIX, la misma memoria se asigna a dos objetos diferentes. Mucha diversión de depuración allí ...


Más prácticamente:

  • Las referencias ocultan que está cambiando los datos almacenados en otro lugar.
  • Es fácil confundir una referencia con un objeto copiado.
  • Los punteros lo hacen obvio!
Mr.Ree
fuente
16
ese no es el problema de la función o las referencias. Estás rompiendo las reglas del idioma. desreferenciar un puntero nulo por sí solo ya es un comportamiento indefinido. "Las referencias NO son una garantía de obtener un puntero no nulo": el estándar mismo dice que sí. otras formas constituyen un comportamiento indefinido.
Johannes Schaub - litb
1
Estoy de acuerdo con litb. Si bien es cierto, el código que nos está mostrando es más sabotaje que cualquier otra cosa. Hay formas de sabotear cualquier cosa, incluidas las notaciones de "referencia" y "puntero".
paercebal
1
¡Dije que era "llevarte detrás del código malo de la leñera"! En la misma línea, también puede tener i = new FOO; eliminar i; prueba (* i); Otra ocurrencia de puntero / referencia colgante (desafortunadamente común).
Mr.Ree
1
En realidad , el problema no es desreferenciar NULL, sino USAR ese objeto desreferenciado (nulo). Como tal, realmente no hay diferencia (aparte de la sintaxis) entre punteros y referencias desde una perspectiva de implementación del lenguaje. Son los usuarios los que tienen expectativas diferentes.
Mr.Ree
2
Independientemente de lo que haga con la referencia devuelta, en el momento en que dice *i, su programa tiene un comportamiento indefinido. Por ejemplo, el compilador puede ver este código y asumir "OK, este código tiene un comportamiento indefinido en todas las rutas de código, por lo que toda esta función debe ser inalcanzable". Luego asumirá que no se toman todas las ramas que conducen a esta función. Esta es una optimización realizada regularmente.
David Stone
5

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 nullptrpuede 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 nullptrse 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_ory una mapinterfaz, un método que obtiene el valor o arroja una excepción, constexprsoporte. 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.

Tom VH
fuente
3

Realmente no. Internamente, el paso por referencia se realiza esencialmente pasando la dirección del objeto referenciado. Por lo tanto, realmente no hay ganancias de eficiencia al pasar un puntero.

Sin embargo, pasar por referencia tiene un beneficio. Se garantiza que tendrá una instancia de cualquier objeto / tipo que se esté pasando. Si pasa un puntero, corre el riesgo de recibir un puntero NULL. Al usar el paso por referencia, está empujando una comprobación NULL implícita de un nivel a la persona que llama de su función.

Brian
fuente
1
Eso es tanto una ventaja como una desventaja. Muchas API usan punteros NULL para significar algo útil (es decir, la especificación de tiempo NULL espera para siempre, mientras que el valor significa esperar tanto).
Greg Rogers
1
@Brian: No quiero ser quisquilloso, pero: no diría que uno está garantizado para obtener una instancia al obtener una referencia. Las referencias colgantes todavía son posibles si el llamador de una función desreferencia un puntero colgante, que el destinatario no puede saber.
miedo el
a veces incluso puede obtener rendimiento mediante el uso de referencias, ya que no necesitan tomar ningún almacenamiento y no tienen direcciones asignadas para sí mismos. No se requiere indirección.
Johannes Schaub - litb
Los programas que contienen referencias colgantes no son válidos en C ++. Por lo tanto, sí, el código puede suponer que todas las referencias son válidas.
Konrad Rudolph
2
Definitivamente puedo desreferenciar un puntero nulo y el compilador no podrá decir ... si el compilador no puede decir que es "C ++ inválido", ¿es realmente inválido?
rmeador