Semántica para objetos envueltos: referencia / valor por defecto a través de std :: move / std :: ref
En los últimos tiempos, a menudo uso un lenguaje natural que "descubrí" en C ++ 11, es decir, que el objeto envuelto puede contener referencias automáticamente cuando esto es posible. La pregunta principal aquí será acerca de la comparación con el comportamiento de este "lenguaje" a otros comportamientos en la norma (ver más abajo).
Por ejemplo:
template<class T>
struct wrap{
T t;
};
template<class T> wrap<T> make_wrap(T&& t){
return wrap{std::forward<T>(t)};
}
De esta manera para el código.
double a = 3.14
double const c = 3.14
Yo obtengo,
typeid( make_wrap(3.14) ) --> wrap<double>
typeid( make_wrap(a) ) --> wrap<double&>
typeid( make_wrap(c) ) --> wrap<double const&>
que si tengo cuidado (con referencias pendientes) puedo manejar bastante bien. Y si quieroevitar referencias que hago:
typeid( make_wrap(std::move(a)) ) --> wrap<double> // noref
typeid( make_wrap(std::move(c)) ) --> wrap<double const> // noref
Entonces, este comportamiento parece natural en C ++ 11.
Luego volví astd::pair
ystd::make_pair
y de alguna manera esperaba que usaran este nuevo comportamiento aparentemente natural, pero aparentemente el comportamiento es "más tradicional". Así por ejemplo:
typeid( std::make_pair(3.14, 3.14) ) --> std::pair<double, double>
typeid( std::make_pair(a, a) ) --> std::pair<double, double> // noref
typeid( std::make_pair(c, c) ) --> std::pair<double, double> // noref
ypara referencias:
typeid( std::make_pair(std::ref(a), std::ref(a) ) ) --> std::pair<double&, double&> // ref
typeid( std::make_pair(std::ref(c), std::ref(c) ) ) --> std::pair<double const&, double const&> // const ref
Esto se documenta aquí:http://en.cppreference.com/w/cpp/utility/pair/make_pair
Como ves, los dos comportamientos son "opuestos", en cierto sentidostd::ref
es el complemento astd::move
. Así que ambos comportamientos son igualmente flexibles al final, pero me parece que elstd::make_pair
El comportamiento es más difícil de implementar y mantener.
La pregunta es:Es el comportamiento actual destd::make_pair
de descartar referencias por defecto solo un problema de compatibilidad hacia atrás? ¿Porque alguna expectativa histórica? ¿O hay una razón más profunda que todavía existe en C ++ 11?
Como es, se ve asístd::make_pair
El comportamiento es mucho más difícil de implementar, ya que requiere especialización parastd::ref
(std::reference_wrapper
) ystd::decay
e incluso parece antinatural (en presencia de "movimiento C ++ 11"). Al mismo tiempo, incluso si decido seguir utilizando el primer comportamiento, me temo que el comportamiento será bastante inesperado con respecto a los estándares actuales, incluso en C ++ 11.
De hecho, me gusta mucho el primer comportamiento, hasta el punto de que la solución elegante tal vez cambie el prefijomake_something
para algo comoconstruct_something
con el fin de marcar la diferencia en el comportamiento. (EDITAR: uno de los comentarios sugeridos para mirarstd::forward_as_tuple
, entonces otra convención de nombres podría serforward_as_something
). En cuanto a la denominación, la situación no está clara cuando se mezcla paso a valor, paso a paso en la construcción del objeto.
EDIT2: Esta es una edición solo para responder a un @ Yakk sobre la posibilidad de "copiar" el objeto de envoltura con diferentes propiedades de referencia / valor. Esto no es parte de la pregunta y es solo un código experimental:
template<class T>
struct wrap{
T t;
// "generalized constructor"? // I could be overlooking something important here
template<class T1 = T> wrap(wrap<T1> const& w) : t(std::move(w.t)){}
wrap(T&& t_) : t(std::move(t)){} // unfortunately I now have to define an explicit constructor
};
Esto parece permitirme copiar entre tipos no relacionadoswrap<T&>
ywrap<T>
:
auto mw = make_wrap(a);
wrap<double const&> copy0 =mw;
wrap<double&> copy1 = mw; //would be an error if `a` is `const double`, ok
wrap<double> copy2 = mw;
EDIT3: Esta edición es para agregar un ejemplo concreto en el que la deducción de referencia tradicional puede fallar dependiendo de un "protocolo". El ejemplo se basa en el uso de Boost.Fusion.
Descubrí cuánto puede depender de la convención la conversión implícita de referencia a valor. Por ejemplo, el viejo y bueno Boost.Fusion sigue la convención de STL de
Las funciones de generación de Fusion (por ejemplo, make_list) almacenan de forma predeterminada los tipos de elementos como tipos simples sin referencia.
Sin embargo, eso se basa en el "tipo" exacto que marca la referencia, en el caso de Fusion fue elboost::ref
y en el caso demake_pair
es...std::ref
, una clase completamente no relacionada. Así que, actualmente, dado
double a;
el tipo deboost::fusion::make_vector(5., a )
esboost::fusion::vector2<double, double>
. Está bien.
Y el tipo deboost::fusion::make_vector(5., boost::ref(a) ) ) is
boost :: fusion :: vector2`. Ok, como se documenta.
Sin embargo, sorpresa, ya que Boost.Fusion no se escribió con C ++ 11 STL en mente, obtenemos:boost::fusion::make_vector(5., std::ref(a) ) )
es de tipoboost::fusion::vector2<double, std::reference_wrapper<double const> >
. ¡Sorpresa!
Esta sección fue para mostrar que el comportamiento STL actual depende de un protocolo (por ejemplo, qué clase usar para etiquetar referencias), mientras que el otro (lo que llamé comportamiento "natural") usastd::move
(o, más exactamente, la conversión de valores) no depende de un protocolo, pero es más nativo al idioma (actual).