¿Qué es “referencia de valor para * esto”?

12 minutos de lectura

¿Que es referencia de valor para esto
Ryaner

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?

1647546912 518 ¿Que es referencia de valor para esto
Xeo

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*. […]

  • Creo que los tipos de parámetros justo después de la sección “después de la transformación” deberían ser ‘foo’ en lugar de ‘prueba’.

    – Ryaner

    25 de diciembre de 2011 a las 5:04

  • @ryaner: Buen hallazgo, gracias. Aunque no el parámetro sino el identificador de clase de las funciones es incorrecto. 🙂

    – Xeo

    25 de diciembre de 2011 a las 8:50


  • Vaya, lo siento, me olvidé de la clase de juguete llamada prueba cuando leí esa parte y pensé que f está contenida dentro de foo, por lo tanto, mi comentario …

    – Ryaner

    25 de diciembre de 2011 a las 18:05

  • ¿Se puede hacer esto con constructores? MyType(int a, double b) &&?

    – Germán Diago

    2 de diciembre de 2014 a las 4:52

  • “El tipo de *esto nunca cambia” Tal vez debería ser un poco más claro que no cambia en función de la calificación del valor r/l. pero puede cambiar entre const/non-const.

    – xaxxon

    13/11/2016 a las 11:30

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.

1647546912 126 ¿Que es referencia de valor para esto
Nicolás Bolas

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 constel 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

  • Creo que necesitas std::move la segunda versión, no? Además, ¿por qué regresa la referencia rvalue?

    – Xeo

    22 de diciembre de 2011 a las 23:17

  • @Xeo: Porque ese era el ejemplo en la propuesta; No tengo idea si todavía funciona con la versión actual. Y el motivo del retorno de referencia del valor r es que el movimiento debe depender de la persona que lo captura. No debería suceder todavía, en caso de que realmente quiera almacenarlo en un && en lugar de un valor.

    – Nicolás Bolas

    22 de diciembre de 2011 a las 23:20

  • Correcto, pensé un poco en el motivo de mi segunda pregunta. Sin embargo, me pregunto, ¿una referencia de valor al miembro de un temporal prolonga la vida útil de ese temporal o del miembro del mismo? Podría jurar que vi una pregunta sobre eso en SO hace algún tiempo…

    – Xeo

    22 de diciembre de 2011 a las 23:22

  • @Xeo: No es del todo cierto. La resolución de sobrecarga siempre elegirá la versión no constante, si existe. Tendrías que hacer un lanzamiento para obtener la versión const. He actualizado la publicación para aclarar.

    – Nicolás Bolas

    23 de diciembre de 2011 a las 18:04

  • Pensé que podría explicarlo, después de todo, creé esta función 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 ref-qualifier cambia el tipo de parámetro de función implícito (u “oculto”) al que se vincula el objeto “este” (¡comillas puestas a propósito aquí!) durante la resolución de sobrecarga y la llamada de función. Entonces, no hay cambio de *this ya que esto se soluciona como explica Xeo. En su lugar, cambie el parámetro “oculto” para que sea una referencia de valor l o valor r, al igual que const el calificador de función lo hace const etc..

    – bronekk

    1 de febrero de 2012 a las 22:28


¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad