2012-03-05 17 views
15

Comenzaré por ejemplo. Hay una buena clase de "tokenizer" en potencia. Se toma una cadena que se tokens como un parámetro en un constructor:¿Cuál es la forma preferida de pasar el puntero/referencia al objeto existente en un constructor?

std::string string_to_tokenize("a bb ccc ddd 0"); 
boost::tokenizer<boost::char_separator<char> > my_tok(string_to_tokenize); 
/* do something with my_tok */ 

La cadena no es Modifed en el tokenizer, por lo que se pasa por referencia objeto const. Por lo tanto me puede pasar un objeto temporal no:

boost::tokenizer<boost::char_separator<char> > my_tok(std::string("a bb ccc ddd 0")); 
/* do something with my_tok */ 

Todo se ve muy bien, pero si trato de usar el señalizador, se produce un desastre. Después de una breve investigación, me di cuenta de que la clase tokenizer almacena la referencia que le di, y la uso en un uso posterior. Por supuesto, no puede funcionar bien para hacer referencia al objeto temporal.

La documentación no dice explícitamente que el objeto pasado en el constructor se usará más tarde, pero está bien, tampoco está indicado, que no será :) Así que no puedo asumir esto, mi error.

Sin embargo, es un poco confuso. En un caso general, cuando un objeto toma otro por referencia constante, sugiere que se puede dar un objeto temporal allí. ¿Qué piensas? ¿Es esta una mala convención? ¿Tal vez se debería usar puntero a objeto (en lugar de referencia) en tales casos? O incluso más: ¿no sería útil tener alguna palabra clave especial para el argumento que permita/desaprobe el dar un objeto temporal como parámetro?

EDIT: La documentación (versión 1.49) es bastante minimalista y la única parte que puede sugerir un problema de este tipo es:

Nota: Sin el análisis se hace realmente en la construcción. El análisis se realiza bajo demanda ya que se accede a los tokens a través del iterador provisto por begin.

Pero no establece explícitamente, que se utilizará el mismo objeto que se le dio.

Sin embargo, el objetivo de esta pregunta es más bien la discusión sobre el estilo de codificación en tal caso, este es solo un ejemplo que me inspiró.

+0

sido mordido por lo mismo. Cuando puedo, uso boost :: ref como ctor arg ahora para al menos indicar que la referencia se almacenará – Anycorn

+0

. Me sorprendería si realmente hay un error como este en boost :: tokenizer. – CashCow

+0

@CashCow: es más un error en la documentación, en el sentido de que 'tokenizer' mantiene una referencia a su argumento constructor durante toda su vida, que es infernal con los temporales ... –

Respuesta

8

Personalmente, creo que es una mala idea, y sería mejor escribir el constructor para copiar la cadena, o para tomar un const std::string* en su lugar. Es solo un personaje extra para que la persona que llama escriba, pero ese personaje los detiene accidentalmente usando un temporal.

Como regla: no cree responsabilidades en las personas para mantener los objetos sin que sea muy obvio que tienen esa responsabilidad.

Creo que una palabra clave especial no sería una solución lo suficientemente completa como para justificar el cambio de idioma. En realidad, no son los temporales los que son el problema, es cualquier objeto que vive por menos tiempo que el objeto que se está construyendo. En algunas circunstancias, un temporal estaría bien (por ejemplo, si el objeto tokenizer también fuera temporal en la misma expresión completa). Realmente no quiero meterme con el lenguaje por la mitad de la corrección, y hay soluciones más completas disponibles (por ejemplo, tome un shared_ptr, aunque eso tiene sus propios problemas).

"Así que no puedo asumir esto, mi error"

No creo que realmente es su error, estoy de acuerdo con Frerich que, además de ser en contra de mi guía de estilo personal de hacer esto en absoluto, si lo haces y no documentas, entonces eso es un error de documentación en cualquier guía de estilo razonable.

Es absolutamente esencial que la vida útil requerida de los parámetros de la función de referencia esté documentada, si es otra cosa que "al menos tan larga como la llamada a la función". Es algo que los documentos a menudo son laxos, y debe hacerse correctamente para evitar errores.

Incluso en los lenguajes recogidos de basura, donde la vida útil se maneja automáticamente y tiende a descuidarse, importa si puede cambiar o reutilizar su objeto sin cambiar el comportamiento de algún otro objeto que lo haya pasado al método de, alguna vez en el pasado. Por lo tanto, las funciones deben documentar si conservan un alias para sus argumentos en cualquier lenguaje que carezca de transparencia referencial. Especialmente en C++, donde la duración del objeto es el problema del que llama.

