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

238

На странице статуса C ++ 11 в Clang наткнулся на предложение под названием «ссылка на rvalue для * this» .

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

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

О чем эта особенность?

ryaner
источник

Ответы:

293

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

Затем следующий код выбирает функцию для вызова на основе ref-квалификатора «параметра неявного объекта» функции :

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

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

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-квалификаторы ( &и &&).


Примечание: многие стандартные кавычки и объяснение разрешения перегрузки приведены здесь!

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

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

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

§13.3.1 [over.match.funcs]

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

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

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

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

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

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

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

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

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

  • «Lvalue ссылка на cv X » для функций, объявленных без квалификатора ref или с & квалификатором ref

  • «Rvalue ссылка на 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квалифицированным, значение r может быть привязано к параметру, если во всех других отношениях аргумент может быть преобразован в тип параметра неявного объекта.
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это всегда именующий:

§5.3.1 [expr.unary.op] p1

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

§9.3.2 [class.this] p1

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

Xeo
источник
Я считаю, что типы паранетров сразу после раздела «после преобразования» должны быть «foo» вместо «test».
Райанер
@ryaner: Хорошая находка, спасибо. Хотя не параметр, а идентификатор класса функций неверен. :)
Xeo
ой, извините, я забыл о классе игрушек с именем test, когда прочитал эту часть и подумал, что f содержится в foo, поэтому мой комментарий ..
ryaner
Можно ли это сделать с конструкторами: MyType(int a, double b) &&?
Герман Диаго
2
«Тип * это никогда не меняется». Возможно, вам следует немного прояснить, что это не меняется в зависимости от квалификации r / l-value. но это может измениться между const / non-const.
xaxxon
78

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

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

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

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

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

JohannesD
источник
28

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

void SomeFunc() const;
void SomeFunc();

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

«Ссылка на r-значение для этого» позволяет вам добавить еще одну альтернативу:

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-значение. Таким образом, становится возможным легко перемещаться изнутри объекта. Пример, приведенный в первой версии предложения: (примечание: следующее может быть неверным с окончательной версией C ++ 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
Николь Болас
источник
2
Я думаю, вам нужна std::moveвторая версия, не так ли? Кроме того, зачем возвращать ссылку на rvalue?
Xeo
1
@Xeo: Потому что это то, что был пример в предложении; Я понятия не имею, если это все еще работает с текущей версией. И причина возврата эталонного значения r заключается в том, что движение должно зависеть от того, кто его захватит. Это еще не должно произойти, на тот случай, если он действительно хочет сохранить его в && вместо значения.
Никол Болас
3
Хорошо, я вроде подумал о причине моего второго вопроса. Интересно, хотя бы ссылка на член временного члена продлевает срок службы этого временного члена или его члена? Я могу поклясться, что видел вопрос об этом на SO некоторое время назад ...
Xeo
1
@Xeo: Это не совсем так. Разрешение перегрузки всегда выберет неконстантную версию, если она существует. Вам нужно будет выполнить приведение, чтобы получить версию const. Я обновил пост, чтобы уточнить.
Николь Болас
15
Я подумал, что мог бы объяснить, после всего, что я создал эту функцию для C ++ 11;) Xeo прав, настаивая на том, что он не меняет тип *this, однако я могу понять, откуда возникла путаница. Это связано с тем, что ref-qualifier изменяет тип неявного (или «скрытого») параметра функции, с которым связан объект «this» (здесь ставятся цели!) Во время разрешения перегрузки и вызова функции. Таким образом, без изменений, *thisпоскольку это исправлено, как объясняет Xeo. Вместо этого измените параметр «hiddden», чтобы сделать его ссылкой на lvalue или rvalue, точно так же, как constэто делает квалификатор функции и constт. Д.
bronekk