¿Tienen alguna utilidad las referencias de rvalue a const?

Respuestas:

78

En ocasiones son útiles. El propio borrador de C ++ 0x los usa en algunos lugares, por ejemplo:

template <class T> void ref(const T&&) = delete;
template <class T> void cref(const T&&) = delete;

Las dos sobrecargas anteriores aseguran que las otras funciones ref(T&)y cref(const T&)no se unan a rvalues ​​(que de otra manera sería posible).

Actualizar

Acabo de verificar el estándar oficial N3290 , que desafortunadamente no está disponible públicamente, y tiene en 20.8 objetos de función [function.objects] / p2:

template <class T> void ref(const T&&) = delete;
template <class T> void cref(const T&&) = delete;

Luego verifiqué el borrador más reciente posterior a C ++ 11, que está disponible públicamente, N3485 , y en 20.8 Objetos de función [function.objects] / p2 todavía dice:

template <class T> void ref(const T&&) = delete;
template <class T> void cref(const T&&) = delete;
Howard Hinnant
fuente
Mirando cppreference , parece que este ya no es el caso. ¿Alguna idea de por qué? ¿Se const T&&utiliza algún otro lugar ?
Pubby
58
¿Por qué incluyó el mismo código en su respuesta tres veces? Traté de encontrar una diferencia durante demasiado tiempo.
typ1232
9
@ typ1232: Parece que actualicé la respuesta casi 2 años después de responder, debido a preocupaciones en los comentarios de que las funciones a las que se hace referencia ya no aparecían. Hice una copia / pegado de N3290 y del último borrador N3485 para mostrar que las funciones aún aparecían. Usar copiar / pegar, en mi opinión en ese momento, era la mejor manera de asegurarme de que más ojos que los míos pudieran confirmar que no estaba pasando por alto algún cambio menor en estas firmas.
Howard Hinnant
1
@kevinarpe: Las "otras sobrecargas" (no se muestran aquí, pero en el estándar) toman referencias de lvalue y no se eliminan. Las sobrecargas que se muestran aquí coinciden mejor con rvalues ​​que las sobrecargas que toman referencias de lvalue. Por lo tanto, los argumentos de rvalue se unen a las sobrecargas que se muestran aquí y luego causan un error de tiempo de compilación porque estas sobrecargas se eliminan.
Howard Hinnant
1
El uso de const T&&evita que alguien use tontamente los argumentos de plantilla explícitos del formulario ref<const A&>(...). Ese no es un argumento muy fuerte, pero el costo de const T&&más T&&es bastante mínimo.
Howard Hinnant
6

La semántica de obtener una referencia const rvalue (y no para =delete) es para decir:

  • ¡no apoyamos la operación para lvalues!
  • aunque todavía copiamos , porque no podemos mover el recurso pasado, o porque no hay un significado real para "moverlo".

El siguiente caso de uso podría haber sido en mi humilde opinión un buen caso de uso para la referencia de rvalue a const , aunque el lenguaje decidió no adoptar este enfoque (ver la publicación SO original ).


El caso: constructor de punteros inteligentes a partir de puntero en bruto

Por lo general, sería aconsejable usar make_uniquey make_shared, pero ambos unique_ptry shared_ptrse pueden construir a partir de un puntero sin formato. Ambos constructores obtienen el puntero por valor y lo copian. Ambos permiten (es decir, en el sentido de: no previenen ) un uso continuo del puntero original que se les pasó en el constructor.

El siguiente código se compila y da como resultado double free :

int* ptr = new int(9);
std::unique_ptr<int> p { ptr };
// we forgot that ptr is already being managed
delete ptr;

Ambos unique_ptry shared_ptrpodrían evitar lo anterior si sus constructores relevantes esperaran obtener el puntero sin formato como un valor constante , por ejemplo, para unique_ptr:

unique_ptr(T* const&& p) : ptr{p} {}

En cuyo caso, el código doble libre anterior no se compilaría, pero lo siguiente sí:

