2012-01-25 10 views
5

A veces nos gusta tomar un parámetro grande por referencia, y también para hacer la referencia const si es posible para advertir que es un parámetro de entrada. Pero al hacer la referencia const, el compilador se permite a sí mismo convertir datos si es del tipo incorrecto. Esto significa que no es tan eficiente, pero más preocupante es el hecho de que creo que me estoy refiriendo a los datos originales; quizás tomaré su dirección, sin darme cuenta de que estoy, en efecto, tomando la dirección de un temporal.¿Es posible tomar un parámetro por referencia constante, mientras se prohíben las conversiones para que no se pasen los temporales?

La llamada a bar en este código falla. Esto es deseable, porque la referencia no es del tipo correcto. La llamada al bar_const también es del tipo incorrecto, pero se compila silenciosamente. Esto no es deseable para mí.

#include<vector> 
using namespace std; 

int vi; 

void foo(int &) { } 
void bar(long &) { } 
void bar_const(const long &) { } 

int main() { 
    foo(vi); 
    // bar(vi); // compiler error, as expected/desired 
    bar_const(vi); 
} 

¿Cuál es la forma más segura de pasar una referencia liviana y de solo lectura? Estoy tentado de crear una nueva plantilla tipo referencia.

(Obviamente, int y long son muy pequeños tipos. Pero han sido atrapados con estructuras más grandes que se pueden convertir el uno al otro. No quiero que esto suceda en silencio cuando estoy tomando una referencia constante. a veces, marcando los constructores como explícita ayuda, pero eso no es lo ideal)

actualización: me imagino un sistema como el siguiente: imagine tener dos funciones X byVal(); y X& byRef(); y el siguiente bloque de código:

X x; 
const_lvalue_ref<X> a = x; // I want this to compile 
const_lvalue_ref<X> b = byVal(); // I want this to fail at compile time 
const_lvalue_ref<X> c = byRef(); // I want this to compile 

Ese ejemplo se basa en variables locales, pero también quiero que funcione con parámetros. Quiero obtener algún tipo de mensaje de error si accidentalmente paso un ref-to-temporary o un ref-to-a-copy cuando creo que voy a pasar algo ligero, como un ref-to-lvalue. Esto es solo un "estándar de codificación": si realmente quiero permitir pasar una referencia a una temporal, entonces usaré un const X& directo. (Estoy encontrando this piece on Boost's FOREACH para ser bastante útil.)

+0

Usted dice que marca constructores explícita "ayuda, pero no es lo ideal" - lo que podría ser la solución ideal? Considera la respuesta de James Kanze. –

+0

Los constructores no '' explícitos' tienen sus usos. Por ejemplo, al pasar cosas por valor, o bien con cosas como 'A a; B b = a; '. Es el caso específico donde cambiar 'T &' a 'const T &' hace más que prohibir modificaciones. Parece un poco inconsistente que agregar 'const' a una referencia hace más que solo prohibir la modificación. (Voy a comentar todas las respuestas tarde o temprano, todas son interesantes.) –

Respuesta

0

(respondiendo a mi propia pregunta, gracias a this great answer en otra pregunta que hice. Gracias @hvd.)

En resumen, marcando un parámetro de función como volatile significa que no se puede enlazar a un valor de lado derecho. (¿Alguien puede fijar una cotización estándar para eso? Los temporales pueden estar limitados a const&, pero no a const volatile & aparentemente. Esto es lo que obtengo en g ++ - 4.6.1. (Extra: vea this extended comment stream para algunos detalles sangrientos que son manera sobre mi cabeza :-)))

void foo(const volatile Input & input, Output & output) { 
} 

foo(input, output); // compiles. good 
foo(get_input_as_value(), output); // compile failure, as desired. 

Pero, no realidad quieren que los parámetros sean volatile. Así que he escrito una pequeña envoltura para const_cast volatile de distancia. Por lo que la firma del foo se convierte esto en su lugar:

void foo(const_lvalue<Input> input, Output & output) { 
} 

cuando la capa es:

template<typename T> 
struct const_lvalue { 
    const T * t; 
    const_lvalue(const volatile T & t_) : t(const_cast<const T*>(&t_)) {} 
    const T* operator->() const { return t; } 
}; 

Esto puede ser creado a partir de un valor-I sólo

Cualquier desventajas? Puede significar que accidentalmente uso incorrectamente un objeto que es realmente volátil, pero nunca he usado volatile en mi vida. Así que esta es la solución correcta para mí, creo.

Espero tener el hábito de hacer esto con todos los parámetros adecuados de forma predeterminada.

Demo on ideone

1

No puede, y aunque pudiera, probablemente no ayudaría mucho. Considere :

void another(long const& l) 
{ 
    bar_const(l); 
} 

Incluso si de alguna manera podría prevenir la unión a un temporal como entrada a bar_const, funciona como another podrían ser llamados con la referencia unido a un temporal, y que iba a terminar en el misma situacion.

Si no puede aceptar un temporal, tendrá que utilizar una referencia a un no constante , o un puntero:

void bar_const(long const* l); 

requiere una lvalue inicializarlo. Por supuesto, una función como

void another(long const& l) 
{ 
    bar_const(&l); 
} 

seguirá causando problemas. Pero si adopta globalmente la convención a use un puntero si la duración del objeto debe extenderse más allá del final de la llamada, , entonces ojalá el autor de another piense por qué está tomando la dirección y evítela.

+0

Creo que me gustaría una convención en la que '&' fuera solo para los parámetros no const. Para poder leer 'bar (a, b, & c)' y ver que 'a' y' b' son ingresados ​​y 'c' almacenará la salida. Tu respuesta sugiere la convención opuesta. –

