Co to jest „odniesienie do wartości dla * this”?


238

Natrafiłem na propozycję o nazwie „odwołanie do wartości dla * this” na stronie statusu C ++ 11 clanga .

Czytałem sporo o referencjach do wartości i je rozumiałem, ale nie wiem o tym. Nie mogłem też znaleźć wielu zasobów w Internecie, używając tych warunków.

Na stronie znajduje się link do dokumentu z propozycją: N2439 (Rozszerzanie semantyki przenoszenia na * to), ale nie otrzymuję zbyt wielu przykładów.

O czym jest ta funkcja?

Odpowiedzi:


293

Po pierwsze, „kwalifikatory odniesienia dla * tego” to tylko „oświadczenie marketingowe”. Rodzaj *thisnigdy się nie zmienia, patrz na dole tego postu. Jednak dzięki temu sformułowaniu o wiele łatwiej to zrozumieć.

Następnie poniższy kod wybiera funkcję, która ma zostać wywołana na podstawie kwalifikatora odniesienia „niejawnego parametru obiektu” funkcji :

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

Wynik:

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

Wszystko po to, abyś mógł skorzystać z faktu, że obiekt, w którym funkcja jest wywoływana, to wartość (na przykład nienazwana tymczasowa). Weź następujący kod jako kolejny przykład:

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

To może być nieco wymyślone, ale powinieneś o tym pomyśleć.

Pamiętaj, że możesz łączyć kwalifikatory cv ( consti volatile) i kwalifikatory ref ( &i &&).


Uwaga: tutaj znajduje się wiele standardowych cytatów i wyjaśnienie rozwiązania problemu przeciążenia!

† Aby zrozumieć, jak to działa i dlaczego odpowiedź @Nicol Bolas jest co najmniej częściowo błędna, musimy nieco zagłębić się w standard C ++ (część wyjaśniająca, dlaczego odpowiedź @ Nicol jest błędna, znajduje się na dole, jeśli jesteś tylko zainteresowani tym).

Która funkcja ma zostać wywołana, zależy od procesu zwanego rozdzielczością przeciążenia . Ten proces jest dość skomplikowany, więc dotkniemy tylko tego, co jest dla nas ważne.

Po pierwsze, ważne jest, aby zobaczyć, jak działa rozwiązywanie przeciążenia dla funkcji członka:

§13.3.1 [over.match.funcs]

p2 Zbiór funkcji kandydujących może zawierać zarówno funkcje składowe, jak i nie będące członkami, które zostaną rozstrzygnięte na tej samej liście argumentów. Aby listy argumentów i parametrów były porównywalne w ramach tego heterogenicznego zestawu, uważa się, że funkcja elementu zawiera dodatkowy parametr, zwany niejawnym parametrem obiektu, który reprezentuje obiekt, dla którego funkcja elementu została wywołana . [...]

p3 Podobnie, gdy jest to właściwe, kontekst może skonstruować listę argumentów, która zawiera domyślny argument obiektowy dla oznaczenia obiektu, który ma być obsługiwany.

Dlaczego musimy nawet porównywać funkcje składowe i nie będące członkami? Przeciążenie operatora, dlatego. Rozważ to:

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

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

Na pewno chcesz, aby następujące wywołały funkcję bezpłatną, prawda?

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

Dlatego funkcje składowe i nie będące członkami są zawarte w tak zwanym zestawie przeciążenia. Aby uprościć rozdzielczość, istnieje odważna część standardowej oferty. Ponadto jest to dla nas ważny bit (ta sama klauzula):

p4 W przypadku niestatycznych funkcji składowych typem niejawnego parametru obiektu jest

  • „Odwołanie do wartości cv X ” dla funkcji zadeklarowanych bez kwalifikatora ref lub kwalifikatora & ref

  • „Odwołanie do wartości cv X ” dla funkcji zadeklarowanych za pomocą && kwalifikatora ref

gdzie Xjest klasą, której funkcja jest składnikiem, a cv jest kwalifikacją cv w deklaracji funkcji składowej. [...]

