Encontré una propuesta llamada “referencia de rvalue para *esto” en clang’s Página de estado de C++11.
He leído bastante sobre las referencias de rvalue y las entendí, pero no creo que sepa sobre esto. Tampoco pude encontrar muchos recursos en la web usando los términos.
Hay un enlace al documento de propuesta en la página: N2439 (Extendiendo la semántica de movimiento a *esto), pero tampoco obtengo muchos ejemplos de allí.
¿De qué se trata esta característica?
Primero, “calificadores de referencia para *esto” es solo una “declaración de marketing”. El tipo de *this
nunca cambia, vea la parte inferior de esta publicación. Sin embargo, es mucho más fácil de entender con esta redacción.
A continuación, el siguiente código elige la función que se va a llamar en función de la calificador de referencia del “parámetro de objeto implícito” de la función†:
// t.cpp
#include <iostream>
struct test{
void f() &{ std::cout << "lvalue object\n"; }
void f() &&{ std::cout << "rvalue object\n"; }
};
int main(){
test t;
t.f(); // lvalue
test().f(); // rvalue
}
Producción:
$ clang++ -std=c++0x -stdlib=libc++ -Wall -pedantic t.cpp
$ ./a.out
lvalue object
rvalue object
Todo se hace para permitirle aprovechar el hecho de que el objeto al que se llama la función es un valor r (temporal sin nombre, por ejemplo). Tome el siguiente código como un ejemplo más:
struct test2{
std::unique_ptr<int[]> heavy_resource;
test2()
: heavy_resource(new int[500]) {}
operator std::unique_ptr<int[]>() const&{
// lvalue object, deep copy
std::unique_ptr<int[]> p(new int[500]);
for(int i=0; i < 500; ++i)
p[i] = heavy_resource[i];
return p;
}
operator std::unique_ptr<int[]>() &&{
// rvalue object
// we are garbage anyways, just move resource
return std::move(heavy_resource);
}
};
Esto puede ser un poco artificial, pero deberías hacerte una idea.
Tenga en cuenta que puede combinar los cv-calificadores (const
y volatile
) y calificadores de referencia (&
y &&
).
Nota: ¡Muchas comillas estándar y explicaciones de resolución de sobrecarga después de aquí!
† Para entender cómo funciona esto y por qué la respuesta de @Nicol Bolas es al menos parcialmente incorrecta, tenemos que profundizar un poco en el estándar C++ (la parte que explica por qué la respuesta de @Nicol es incorrecta está en la parte inferior, si estás solo me interesa eso).
La función que se va a llamar está determinada por un proceso llamado resolución de sobrecarga. Este proceso es bastante complicado, por lo que solo tocaremos la parte que es importante para nosotros.
Primero, es importante ver cómo funciona la resolución de sobrecarga para las funciones miembro:
§13.3.1 [over.match.funcs]
p2 El conjunto de funciones candidatas puede contener tanto funciones miembro como no miembro para ser resueltas contra la misma lista de argumentos. Para que las listas de argumentos y parámetros sean comparables dentro de este conjunto heterogéneo, se considera que una función miembro tiene un parámetro adicional, denominado parámetro de objeto implícito, que representa el objeto para el que se ha llamado a la función miembro. […]
p3 De manera similar, cuando sea apropiado, el contexto puede construir una lista de argumentos que contenga un argumento de objeto implícito para denotar el objeto que se va a operar.
¿Por qué necesitamos comparar funciones miembro y no miembro? Sobrecarga de operadores, por eso. Considera esto:
struct foo{
foo& operator<<(void*); // implementation unimportant
};
foo& operator<<(foo&, char const*); // implementation unimportant
Seguramente querrías lo siguiente para llamar a la función gratuita, ¿no es así?
char const* s = "free foo!\n";
foo f;
f << s;
Es por eso que las funciones miembro y no miembro se incluyen en el llamado conjunto de sobrecarga. Para que la resolución sea menos complicada, existe la parte en negrita de la cita estándar. Además, esta es la parte importante para nosotros (misma cláusula):
p4 Para funciones miembro no estáticas, el tipo del parámetro de objeto implícito es
-
“lvalue referencia a CV X
” para funciones declaradas sin calificador de referencia o con el &
calificador de referencia
-
“rvalue referencia a CV X
” para funciones declaradas con el &&
calificador de referencia
donde X
es la clase de la que la función es miembro y CV es la calificación cv en la declaración de la función miembro. […]
p5 Durante la resolución de sobrecarga […]
-
no se puede introducir ningún objeto temporal para contener el argumento del parámetro de objeto implícito; y
-
no se pueden aplicar conversiones definidas por el usuario para lograr una coincidencia de tipo con él
[…]
(El último bit solo significa que no puede engañar a la resolución de sobrecarga basada en conversiones implícitas del objeto al que se llama una función miembro (u operador).)
Tomemos el primer ejemplo en la parte superior de esta publicación. Después de la transformación antes mencionada, el conjunto de sobrecarga se parece a esto:
void f1(test&); // will only match lvalues, linked to 'void test::f() &'
void f2(test&&); // will only match rvalues, linked to 'void test::f() &&'
Entonces la lista de argumentos, que contiene un argumento de objeto implícito, se compara con la lista de parámetros de cada función contenida en el conjunto de sobrecarga. En nuestro caso, la lista de argumentos solo contendrá ese argumento de objeto. Veamos cómo se ve eso:
// first call to 'f' in 'main'
test t;
f1
// kept in overload-set
f2
// taken out of overload-set
Si, después de probar todas las sobrecargas en el conjunto, solo queda una, la resolución de la sobrecarga tuvo éxito y se llama a la función vinculada a esa sobrecarga transformada. Lo mismo ocurre con la segunda llamada a ‘f’:
// second call to 'f' in 'main'
f1(test()); // 'test()' not an lvalue, can't match 'test&' (lvalue reference)
// taken out of overload-set
f2(test()); // 'test()' (rvalue) can match 'test&&' (rvalue reference)
// kept in overload-set
Sin embargo, tenga en cuenta que, si no hubiéramos proporcionado ninguna calificador de referencia (y como tal no sobrecargado la función), que f1
haría coincidir con un rvalue (todavía §13.3.1
):
p5 […] Para funciones miembro no estáticas declaradas sin calificador de referenciase aplica una regla adicional:
- incluso si el parámetro de objeto implícito no es
const
-calificado, un valor r se puede vincular al parámetro siempre que, en todos los demás aspectos, el argumento se pueda convertir al tipo del parámetro de objeto implícito.
struct test{
void f() { std::cout << "lvalue or rvalue object\n"; }
};
int main(){
test t;
t.f(); // OK
test().f(); // OK too
}
Ahora, sobre por qué la respuesta de @Nicol es al menos parcialmente incorrecta. Él dice:
Tenga en cuenta que esta declaración cambia el tipo de *this
.
Eso está mal, *this
es siempre un valor l:
§5.3.1 [expr.unary.op] p1
el unario *
el operador realiza indirección: la expresión a la que se aplica será un puntero a un tipo de objeto, o un puntero a un tipo de función y el resultado es un lvalue refiriéndose al objeto o función a la que apunta la expresión.
§9.3.2 [class.this] p1
En el cuerpo de una función miembro no estática (9.3), la palabra clave this
es una expresión prvalue cuyo valor es la dirección del objeto para el que se llama a la función. El tipo de this
en una función miembro de una clase X
es X*
. […]
Hay un caso de uso adicional para el formulario lvalue ref-qualifier. C++98 tiene un lenguaje que permite que noconst
funciones miembro que se llamarán para instancias de clase que son rvalues. Esto conduce a todo tipo de rarezas que van en contra del concepto mismo de rvalueness y se desvía de cómo funcionan los tipos integrados:
struct S {
S& operator ++();
S* operator &();
};
S() = S(); // rvalue as a left-hand-side of assignment!
S& foo = ++S(); // oops, dangling reference
&S(); // taking address of rvalue...
Los calificadores de referencia Lvalue resuelven estos problemas:
struct S {
S& operator ++() &;
S* operator &() &;
const S& operator =(const S&) &;
};
Ahora los operadores funcionan como los de los tipos incorporados, aceptando solo lvalues.
Digamos que tiene dos funciones en una clase, ambas con el mismo nombre y firma. Pero uno de ellos se declara const
:
void SomeFunc() const;
void SomeFunc();
Si una instancia de clase no es const
, la resolución de sobrecarga seleccionará preferentemente la versión no constante. Si la instancia es const
el usuario solo puede llamar al const
versión. Y el this
puntero es un const
puntero, por lo que la instancia no se puede cambiar.
Lo que hace la “referencia de valor r para esto” es permitirle agregar otra alternativa:
void RValueFunc() &&;
Esto le permite tener una función que puede solamente ser llamado si el usuario lo llama a través de un valor r adecuado. Así que si esto está en el tipo Object
:
Object foo;
foo.RValueFunc(); //error: no `RValueFunc` version exists that takes `this` as l-value.
Object().RValueFunc(); //calls the non-const, && version.
De esta manera, puede especializar el comportamiento en función de si se accede al objeto a través de un valor r o no.
Tenga en cuenta que no puede sobrecargar entre las versiones de referencia de valor r y las versiones que no son de referencia. Es decir, si tiene un nombre de función miembro, todas sus versiones usan los calificadores de valor l/r en this
, o ninguno de ellos lo hace. No puedes hacer esto:
void SomeFunc();
void SomeFunc() &&;
Tienes que hacer esto:
void SomeFunc() &;
void SomeFunc() &&;
Tenga en cuenta que esta declaración cambia el tipo de *this
. Esto significa que el &&
todas las versiones acceden a los miembros como referencias de valor r. Por lo tanto, es posible moverse fácilmente desde dentro del objeto. El ejemplo dado en la primera versión de la propuesta es (nota: lo siguiente puede no ser correcto con la versión final de C++ 11; es directamente de la propuesta inicial “valor r de esta”):
class X {
std::vector<char> data_;
public:
// ...
std::vector<char> const & data() const & { return data_; }
std::vector<char> && data() && { return data_; }
};
X f();
// ...
X x;
std::vector<char> a = x.data(); // copy
std::vector<char> b = f().data(); // move