Что такое "rvalue reference для * this"?

Перешел в предложение под названием "rvalue reference for * this" на странице состояния С++ 11.

Я немного прочитал ссылки на rvalue и понял их, но я не думаю, что знаю об этом. Я также не мог найти много ресурсов в Интернете, используя термины.

Там есть ссылка на рекламный документ на странице: N2439 (расширение семантики перемещения до * этого), но я также не получаю много примеров из есть.

Что это за функция?

Ответ 1

Во-первых, "ref-qualifiers for * this" является просто "маркетинговым выражением". Тип *this никогда не изменяется, см. Нижнюю часть этого сообщения. Легче понять это с помощью этой формулировки.

Далее, следующий код выбирает функцию, которая должна быть вызвана на основе ref-qualifier "неявного параметра объекта" функции :

// 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
}

Вывод:

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

Все это сделано для того, чтобы вы могли воспользоваться тем фактом, когда объект, вызываемый функцией, является rvalue (например, неназванным временным). В качестве дополнительного примера возьмем следующий код:

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);
  }
};

Это может быть немного надуманным, но вы должны получить эту идею.

Обратите внимание, что вы можете комбинировать cv-квалификаторы (const и volatile) и ref-qualifiers (& и &&).


Примечание. Многие стандартные цитаты и описание разрешения перегрузки после этого!

† Чтобы понять, как это работает, и почему ответ @Nicol Bolas, по крайней мере, частично ошибочен, нам нужно немного выкопать в стандарте С++ (часть, объясняющая, почему ответ @Nicol неверен, внизу, если вас это интересует только).

Какую функцию вызывается, определяется процессом, называемым разрешением перегрузки. Этот процесс довольно сложный, поэтому мы будем касаться только того, что важно для нас.

Во-первых, важно посмотреть, как работает разрешение перегрузки для функций-членов:

§13.3.1 [over.match.funcs]

p2 Набор функций-кандидатов может содержать как функции-члены, так и не-члены, которые должны быть разрешены в отношении одного и того же списка аргументов. Таким образом, список аргументов и параметров сопоставим в этом гетерогенном наборе, считается, что функция-член имеет дополнительный параметр, называемый неявным параметром объекта, который представляет объект, для которого функция-член была вызвана. [...]

p3 Аналогично, в случае необходимости контекст может построить список аргументов, который содержит подразумеваемый аргумент объекта для обозначения объекта, который будет использоваться.

Почему нам даже нужно сравнивать функции-члены и не-члены? Поэтому перегрузка оператора. Рассмотрим это:

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

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

Вы, конечно, хотите, чтобы следующее вызывало свободную функцию, не так ли?

char const* s = "free foo!\n";
foo f;
f << s;

Вот почему функции-члены и не-члены включены в так называемый набор перегрузки. Чтобы сделать разрешение менее сложным, существует полужирная часть стандартной цитаты. Кроме того, это важный бит для нас (то же предложение):

p4 Для нестатических функций-членов тип параметра неявного объекта

  • "lvalue reference to cv X" для функций, объявленных без ref-qualifier или с & ref-qualifier

  • "rvalue reference to cv X" для функций, объявленных с && ref-qualifier

где X - класс, членом которого является член, а cv - cv-квалификация в объявлении функции-члена. [...]

p5 Во время разрешения перегрузки [...] [t] он неявный параметр объекта [...] сохраняет свою идентичность, поскольку преобразования в соответствующем аргументе должны подчиняться этим дополнительным правилам:

  • не может быть введен временный объект для хранения аргумента для параметра неявного объекта; и

  • никакие пользовательские преобразования не могут применяться для достижения соответствия типа с ним

[...]

(Последний бит означает, что вы не можете обмануть разрешение перегрузки на основе неявных преобразований объекта, на который вызывается функция-член (или оператор).)

Возьмем первый пример в верхней части этого сообщения. После вышеупомянутого преобразования набор перегрузки выглядит примерно так:

void f1(test&); // will only match lvalues, linked to 'void test::f() &'
void f2(test&&); // will only match rvalues, linked to 'void test::f() &&'