p5 Podczas rozwiązywania przeciążenia [...] [t] domyślny parametr obiektu [...] zachowuje swoją tożsamość, ponieważ konwersje na odpowiedni argument powinny być zgodne z następującymi dodatkowymi zasadami:

  • nie można wprowadzić żadnego obiektu tymczasowego do przechowywania argumentu dla parametru obiektu niejawnego; i

  • żadne konwersje zdefiniowane przez użytkownika nie mogą być zastosowane, aby osiągnąć dopasowanie typu do niego

[...]

(Ostatni bit oznacza po prostu, że nie można oszukiwać rozdzielczości przeciążenia na podstawie niejawnej konwersji obiektu, do którego wywoływana jest funkcja członka (lub operator).)

Weźmy pierwszy przykład na początku tego postu. Po wspomnianej transformacji zestaw przeciążeń wygląda mniej więcej tak:

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

Następnie lista argumentów zawierająca domyślny argument obiektowy jest porównywana z listą parametrów każdej funkcji zawartej w zestawie przeciążenia. W naszym przypadku lista argumentów będzie zawierać tylko ten argument obiektowy. Zobaczmy, jak to wygląda:

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

Jeśli po przetestowaniu wszystkich przeciążeń w zestawie pozostanie tylko jeden, rozwiązywanie problemu przeciążenia zakończyło się powodzeniem i wywoływana jest funkcja powiązana z tym transformowanym przeciążeniem. To samo dotyczy drugiego połączenia z „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

Uwaga jednak, że gdyby nie my dostarczyły żadnych Ref-kwalifikator (i jako takie nie przeciążony funkcji), że f1 byłoby dopasować rvalue (jeszcze §13.3.1):

p5 [...] W przypadku niestatycznych funkcji składowych zadeklarowanych bez kwalifikatora odniesienia obowiązuje dodatkowa reguła:

  • nawet jeśli niejawny parametr obiektu nie constjest kwalifikowany, wartość można powiązać z parametrem, o ile pod wszystkimi innymi względami argument można przekonwertować na typ parametru obiektu niejawnego.
struct test{
  void f() { std::cout << "lvalue or rvalue object\n"; }
};

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

Teraz, dlaczego odpowiedź @ Nicol jest przynajmniej częściowo błędna. On mówi:

Uwaga: ta deklaracja zmienia typ *this.

To źle, zawsze*this jest to wartość:

§5.3.1 [expr.unary.op] p1

Jednoargumentowy *operator wykonuje pośrednie : wyrażenie, do którego jest stosowane, powinno być wskaźnikiem do typu obiektu lub wskaźnikiem do typu funkcji, a wynikiem jest wartość odnosząca się do obiektu lub funkcji, na którą wskazuje wyrażenie.

§9.3.2 [class.this] p1

W treści niestatystycznej (9,3) funkcji thisskładowej słowo kluczowe jest wyrażeniem prvalue, którego wartością jest adres obiektu, dla którego funkcja jest wywoływana. Typem thisfunkcji składowej klasy Xjest X*. [...]


Uważam, że typy paraneterów zaraz po sekcji „po transformacji” powinny być „foo” zamiast „test”.
ryaner

@ryaner: Dobre znalezisko, dziękuję. Chociaż nie parametr, ale identyfikator klasy funkcji jest nieprawidłowy. :)
Xeo

Ups, przepraszam, zapomniałem o klasie zabawek o nazwie test, kiedy przeczytałem tę część i pomyślałem, że f jest zawarte w foo, więc mój komentarz ..
Ryaner

Czy można to zrobić za pomocą konstruktorów MyType(int a, double b) &&:?
Germán Diago,

2
„Rodzaj * to się nigdy nie zmienia” Być może powinieneś być nieco jaśniejszy, że nie zmienia się w oparciu o kwalifikację wartości r / l. ale może zmieniać się między const / non-const.
xaxxon,

78

Istnieje dodatkowy przypadek użycia formularza kwalifikatora refinansowania wartości. C ++ 98 ma język, który umożliwia constwywoływanie funkcji nie będących członkami dla instancji klas, które są wartościami. Prowadzi to do wszelkiego rodzaju dziwności, która jest sprzeczna z samą koncepcją wartościowości i odbiega od sposobu działania typów wbudowanych:

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