+0

@AaronMcDaid Hay varias convenciones posibles diferentes. Básicamente, tenemos dos alternativas, puntero y referencia, pero más de dos distinciones que vale la pena hacer. Entre las convenciones que he visto están las referencias de uso siempre que sea posible, use punteros si la duración del objeto debe extenderse más allá de la llamada de función, y use punteros si el objeto será modificado; Estoy seguro de que los demás también son razonables. Lo importante es que todos en el proyecto usan la misma convención. –

7

Bueno, si su "gran parámetro" es una clase, lo primero que debe hacer es asegurarse de que se marca ningún constructor único parámetro explícito (aparte del constructor de copia):

class BigType 
{ 
public: 
    explicit BigType(int); 
}; 

Esto se aplica a los constructores que tienen parámetros predeterminados que potencialmente podrían invocarse con un solo argumento, también.

Entonces no se convertirá automáticamente a, ya que no existen constructores implícitos que el compilador pueda usar para realizar la conversión. Es probable que no tiene ningún operadores de conversión globales que hacen ese tipo, pero si lo hace, entonces

Si eso no funciona para usted, usted podría utilizar un poco de magia plantilla, como:

template <typename T> 
void func(const T &); // causes an undefined reference at link time. 

template <> 
void func(const BigType &v) 
{ 
    // use v. 
} 
+0

Apuesto a que podría poner un 'static_assert' dependiente del tipo en el func de la plantilla para atraparlo incluso en tiempo de compilación. –

+0

Luce bien. Hice más experimentos con esta respuesta. Parece que la función definida no necesita ser una plantilla. es decir, que la función de plantilla atraerá todas las llamadas no válidas, y la función sin plantilla puede proporcionar la implementación válida. ¿Esto tiene sentido? –

+1

@Aaron McDaid Deberías (casi) nunca mezclar la sobrecarga con la especialización por razones que ahora se me escapan, pero tiene que ver con que el compilador no siempre encuentre la función que deseas. –

0

Creo que su ejemplo con int y long es un poco descabellado, ya que en C++ canónico nunca pasará tipos incorporados por referencia constante de todos modos: los pasa por valor o por referencia no const.

Supongamos, en cambio, que tiene una gran clase definida por el usuario. En este caso, si está creando temporarios para usted, significa que ha creado conversiones implícitas para esa clase. Todo lo que tiene que hacer es marcar todos los constructores de conversión (aquellos que se pueden llamar con un solo parámetro) como explicit y el compilador evitará que esos temporales se creen automáticamente. Por ejemplo:

class Foo 
{ 
    explicit Foo(int bar) { } 
}; 
2

Esto es bastante simple de resolver: deje de tomar valores por referencia. Si desea asegurarse de que un parámetro es direccionable , a continuación, hacer que sea una dirección :

void bar_const(const long *) { } 

De esta manera, el usuario debe pasar un puntero. Y no puede obtener un puntero a un temporal (a menos que el usuario esté siendo terriblemente malicioso).

Habiendo dicho eso, creo que su forma de pensar sobre este asunto es ... equivocada. Todo se reduce a este punto.

quizás tomaré su dirección, sin darme cuenta de que estoy, en efecto, tomando la dirección de un temporal.

Tomando la dirección de un const& que pasa a ser temporal está realmente bien. El problema es que no puede almacenar a largo plazo. Tampoco puedes transferir la propiedad de él. Después de todo, tiene una referencia const.

Y eso es parte del problema. Si toma un const&, su interfaz dice: "Puedo usar este objeto, pero no soy dueño de él ni puedo cederle la propiedad a otra persona". Como no es propietario del objeto, no puede almacenarlo a largo plazo en. Esto es lo que significa const&.

En su lugar, tomar un const* puede ser problemático. ¿Por qué? Porque no sabes de dónde vino ese puntero. ¿A quién pertenece este puntero?const& tiene una serie de medidas de seguridad sintáctica para evitar que haga cosas malas (siempre que no tome su dirección). const* no tiene nada; puedes copiar ese puntero al contenido de tu corazón. Su interfaz no dice nada sobre si se le permite poseer el objeto o transferir la propiedad a otros.

Esta ambigüedad es la razón por la cual C++ 11 tiene punteros inteligentes como unique_ptr y shared_ptr. Estos punteros pueden describir real relaciones de propiedad de memoria.

Si su función toma un unique_ptr por valor, entonces ahora posee ese objeto. Si toma shared_ptr, ahora usted comparte propiedad de ese objeto. Existen garantías sintácticas que garantizan la propiedad (nuevamente, a menos que tome medidas desagradables).

En el caso de que no utilice C++ 11, debe usar punteros inteligentes Boost para lograr efectos similares.

3

Si puede utilizar C++ 11 (o partes de los mismos), esto es fácil:

void f(BigObject const& bo){ 
    // ... 
} 

void f(BigObject&&) = delete; // or just undefined 

Live example on Ideone.

Esto funcionará, porque el enlace a una referencia rvalue es preferible a la vinculación a una referencia-a-const para un objeto temporal.

También puede explotar el hecho de que sólo una sola conversión definida por el usuario está permitido en una secuencia de conversión implícita:

struct BigObjWrapper{ 
    BigObjWrapper(BigObject const& o) 
    : object(o) {} 

    BigObject const& object; 
}; 

void f(BigObjWrapper wrap){ 
    BigObject const& bo = wrap.object; 
    // ... 
} 

Live example on Ideone.

Cuestiones relacionadas