std::unique_ptr<int> p1 { std::move(ptr) }; // more verbose: user moves ownership
std::unique_ptr<int> p2 { new int(7) };     // ok, rvalue

Tenga en cuenta que ptraún se podría usar después de que se movió, por lo que el error potencial no desapareció por completo. Pero si el usuario debe llamar a std::moveun error de este tipo, caería en la regla común de: no use un recurso que se movió.


Uno puede preguntar: OK, pero ¿por qué T* const&& p ?

La razón es simple, para permitir la creación de un unique_ptr puntero constante . Recuerde que la referencia const rvalue es más genérica que solo la referencia rvalue, ya que acepta tanto consty non-const. Entonces podemos permitir lo siguiente:

int* const ptr = new int(9);
auto p = std::unique_ptr<int> { std::move(ptr) };

esto no funcionaría si esperáramos solo una referencia de rvalue (error de compilación: no se puede vincular const rvalue a rvalue ).


De todos modos, es demasiado tarde para proponer tal cosa. Pero esta idea presenta un uso razonable de una referencia rvalue a const .

Amir Kirsh
fuente
Estaba entre personas con ideas similares, pero no tenía el apoyo para seguir adelante.
Red.Wave
"auto p = std :: unique_ptr {std :: move (ptr)};" no se compila con el error "error en la deducción de argumento de plantilla de clase". Creo que debería ser "unique_ptr <int>".
Zehui Lin
4

Están permitidos e incluso las funciones se clasifican según const, pero como no puede moverse desde el objeto const referido por const Foo&&, no son útiles.

Gene Bushuyev
fuente
¿Qué quiere decir exactamente con el comentario "clasificado"? ¿Algo que ver con la resolución de sobrecarga, supongo?
fredoverflow
¿Por qué no puedes pasar de una const rvalue-ref, si el tipo dado tiene un movimiento ctor que toma una const rvalue-ref?
Fred Nurk
6
@FredOverflow, la clasificación de sobrecarga es la siguiente:const T&, T&, const T&&, T&&
Gene Bushuyev
2
@Fred: ¿Cómo te mueves sin modificar la fuente?
fredoverflow
3
@Fred: miembros de datos mutables, o quizás moverse para este tipo hipotético no requiere modificar miembros de datos.
Fred Nurk
2

Además de std :: ref , la biblioteca estándar también usa la referencia const rvalue en std :: as_const para el mismo propósito.

template <class T>
void as_const(const T&&) = delete;

También se usa como valor de retorno en std :: opcional cuando se obtiene el valor envuelto:

constexpr const T&& operator*() const&&;
constexpr const T&& value() const &&;

Así como en std :: get :

template <class T, class... Types>
constexpr const T&& get(const std::variant<Types...>&& v);
template< class T, class... Types >
constexpr const T&& get(const tuple<Types...>&& t) noexcept;

Esto es presumiblemente para mantener la categoría de valor así como la consistencia de la envoltura al acceder al valor envuelto.

Esto hace una diferencia si las funciones calificadas por ref const rvalue se pueden llamar en el objeto empaquetado. Dicho esto, no conozco ningún uso para las funciones calificadas const rvalue ref.

eerorika
fuente
1

No puedo pensar en una situación en la que esto sea útil directamente, pero podría usarse indirectamente:

template<class T>
void f(T const &x) {
  cout << "lvalue";
}
template<class T>
void f(T &&x) {
  cout << "rvalue";
}

template<class T>
void g(T &x) {
  f(T());
}

template<class T>
void h(T const &x) {
  g(x);
}

La T en g es T constante, entonces f 's x es una T constante &&.

Es probable que esto dé como resultado un error de comilla en f (cuando intenta mover o usar el objeto), pero f podría tomar un rvalue-ref para que no se pueda llamar en lvalues, sin modificar el rvalue (como en el demasiado simple ejemplo anterior).

Fred Nurk
fuente