Kwalifikatory referencyjne Lvalue rozwiązują następujące problemy:

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

Teraz operatory działają jak te wbudowanych typów, akceptując tylko wartości lv.


28

Załóżmy, że masz dwie funkcje w klasie, obie o tej samej nazwie i podpisie. Ale jeden z nich jest zadeklarowany const:

void SomeFunc() const;
void SomeFunc();

Jeśli instancja klasy nie jest const, rozdzielczość przeciążenia będzie preferencyjnie wybierać wersję inną niż const. Jeśli instancja jest const, użytkownik może wywołać tylko constwersję. A thiswskaźnik toconst wskaźnik, więc instancja nie może być zmieniony.

To, co robi „wartość odniesienia r dla tego”, pozwala dodać inną alternatywę:

void RValueFunc() &&;

Pozwala to na posiadanie funkcji, którą można wywołać tylko wtedy, gdy użytkownik wywoła ją za pomocą odpowiedniej wartości r. Więc jeśli jest to typ Object:

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

W ten sposób możesz specjalizować zachowanie w oparciu o to, czy dostęp do obiektu jest uzyskiwany za pomocą wartości r, czy nie.

Pamiętaj, że nie możesz przeciążać wersji referencyjnych o wartości r i wersji innych niż referencyjne. To znaczy, jeśli masz nazwę funkcji składowej, wszystkie jej wersje albo używają kwalifikatorów wartości l / r this, albo żadna z nich nie. Nie możesz tego zrobić:

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

Musisz to zrobić:

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

Uwaga: ta deklaracja zmienia typ *this. Oznacza to, że &&wszystkie wersje mają dostęp do elementów jako referencje wartości r. Dzięki temu możliwe jest łatwe poruszanie się wewnątrz obiektu. Przykład podany w pierwszej wersji propozycji to (uwaga: poniższe informacje mogą być niepoprawne w ostatecznej wersji C ++ 11; pochodzi bezpośrednio z początkowej propozycji „wartość r z tej”):

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
Myślę, że potrzebujesz std::movedrugiej wersji, nie? Ponadto, dlaczego zwracane jest odwołanie do wartości?
Xeo

1
@Xeo: Bo taki był przykład w propozycji; Nie mam pojęcia, czy nadal działa z bieżącą wersją. Powodem powrotu wartości referencyjnej r jest to, że ruch powinien zależeć od osoby ją przechwytującej. To nie powinno się jeszcze zdarzyć, na wypadek, gdyby naprawdę chciał zapisać go w && zamiast wartości.
Nicol Bolas,

3
Tak, pomyślałem trochę o przyczynie mojego drugiego pytania. Zastanawiam się jednak, czy rvalue odnosi się do członka tymczasowego przedłuża jego żywotność, czy do jego członka? Mógłbym przysiąc, że widziałem o tym pytanie na SO jakiś czas temu ...
Xeo

1
@Xeo: To nie do końca prawda. Rozwiązanie problemu przeciążenia zawsze wybierze wersję inną niż const, jeśli istnieje. Aby uzyskać wersję const, musisz wykonać rzutowanie. Zaktualizowałem post, aby wyjaśnić.
Nicol Bolas,

15
Pomyślałem, że mógłbym to wyjaśnić, po tym wszystkim, że stworzyłem tę funkcję dla C ++ 11;) Xeo słusznie twierdzi, że nie zmienia typu *this, ale rozumiem, skąd bierze się zamieszanie. Wynika to z faktu, że kwalifikator ref zmienia typ niejawnego (lub „ukrytego”) parametru funkcji, z którym związany jest ten „obiekt” (tutaj podano cudzysłowy!) Podczas rozwiązywania przeciążenia i wywołania funkcji. Więc nie ma zmiany, *thisponieważ jest to naprawione, jak wyjaśnia Xeo. Zamiast tego zmiana parametru „ukrytego”, aby stał się referencją do wartości lvalue lub rvalue, podobnie jak constkwalifikator funkcji sprawia, że constitd.
bronekk
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.