12.4 — Lvalue odniesienia do const

W poprzedniej lekcji (12.3 -- Odniesienia do wartości), omówiliśmy, w jaki sposób odwołanie do lwartości może wiązać się tylko z modyfikowalną wartością. Oznacza to, że poniższe działania są nielegalne:

int main()
{
    const int x { 5 }; // x to niemodyfikowalna (stała) wartość
    int& ref { x }; // błąd: ref nie może powiązać z niemodyfikowalnym lwartość

    return 0;
}

Jest to niedozwolone, ponieważ umożliwiłoby nam modyfikację zmiennej stałej (x) poprzez odwołanie inne niż stałe (ref).

Ale co, jeśli chcemy mieć zmienną stałą, do której chcemy utworzyć odniesienie? Zwykłe odniesienie do wartości (do wartości niebędącej stałą) nie wystarczy.

Odniesienie do wartości const

Używając słowa kluczowego const podczas deklarowania odniesienia do wartości, informujemy Odniesienie do wartości, aby traktować obiekt, do którego się odwołuje, jako do stałej. Takie odniesienie nazywa się odniesieniem do wartości stałej (czasami nazywany odniesieniem do const lub odniesieniem do stałej).

Odwołania lwartości do const mogą wiązać się z niemodyfikowalnymi lwartościami:

int main()
{
    const int x { 5 };    // x jest niemodyfikowalną wartością
    const int& ref { x }; // OK: ref jest referencją do lwartości const wartość

    return 0;
}

Ponieważ odniesienia lwartości do const traktują obiekt, do którego się odwołują jako const, można ich używać do uzyskiwania dostępu, ale nie do modyfikowania wartości, do której się odwołuje:

#include <iostream>

int main()
{
    const int x { 5 };    // x jest niemodyfikowalną wartością
    const int& ref { x }; // OK: ref jest referencją do lwartości const wartość

    std::cout << ref << '\n'; // OK: możemy uzyskać dostęp do const obiekt
    ref = 6;                  // błąd: nie możemy zmodyfikować obiektu poprzez odwołanie do stałej
    
    return 0;
}

Inicjowanie odniesienia do wartości do const z modyfikowalną wartością

Odniesienia do wartości do const mogą również wiązać do modyfikowalnych lwartości. W takim przypadku obiekt, do którego się odwołujemy, jest traktowany jako stały, gdy uzyskujemy do niego dostęp poprzez odwołanie (nawet jeśli obiekt bazowy nie jest stały):

#include <iostream>

int main()
{
    int x { 5 };          // x jest modyfikowalną wartością
    const int& ref { x }; // ok: możemy powiązać odwołanie do stałej z modyfikowalną wartością

    std::cout << ref << '\n'; // ok: możemy uzyskać dostęp do obiektu poprzez nasze odwołanie do stałej
    ref = 7;                  // błąd: nie możemy zmodyfikować obiektu poprzez odwołanie do stałej

    x = 6;                // ok: x jest modyfikowalną wartością, nadal możemy ją modyfikować poprzez oryginalny identyfikator

    return 0;
}

W powyższym programie łączymy stałą referencję ref z modyfikowalną lwartością x. Możemy wtedy użyć ref aby uzyskać dostęp x, ale ponieważ ref jest const, nie możemy modyfikować wartości x do ref. Jednakże nadal możemy modyfikować wartość x bezpośrednio (używając identyfikatora x).

Najlepsza praktyka

Zamawiaj lvalue references to const za lvalue references to non-const chyba że musisz zmodyfikować obiekt, do którego się odwołujesz.

Inicjowanie odniesienia do wartości do const za pomocą rwartości

Być może, co zaskakujące, lwartości odniesienia do const mogą również wiązać się z rwartościami:

#include <iostream>

int main()
{
    const int& ref { 5 }; // OK: 5 to wartość

    std::cout << ref << '\n'; // prints 5

    return 0;
}

Kiedy tak się stanie, tworzony jest obiekt tymczasowy i inicjalizowany tą wartością, a odwołanie do const jest powiązane z tym obiektem tymczasowym.

Powiązana treść

Omówiliśmy obiekty tymczasowe w lekcji 2.5 — Wprowadzenie do zakresu lokalnego.