Lamentablemente, el único mecanismo para realmente garantizar que su función no puede retener una referencia es pasar por valor, que tiene un costo de rendimiento. Si puede inventar un lenguaje que permita alias normalmente, pero también tiene una propiedad de estilo C restrict que se aplica en tiempo de compilación, const-style, para evitar que las funciones retengan referencias a sus argumentos, entonces buena suerte y regístrate. .

+0

Estoy de acuerdo con usted, y estoy sorprendido de que lo haya impulsado. Tomaría el parámetro por referencia no constante, lo que garantiza que el usuario NO pase de manera temporal. También puedo almacenar un miembro std :: string e intercambiar con el parámetro pasado, y dejar que el usuario cree su propia copia si quiere su cadena original. – CashCow

+0

meta comment: ¿por qué es una respuesta wiki de la comunidad? +1 de mí en cualquier caso. – Francesco

+0

@Francesco: me he desinteresado más o menos de intentar descubrir qué hay en el tema para SO, eso es para que otros decidan. Pero generalmente no tomo representantes para preguntas de opinión. –

11

Si alguna función (tal como un constructor) toma un argumento como referencia a const entonces debería ya sea

  • Documento claramente que el tiempo de vida del objeto referenciado debe satisfacer ciertos requisitos (como en "no se destruye antes de que esto suceda y que")

o

  • Crea copias internamente si necesita hacer uso del objeto dado en un momento posterior.

En este caso en particular (la clase boost::tokenizer) Me asumir que este último no se hace por razones de rendimiento y/o para hacer que la clase se puede utilizar con los tipos de envases que no son copiables, incluso en el primer lugar . Por esta razón, consideraría esto un error de documentación.

3

Como han dicho otros, el ejemplo boost::tokenizer es el resultado de un error en el tokenizer o de una advertencia que falta en la documentación.

Para responder en general a la pregunta, encontré útil la siguiente lista de prioridades. Si no puede elegir una opción por algún motivo, vaya al siguiente elemento.

  1. Pass por valor (copiable a un coste aceptable y no es necesario cambiar objeto original)
  2. Pasar por referencia const (no necesitar cambiar objeto original)
  3. Pass por referencia (necesidad para cambiar el objeto original)
  4. Pase por shared_ptr (la vida útil del objeto es administrada por otra cosa, esto también muestra claramente la intención de mantener la referencia)
  5. Pase por el puntero sin formato (tiene una dirección para enviar, o no puede usar un puntero inteligente por alguna razón)

Además, si su razonamiento para elegir el siguiente elemento de la lista es "rendimiento", entonces siéntese y mida la diferencia. Según mi experiencia, la mayoría de las personas (especialmente con antecedentes de Java o C#) tienden a sobreestimar el costo de pasar un objeto por valor (y subestiman el costo de desreferencia). Pasar por valor es la opción más segura (no causará sorpresas fuera del objeto o función, ni siquiera en otro hilo), no pierda esa gran ventaja fácilmente.

1

Mucho tiempo dependerá del contexto, por ejemplo, si se trata de un funtor que se llamará de una vez o similar, entonces a menudo almacenará una referencia o un puntero dentro de su functor a un objeto que espera tendrá una vida más allá de tu functor.

Si se trata de una clase de uso general, entonces hay que considerar cómo las personas van a usarla.

Si está escribiendo un tokenizador, debe considerar que copiar lo que está tokenizando puede ser costoso, sin embargo, también debe tener en cuenta que si está escribiendo una biblioteca de impulso lo está escribiendo para el público en general que lo hará Úselo de una manera multiuso.

Almacenar un const char * sería mejor que un std::string const& aquí. Si el usuario tiene un std::string, entonces el const char * seguirá siendo válido siempre que no modifiquen su cadena, y probablemente no lo harán. Si tienen un const char * o algo que contenga una matriz de caracteres y los entregue, lo copiará de todos modos para crear el std::string const & y correrá un gran peligro de no vivir más allá de su constructor.

Por supuesto, con un const char * no puede usar todas las funciones adorables std::basic_string en su implementación.

Hay una opción para tomar, como parámetro, un std::string& (no referencia) que debe garantizar (con un compilador compatible) que nadie pasará de forma temporal, pero podrá documentar que no lo hace en realidad lo cambian, y la razón detrás de su código aparentemente no const-correcto. Tenga en cuenta que también he usado este truco en mi código. Y puedes usar felizmente las funciones de búsqueda de cuerdas. (Además, si lo desea, tome basic_string en lugar de string para que pueda tokenizar cadenas de caracteres anchas también).

+0

Acepto que la referencia no constante ayudaría a evitar ese error, pero este es un tipo de solución. Ciertamente no es una solución limpia, ¿verdad? – peper0

+0

En mi opinión, "const char *" no resolvería el problema en la mayoría de los casos, ya que aún puedo pasar una cadena (algo) .c_str() que hace referencia al objeto temporal. – peper0

Cuestiones relacionadas