¿Qué es la "referencia de valor para * esto"?

238

Encontré una propuesta llamada "referencia de valor para * esto" en la página de estado de C ++ 11 de clang .

He leído bastante sobre las referencias de valor y las he entendido, pero no creo saberlo. Tampoco pude encontrar muchos recursos en la web usando los términos.

Hay un enlace al documento de propuesta en la página: N2439 (Ampliando la semántica de movimiento a * esto), pero tampoco obtengo muchos ejemplos a partir de ahí.

¿De qué se trata esta característica?

ryaner
fuente

Respuestas:

293

Primero, los "calificadores de referencia para * esto" es solo una "declaración de marketing". El tipo de *thisnunca cambia, mira al final de esta publicación. Sin embargo, es mucho más fácil entenderlo con esta redacción.

A continuación, el siguiente código elige la función que se llamará en función del 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
}

Salida:

$ clang++ -std=c++0x -stdlib=libc++ -Wall -pedantic t.cpp
$ ./a.out
lvalue object
rvalue object

Todo se hace para permitirle aprovechar el hecho cuando el objeto al que se llama la función es un rvalue (sin nombre temporal, por ejemplo). Tome el siguiente código como otro ejemplo:

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 debes entender la idea.

Tenga en cuenta que puede combinar los calificadores cv ( consty volatile) y los calificadores ref ( &y &&).


Nota: ¡Muchas citas estándar y explicación 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 interesado en eso).

La función que se llamará está determinada por un proceso llamado resolución de sobrecarga . Este proceso es bastante complicado, por lo que solo tocaremos el bit 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 funciones miembro y no miembro para resolver con 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, llamado parámetro de objeto implícito, que representa el objeto para el que se ha llamado 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 sobre el que se va a operar.

¿Por qué necesitamos incluso comparar funciones miembro y no miembro? Sobrecarga del operador, por eso. Considera esto:

struct foo{
  foo& operator<<(void*); // implementation unimportant
};

foo& operator<<(foo&, char const*); // implementation unimportant

Sin duda querrás que lo siguiente llame a la función gratuita, ¿no?

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, este es el bit importante para nosotros (misma cláusula):

p4 Para funciones miembro no estáticas, el tipo del parámetro de objeto implícito es

  • "Referencia de valor de cv X " para funciones declaradas sin un calificador de referencia o con el & calificador de referencia

  • "Referencia de valor de cv X " para funciones declaradas con el && calificador de referencia

donde Xes la clase de la cual 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 [...] [t] el parámetro objeto [...] implícito conserva su identidad ya que las conversiones en el argumento correspondiente obedecerán estas reglas adicionales:

  • no se puede introducir ningún objeto temporal para contener el argumento para el parámetro del objeto implícito; y

  • no se pueden aplicar conversiones definidas por el usuario para lograr una coincidencia de tipos con él

[...]

(El último bit solo significa que no puede hacer trampa en 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 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() &&'

Luego, 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(t); // 't' (lvalue) can match 'test&' (lvalue reference)
       // kept in overload-set
f2(t); // 't' not an rvalue, can't match 'test&&' (rvalue reference)
       // taken out of overload-set

Si, después de probar todas las sobrecargas del conjunto, solo queda una, la resolución de sobrecarga se realizó correctamente 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

Nota sin embargo, que, si no nos proporciona ninguna ref-calificador (y, como tal, no sobrecargado la función), que f1 podría coincidir con un valor p (todavía §13.3.1):

p5 [...] Para funciones miembro no estáticas declaradas sin un calificador de referencia , se aplica una regla adicional:

  • incluso si el parámetro del objeto implícito no está constcalificado, se puede vincular un valor r al parámetro siempre que en todos los demás aspectos el argumento se pueda convertir al tipo del parámetro del 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 en parte incorrecta. Él dice:

Tenga en cuenta que esta declaración cambia el tipo de *this.

Eso está mal, siempre*this es un valor:

§5.3.1 [expr.unary.op] p1

El *operador unario 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 valor l que se refiere al objeto o función al 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 thises una expresión de valor cuyo valor es la dirección del objeto para el que se llama la función. El tipo de thisen una función miembro de una clase Xes X*. [...]

Xeo
fuente
Creo que los tipos de paraneter justo después de la sección "después de la transformación" deberían ser 'foo' en lugar de 'test'.
ryaner
@ryaner: Buen hallazgo, gracias. Aunque no es el parámetro, pero el identificador de clase de las funciones es incorrecto. :)
Xeo
vaya lo siento me olvidé de la prueba denominada clase juguete cuando leí esa parte y pensé f está contenida dentro de foo por lo tanto mi comentario ..
ryaner
¿Se puede hacer esto con constructores MyType(int a, double b) &&:?
Germán Diago
2
"El tipo de * esto nunca cambia" Tal vez debería ser un poco más claro que no cambia según la calificación del valor r / l. pero puede cambiar entre const / non-const.
xaxxon el
78