Inicjowanie odniesienia do wartości do const wartością innego typu

Odniesienia do wartości stałej mogą nawet wiązać się z wartościami innego typu typ, o ile te wartości można niejawnie przekonwertować na typ referencyjny:

#include <iostream>

int main()
{
    // case 1
    const double& r1 { 5 };  // tymczasowe double inicjalizowane wartością 5, r1 łączy się z tymczasowym

    std::cout << r1 << '\n'; // prints 5

    // case 2
    char c { 'a' };
    const int& r2 { c };     // tymczasowym int inicjalizowanym wartością 'a', r2 łączy się z tymczasowy

    std::cout << r2 << '\n'; // drukuje 97 (ponieważ r2 jest odniesieniem do int)

    return 0;
}

W przypadku 1 tworzony jest tymczasowy obiekt typu double i inicjowany wartością int 5. Następnie const double& r1 jest powiązany z tym tymczasowym obiektem double.

W przypadku 2 tymczasowy obiekt typu int jest tworzony i inicjowany wartością char a. Następnie const int& r2 jest powiązany z tym tymczasowym obiektem int.

W obu przypadkach typ odniesienia i typ tymczasowego dopasowania.

Kluczowa informacja

Jeśli spróbujesz powiązać odwołanie do stałej lwartości z wartością innego typu, kompilator utworzy obiekt tymczasowy tego samego typu co odwołanie, zainicjuje go przy użyciu wartości, a następnie powiąże odwołanie z odwołaniem tymczasowym.

Zauważ także, że kiedy print r2 wypisuje jako int, a nie jako char. Dzieje się tak, ponieważ r2 jest odniesieniem do obiektu int (tymczasowego inta, który został utworzony), a nie do char c.

Chociaż zezwolenie na to może wydawać się dziwne, zobaczymy przykłady, gdzie jest to przydatne na lekcji 12.6 -- Przekaż stałą lwartość odniesienia.

Ostrzeżenie

Zwykle zakładamy, że referencja jest identyczna z obiektem, z którym jest powiązana - ale to założenie zostaje złamane, gdy referencja jest związany z tymczasową kopią obiektu lub tymczasowym obiektem powstałym w wyniku konwersji obiektu. Wszelkie modyfikacje dokonane później w oryginalnym obiekcie nie będą widoczne w odniesieniu (ponieważ odnosi się on do innego obiektu) i odwrotnie.

Oto głupi przykład ilustrujący to:

#include <iostream>

int main()
{
    short bombs { 1 };         // Mogę mieć bombę! (uwaga: typ jest krótki)
    
    const int& you { bombs };  // Możesz też mieć bombę (uwaga: typ to int&)
    --bombs;                   // Bomba zniknęła

    if (you)                   // Nadal żartuję?
    {
        std::cout << "Bombs away!  Goodbye, cruel world.\n"; // Aby zatańczyć bombę
    }

    return 0;
}

W powyższym przykładzie bombs jest short i you jest const int&. Ponieważ you można powiązać z int obiektem, gdy you inicjowania za pomocą bombs, kompilator niejawnie przekonwertuje bombs na int, co skutkuje utworzeniem obiektu tymczasowy int obiekt (o wartości 1). you jest ostatecznie powiązany z tym obiektem tymczasowym, a nie bombs.

Po wywołaniu bombs zmniejszany, you nie ma to wpływu, ponieważ odwołuje się do innego obiektu. Zatem chociaż spodziewamy się if (you) będzie oceniane jako false, w rzeczywistości wynosi true.

Gdybyś przestał wysadzać świat w powietrze, byłoby wspaniale.

Odniesienia stałe powiązane z obiektami tymczasowymi wydłużają czas życia obiektu tymczasowego

Obiekty tymczasowe są zwykle niszczone na końcu wyrażenia, w którym zostały utworzone.

Biorąc pod uwagę instrukcję const int& ref { 5 };, zastanów się, co by się stało, gdyby obiekt tymczasowy został utworzony w celu przechowywania rvalue 5 została zniszczona na końcu wyrażenia inicjującego ref. Referencja ref pozostałaby zawieszona (odwołująca się do obiektu, który został zniszczony), a przy próbie uzyskania dostępu ref.

