Что такое "ссылка на значение для * этого"?
На странице статуса C++11 в Clang наткнулся на предложение под названием "ссылка на rvalue для * this".
Я прочитал довольно много о ссылках rvalue и понял их, но я не думаю, что знаю об этом. Я также не мог найти много ресурсов в Интернете, используя термины.
На странице есть ссылка на документ с предложением: N2439 (Расширение семантики перемещения на *this), но я также не получаю много примеров оттуда.
О чем эта особенность?
3 ответа
Во-первых, "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-qualifiers (const
а также volatile
) и реф-классификаторы (&
а также &&
).
Примечание: многие стандартные кавычки и объяснение разрешения перегрузки приведены здесь!
† Чтобы понять, как это работает, и почему ответ @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 ссылка на резюме
X
”Для функций, объявленных без квалификатора ref или с&
реф-классификатор"Rvalue ссылка на резюме
X
”Для функций, объявленных с&&
реф-классификаторгде
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, значение 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
всегда lvalue:
§5.3.1 [expr.unary.op] p1
Одинарный
*
Оператор выполняет косвенное обращение: выражение, к которому оно применяется, должно быть указателем на тип объекта или указателем на тип функции, а результатом является lvalue, указывающее на объект или функцию, на которые указывает выражение.
§9.3.2 [class.this] p1
В теле нестатической (9.3) функции-члена ключевое слово
this
является выражением prvalue, значением которого является адрес объекта, для которого вызывается функция. Типthis
в функции-члене классаX
являетсяX*
, [...]
Существует дополнительная возможность использования формы 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.
Допустим, у вас есть две функции в классе, оба с одинаковым именем и подписью. Но один из них объявлен 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