Existe un caso de uso adicional para el formulario de calificador de referencia de lvalue. C ++ 98 tiene un lenguaje que permite invocar constfunciones que no son miembros para instancias de clase que son valores. Esto lleva a todo tipo de rarezas que va en contra del concepto mismo de valor y se desvía de cómo funcionan los tipos incorporados:

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 de 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 valores.

JohannesD
fuente
28

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 lo es const, la resolución de sobrecarga seleccionará preferentemente la versión no constante. Si la instancia es const, el usuario solo puede llamar a la constversión. Y el thispuntero es un constpuntero, por lo que la instancia no se puede cambiar.

Lo que hace "referencia de valor r para esto" es permitirle agregar otra alternativa:

void RValueFunc() &&;

Esto le permite tener una función que solo se puede llamar si el usuario la llama a través de un valor r adecuado. Entonces, 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 está permitido sobrecargar entre las versiones de referencia de valor r y las versiones sin referencia. Es decir, si tiene un nombre de función miembro, todas sus versiones usan los calificadores de valor l / r this, o ninguno 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 las &&versiones de todos los miembros de acceso 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 desde 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
Nicol Bolas
fuente
2
Creo que necesitas std::movela segunda versión, ¿no? Además, ¿por qué devuelve la referencia rvalue?
Xeo
1
@Xeo: Porque ese es el ejemplo en la propuesta; No tengo idea si aún funciona con la versión actual. Y la razón del retorno de referencia del valor r es porque el movimiento debe ser de la persona que lo captura. Todavía no debería suceder, en caso de que realmente quiera almacenarlo en un && en lugar de un valor.
Nicol Bolas
3
Bien, pensé un poco en la razón de mi segunda pregunta. Sin embargo, me pregunto si una referencia de valor al miembro de un temporal prolonga la vida útil de ese temporal o al miembro del mismo. Podría jurar que vi una pregunta sobre eso en SO hace algún tiempo ...
Xeo
1
@Xeo: No es del todo cierto. La resolución de sobrecarga siempre elegirá la versión no constante si existe. Tendría que hacer un reparto para obtener la versión constante. He actualizado la publicación para aclarar.
Nicol Bolas
15
Pensé que podría explicarlo, después de todo, creé esta característica para C ++ 11;) Xeo tiene razón al insistir en que no cambia el tipo de *this, sin embargo, puedo entender de dónde viene la confusión. Esto se debe a que el calificador de referencia cambia el tipo de parámetro de función implícito (u "oculto") al que se vincula el objeto "este" (¡cita aquí a propósito!) Durante la resolución de sobrecarga y la llamada a la función. Por lo tanto, no hay cambios *thisya que esto se corrige como explica Xeo. En cambio, cambie el parámetro "hiddden" para que sea lvalue- o rvalue-reference, tal como lo consthace el calificador de función, constetc.
bronekk