Затем список аргументов, содержащий подразумеваемый аргумент объекта, сопоставляется с списком параметров каждой функции, содержащейся в наборе перегрузки. В нашем случае список аргументов будет содержать только этот аргумент объекта. Посмотрим, как это выглядит:

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

Если после проверки всех перегрузок в наборе остается только один, разрешение перегрузки сработало и вызывается функция, связанная с этой преобразованной перегрузкой. То же самое касается второго вызова "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

Обратите внимание, однако, что если бы мы не предоставили какой-либо ref-квалификатор (и как таковой не перегружали функцию), то f1 будет соответствовать rvalue (еще §13.3.1):

p5 [...] Для нестатических функций-членов, объявленных без ref-квалификатора, применяется дополнительное правило:

  • даже если неявный параметр объекта не const -qualified, rvalue может быть привязан к параметру, если во всех других отношениях аргумент может быть преобразован в тип неявного параметра объекта.
struct test{
  void f() { std::cout << "lvalue or rvalue object\n"; }
};

int main(){
  test t;
  t.f(); // OK
  test().f(); // OK too
}

Теперь о том, почему ответ @Nicol по крайней мере частично ошибочен. Он говорит:

Обратите внимание, что это объявление изменяет тип *this.

Это неверно, *this всегда имеет значение lvalue:

§5.3.1 [expr.unary.op] p1

Унарный оператор * выполняет косвенное обращение: выражение, к которому оно применяется, должно быть указателем на тип объекта или указателем на тип функции , а результатом является значение lvalue, ссылающееся на объект или функция, на которые указывает выражение.

§9.3.2 [class.this] p1

В теле нестатической (9.3) функции-члена ключевое слово this является выражением prvalue, значением которого является адрес объекта, для которого вызывается функция. Тип this в функции-члене класса X равен X*. [...]

Ответ 2

Существует дополнительный прецедент для формы ref-квалификатора lvalue. С++ 98 имеет язык, позволяющий вызывать функции-члены не const для экземпляров класса, которые являются значениями r. Это приводит ко всем видам странности, которые противоречат самой концепции rvalueness и отклоняются от того, как работают встроенные типы:

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

ref-квалификаторы Lvalue решают эти проблемы:

struct S {
  S& operator ++() &;
  S* operator &() &;
  const S& operator =(const S&) &;
};

Теперь операторы работают так же, как и у встроенных типов, принимая только lvalues.

Ответ 3

Скажем, у вас есть две функции в классе, имеющие одинаковое имя и подпись. Но один из них объявлен const:

void SomeFunc() const;
void SomeFunc();

Если экземпляр класса не является const, разрешение перегрузки будет предпочтительно выбирать неконстантную версию. Если экземпляр const, пользователь может вызывать только версию const. Указатель this - это указатель const, поэтому экземпляр не может быть изменен.

То, что "r-value reference для этого" делает, позволяет вам добавить другую альтернативу:

void RValueFunc() &&;

Это позволяет вам иметь функцию, которую можно вызвать только в том случае, если пользователь вызывает ее через правильное значение r. Итак, если это в типе Object:

Object foo;
foo.RValueFunc(); //error: no `RValueFunc` version exists that takes `this` as l-value.
Object().RValueFunc(); //calls the non-const, && version.

Таким образом, вы можете специализировать поведение, основанное на доступе к объекту через r-значение или нет.

Обратите внимание, что вам не разрешается перегружать между ссылочными версиями r-значения и версиями без ссылки. То есть, если у вас есть имя функции-члена, все его версии либо используют квалификаторы l/r-value на this, либо ни один из них не выполняет. Вы не можете этого сделать:

void SomeFunc();
void SomeFunc() &&;

Вы должны сделать это:

void SomeFunc() &;
void SomeFunc() &&;

Обратите внимание, что это объявление изменяет тип *this. Это означает, что && поддерживает все элементы доступа в качестве ссылок на r-значение. Таким образом, становится возможным легко перемещаться из объекта. Пример, приведенный в первой версии предложения (примечание: следующее может быть неверным с окончательной версией С++ 11, оно прямо из исходного "r-значения из этого" предложения):

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