zachowanie byłoby niezdefiniowane.Aby uniknąć w takich przypadkach zawieszonych referencji, w C++ obowiązuje specjalna zasada: Kiedy odwołanie do stałej lwartości jest bezpośrednio powiązane z obiektem tymczasowym, czas życia obiektu tymczasowego jest wydłużany tak, aby odpowiadał czasowi życia obiektu tymczasowego reference.

#include <iostream>

int main()
{
    const int& ref { 5 }; // Typ obiektu tymczasowego przechowującego wartość 5 został przedłużony tak, aby pasował do ref

    std::cout << ref << '\n'; // Dlatego możemy bezpiecznie użyć jej tutaj

    return 0;
} // Zarówno ref, jak i obiekt tymczasowy tutaj umierają

W powyższym przykładzie, gdy ref jest inicjowany wartością 5, tworzony jest obiekt tymczasowy i ref jest powiązany z tym obiektem tymczasowym. Czas życia obiektu tymczasowego odpowiada czasowi życia ref. Dzięki temu możemy bezpiecznie wydrukować wartość ref w kolejnym wyciągu. Następnie zarówno ref jak i obiekt tymczasowy wychodzą poza zakres i są niszczone na końcu bloku.

Kluczowa informacja

Odniesienia do wartości lvalue mogą wiązać się tylko z modyfikowalnymi lwartościami.

Odniesienia lwartości do const mogą wiązać się z modyfikowalnymi lwartościami, niemodyfikowalnymi lwartościami i rwartościami. To czyni je znacznie bardziej elastycznym typem odniesienia.

Dla zaawansowanych czytelników

Przedłużenie okresu istnienia działa tylko wtedy, gdy odwołanie stałe jest bezpośrednio powiązane z tymczasowym. Elementy tymczasowe zwrócone przez funkcję (nawet te zwrócone przez odwołanie do stałej) nie kwalifikują się do przedłużenia okresu istnienia.

Pokazujemy przykład tego na lekcji 12.12 -- Powrót przez referencję i powrót przez adres.

W przypadku wartości typu klasy powiązanie referencji z elementem wydłuży czas życia całego obiektu.

Dlaczego więc C++ pozwala na powiązanie stałej referencji z wartością? Odpowiemy na to pytanie na następnej lekcji!

Constexpr lvalue references Opcjonalne

Po zastosowaniu do odniesienia constexpr pozwala na użycie tego odniesienia w wyrażeniu stałym. Odniesienia Constexpr mają szczególne ograniczenie: można je powiązać tylko z obiektami o statycznym czasie trwania (albo globalnymi, albo statycznymi lokalnymi). Dzieje się tak, ponieważ kompilator wie, gdzie w pamięci zostaną utworzone instancje obiektów statycznych, więc może traktować ten adres jako stałą czasową kompilacji.

Odwołanie constexpr nie może wiązać się z (niestatyczną) zmienną lokalną. Dzieje się tak, ponieważ adres zmiennych lokalnych nie jest znany, dopóki nie zostanie faktycznie wywołana funkcja, w której są zdefiniowane.

int g_x { 5 };

int main()
{
    [[maybe_unused]] constexpr int& ref1 { g_x }; // ok, można powiązać z global

    static int s_x { 6 };
    [[maybe_unused]] constexpr int& ref2 { s_x }; // ok, można powiązać ze statycznym local

    int x { 6 };
    [[maybe_unused]] constexpr int& ref3 { x }; // kompiluj błąd: nie można powiązać z obiektem niestatycznym

    return 0;
}

Definiując odwołanie constexpr do zmiennej const, musimy zastosować zarówno constexpr (co dotyczy odniesienia), jak i const (co dotyczy typu, do którego się odwołujemy).

int main()
{
    static const int s_x { 6 }; // a const int
    [[maybe_unused]] constexpr const int& ref2 { s_x }; // potrzebujesz constexpr i const

    return 0;
}

Biorąc pod uwagę te ograniczenia, odniesienia constexpr zazwyczaj nie widzą zbyt wiele użyj.

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
223 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze