12.14 — Dedukcja typów ze wskaźnikami, referencjami i const

W lekcji 10.8 -- Dedukcja typu dla obiektów korzystających z auto słowo kluczowe, omówiliśmy, jak można użyć słowa kluczowego auto , aby kompilator wywnioskował typ zmiennej z inicjatora:

int main()
{
    int a { 5 };
    auto b { a }; // b deduced as an int

    return 0;
}

Zauważyliśmy również, że poprzez domyślnie dedukcja typu zostanie usunięta const z typów:

int main()
{
    const double a { 7.8 }; // a has type const double
    auto b { a };           // b has type double (const dropped)

    constexpr double c { 7.8 }; // c has type const double (constexpr implicitly applies const)
    auto d { c };               // d has type double (const dropped)

    return 0;
}

Const (lub constexpr) można zastosować ponownie, dodając kwalifikator const (lub constexpr) do definicji wydedukowanego typu:

int main()
{
    double a { 7.8 };    // a has type double
    const auto b { a };  // b has type const double (const applied)

    constexpr double c { 7.8 }; // c has type const double (constexpr implicitly applies const)
    const auto d { c };         // d is const double (const dropped, const reapplied)
    constexpr auto e { c };     // e is constexpr double (const dropped, constexpr reapplied)

    return 0;
}

Dedukcja typu usuwa odniesienia

Oprócz usunięcia const, dedukcja typu spowoduje również usunięcie referencji:

#include <string>

std::string& getRef(); // some function that returns a reference

int main()
{
    auto ref { getRef() }; // type deduced as std::string (not std::string&)

    return 0;
}

W powyższym przykładzie zmienna ref stosuje dedukcję typu. Chociaż funkcja getRef() zwraca a std::string&, kwalifikator odniesienia jest odrzucany, więc typ ref jest wydedukowany jako std::string.

Podobnie jak w przypadku usuniętego const, jeśli chcesz, aby wydedukowany typ był odniesieniem, możesz ponownie zastosować odniesienie w punkcie definicji:

#include <string>

std::string& getRef(); // some function that returns a reference

int main()
{
    auto ref1 { getRef() };  // std::string (reference dropped)
    auto& ref2 { getRef() }; // std::string& (reference dropped, reference reapplied)

    return 0;
}

Stały najwyższy poziom i niski poziom const

A najwyższego poziomu const jest kwalifikatorem const, który ma zastosowanie do samego obiektu. Na przykład:

const int x;    // this const applies to x, so it is top-level
int* const ptr; // this const applies to ptr, so it is top-level
// references don't have a top-level const syntax, as they are implicitly top-level const

Z kolei stała niskiego poziomu jest kwalifikatorem stałej, który ma zastosowanie do obiektu, do którego odwołuje się lub na który wskazuje:

const int& ref; // this const applies to the object being referenced, so it is low-level
const int* ptr; // this const applies to the object being pointed to, so it is low-level

Odwołanie do wartości stałej jest zawsze stałą niskiego poziomu. Wskaźnik może mieć stałą najwyższego, niskiego poziomu lub oba rodzaje stałych:

const int* const ptr; // the left const is low-level, the right const is top-level

Kiedy mówimy, że dedukcja typu pomija kwalifikatory const, pomija tylko stałe najwyższego poziomu. Stałe niskiego poziomu nie są usuwane. Za chwilę zobaczymy tego przykłady.

Dedukcja typu i odniesienia do stałych

Jeśli inicjatorem jest odwołanie do const, odwołanie jest najpierw usuwane (a następnie stosowane ponownie, jeśli ma to zastosowanie), a następnie z wyniku usuwane są wszelkie stałe najwyższego poziomu.

#include <string>

const std::string& getConstRef(); // some function that returns a reference to const

int main()
{
    auto ref1{ getConstRef() }; // std::string (reference dropped, then top-level const dropped from result)

    return 0;
}

W powyższym przykładzie, ponieważ getConstRef() zwraca a const std::string&, odwołanie jest usuwane jako pierwsze, pozostawiając nas z a const std::string. Ta stała jest teraz stałą najwyższego poziomu, więc została również usunięta, pozostawiając wydedukowany typ jako std::string.

Kluczowa informacja

Usunięcie odniesienia może zmienić stałą niskiego poziomu na stałą najwyższego poziomu: const std::string& jest stałą niskiego poziomu, ale porzuca wydajność odniesienia const std::string, która jest stałą najwyższego poziomu.

Możemy ponownie zastosować odniesienie i/lub const:

#include <string>

const std::string& getConstRef(); // some function that returns a const reference

int main()
{
    auto ref1{ getConstRef() };        // std::string (reference and top-level const dropped)
    const auto ref2{ getConstRef() };  // const std::string (reference dropped, const dropped, const reapplied)

    auto& ref3{ getConstRef() };       // const std::string& (reference dropped and reapplied, low-level const not dropped)
    const auto& ref4{ getConstRef() }; // const std::string& (reference dropped and reapplied, low-level const not dropped)

    return 0;
}

Przypadek ref1 omówiliśmy w poprzednim przykładzie. W przypadku ref2 jest to podobne do przypadku ref1 , z tą różnicą, że ponownie stosujemy kwalifikator const , więc wydedukowany typ to const std::string.

Rzeczy stają się bardziej interesujące z ref3. Zwykle odniesienie zostałoby najpierw usunięte, ale ponieważ ponownie je zastosowaliśmy, nie jest ono usuwane. Oznacza to, że typem jest nadal const std::string&. A ponieważ ta stała jest stałą niskiego poziomu, nie jest ona usuwana. Zatem wydedukowany typ to const std::string&.

Klasa ref4 działa podobnie do przypadku ref3, z tą różnicą, że ponownie zastosowaliśmy również kwalifikator const . Ponieważ typ został już wydedukowany jako odniesienie do const, ponowne zastosowanie const w tym miejscu jest zbędne. To powiedziawszy, użycie const w tym miejscu wyraźnie pokazuje, że nasz wynik będzie stały (podczas gdy w przypadku ref3 stałość wyniku jest ukryta i nieoczywista).

Najlepsza praktyka

Jeśli chcesz odniesienia do stałej, zastosuj ponownie kwalifikator const nawet jeśli nie jest to absolutnie konieczne, ponieważ wyjaśnia to Twoje intencje i pomaga zapobiegać błędy.

A co z odniesieniami do constexpr?

Constexpr nie jest częścią typu wyrażenia, więc nie jest wywnioskowane przez auto.

Przypomnienie

Podczas definiowania odniesienia do const (np. const int&), const odnosi się do obiektu, do którego się odwołuje, a nie do samego odniesienia.

Podczas definiowania odwołania constexpr do zmiennej const (np. constexpr const int&), musimy zastosować oba constexpr (co dotyczy odniesienia), jak i const (co dotyczy typu, do którego się odwołujemy).

Jest to omówione w lekcji 12.4 -- Odniesienia do wartości do const.

#include <string_view>
#include <iostream>

constexpr std::string_view hello { "Hello" };   // implicitly const

constexpr const std::string_view& getConstRef() // function is constexpr, returns a const std::string_view&
{
    return hello;
}

int main()
{
    auto ref1{ getConstRef() };                  // std::string_view (reference dropped and top-level const dropped)
    constexpr auto ref2{ getConstRef() };        // constexpr const std::string_view (reference dropped and top-level const dropped, constexpr applied, implicitly const)

    auto& ref3{ getConstRef() };                 // const std::string_view& (reference reapplied, low-level const not dropped)
    constexpr const auto& ref4{ getConstRef() }; // constexpr const std::string_view& (reference reapplied, low-level const not dropped, constexpr applied)
    
    return 0;
}

Dukcja typu i wskaźniki

W przeciwieństwie do odniesień, dedukcja typu nie powoduje utraty wskaźników:

#include <string>

std::string* getPtr(); // some function that returns a pointer

int main()
{
    auto ptr1{ getPtr() }; // std::string*

    return 0;
}

Możemy również użyć gwiazdki w połączeniu z dedukcją typu wskaźnika (auto*), aby było jaśniejsze, że wydedukowany typem jest wskaźnik:

#include <string>

std::string* getPtr(); // some function that returns a pointer

int main()
{
    auto ptr1{ getPtr() };  // std::string*
    auto* ptr2{ getPtr() }; // std::string*

    return 0;
}

Kluczowa informacja

Powodem, dla którego referencje są odrzucane podczas dedukcji typu, ale wskaźniki nie są usuwane, jest to, że referencje i wskaźniki mają różną semantykę.

Kiedy oceniamy referencję, tak naprawdę oceniamy obiekt, do którego się odwołujemy. Dlatego też, dedukując typ, sensowne jest, abyśmy wydedukowali typ rzeczy, do której się odwołujemy, a nie samo odniesienie. Ponadto, ponieważ wydedukowaliśmy brak odniesienia, bardzo łatwo jest uczynić z niego odniesienie, używając auto&. Jeśli zamiast tego dedukcja typu miałaby wydedukować referencję, składnia usuwania referencji, jeśli tego nie chcieliśmy, byłaby znacznie bardziej skomplikowana.

Z drugiej strony wskaźniki przechowują adres obiektu. Kiedy oceniamy wskaźnik, oceniamy wskaźnik, a nie obiekt, na który wskazujemy (jeśli chcemy, możemy wyłuskać wskaźnik). Dlatego sensowne jest, abyśmy wydedukowali typ wskaźnika, a nie rzecz, na którą wskazuje.

Różnica między auto i auto* Opcjonalne

Kiedy używamy auto z inicjatorem typu wskaźnika, typ wydedukowany dla auto obejmuje wskaźnik. Tak więc dla ptr1 powyżej typ zostaje zastąpiony auto Jest std::string*.

Kiedy używamy auto* z inicjatorem typu wskaźnikowego, typ wydedukowany dla auto nie obejmuje wskaźnik - wskaźnik jest później stosowany ponownie po wydedukowaniu typu. Zatem w ptr2 powyżej typ zostaje zastąpiony auto Jest std::string, a następnie wskaźnik jest ponownie stosowany.

W większości przypadków praktyczny efekt jest taki sam (ptr1 i ptr2 oba wnioski wynikają z std::string* w powyższym przykładzie).

Jest jednak kilka różnic pomiędzy auto i auto* w praktyce. Po pierwsze, auto* musi zostać przekształcony w inicjator wskaźnika, w przeciwnym razie wystąpi błąd kompilacji:

#include <string>

std::string* getPtr(); // some function that returns a pointer

int main()
{
    auto ptr3{ *getPtr() };      // std::string (because we dereferenced getPtr())
    auto* ptr4{ *getPtr() };     // does not compile (initializer not a pointer)

    return 0;
}

Ma to sens: w ptr4 przypadku auto wydedukuje się do std::string, a następnie wskaźnik zostanie ponownie zastosowany. Zatem ptr4 ma typ std::string* i nie możemy zainicjować std::string* inicjatorem, który nie jest wskaźnikiem.

Po drugie, istnieją różnice w auto i auto* zachowaniu, gdy wprowadzamy const do równania. Omówimy to poniżej.

Wpisz dedukcję i wskaźniki const Opcjonalne

Ponieważ wskaźniki nie są usuwane, nie musimy się tym martwić. Ale w przypadku wskaźników mamy do przemyślenia zarówno wskaźnik const, jak i wskaźnik do przypadków const, a także mamy auto vs auto*. Podobnie jak w przypadku referencji, podczas dedukcji typu wskaźnika pomijana jest tylko stała najwyższego poziomu.

Zacznijmy od prostego przypadku:

#include <string>

std::string* getPtr(); // some function that returns a pointer

int main()
{
    const auto ptr1{ getPtr() };  // std::string* const
    auto const ptr2 { getPtr() }; // std::string* const

    const auto* ptr3{ getPtr() }; // const std::string*
    auto* const ptr4{ getPtr() }; // std::string* const

    return 0;
}

Kiedy używamy jednego z auto const lub const auto, mówimy: „uczyń wydedukowany wskaźnik wskaźnikiem stałym”. Zatem w przypadku ptr1 i ptr2 wydedukowanym typem jest std::string*, a następnie stosowana jest const, co daje ostateczny typ std::string* const. Jest to podobne do tego, jak const int i int const oznacza to samo.

Jednak gdy używamy auto*, kolejność kwalifikatora stałej ma znaczenie. A const po lewej stronie oznacza „uczyń wydedukowany wskaźnik wskaźnikiem do stałej”, podczas gdy a const po prawej oznacza „uczyń wydedukowany wskaźnik wskaźnikiem stałym”. Zatem ptr3 kończy jako wskaźnik do const i ptr4 jest wskaźnikiem const.

Przyjrzyjmy się teraz przykładowi, w którym inicjatorem jest wskaźnik const do const.

#include <string>

int main()
{
    std::string s{};
    const std::string* const ptr { &s };

    auto ptr1{ ptr };  // const std::string*
    auto* ptr2{ ptr }; // const std::string*

    auto const ptr3{ ptr };  // const std::string* const
    const auto ptr4{ ptr };  // const std::string* const

    auto* const ptr5{ ptr }; // const std::string* const
    const auto* ptr6{ ptr }; // const std::string*

    const auto const ptr7{ ptr };  // error: const qualifer can not be applied twice
    const auto* const ptr8{ ptr }; // const std::string* const

    return 0;
}

Klasa ptr1 i ptr2 przypadki są proste. Stała najwyższego poziomu (stała na samym wskaźniku) jest usuwana. Stała niskiego poziomu na wskazywanym obiekcie nie jest usuwana. Zatem w obu przypadkach ostateczny typ const std::string*.

Klasa ptr3 i ptr4 przypadki są również proste. Stała najwyższego poziomu została usunięta, ale zastosujemy ją ponownie. Stała niskiego poziomu na wskazywanym obiekcie nie jest usuwana. Zatem w obu przypadkach ostateczny typ to const std::string* const.

Klasa ptr5 i ptr6 przypadki są analogiczne do przypadków, które pokazaliśmy w poprzednim przykładzie. W obu przypadkach stała najwyższego poziomu jest odrzucana. For ptr5, instrukcja auto* const ponownie stosuje stałą najwyższego poziomu, więc ostatecznym typem jest const std::string* const. Instancja for ptr6, instrukcja const auto* stosuje const do wskazywanego typu (który w tym przypadku był już const), więc końcowym typem jest const std::string*.

W ptr7 w przypadku <<<M0>>>stosujemy kwalifikator const dwukrotnie, co jest niedozwolone i spowoduje błąd kompilacji.

I na koniec, w przypadku ptr8 stosujemy const po obu stronach wskaźnika (co jest dozwolone, ponieważ auto* musi być typem wskaźnika), więc wynikowy typ to const std::string* const.

Najlepsza praktyka

Jeśli chcesz wskaźnik const, wskaźnik do const lub wskaźnik const do const, zastosuj ponownie const kwalifikator(y), nawet jeśli nie jest to absolutnie konieczne, ponieważ wyjaśnia to intencje i pomaga zapobiegać błędom.

Wskazówka

Rozważ użycie auto* przy dedukowaniu typu wskaźnika. Użycie auto* w tym przypadku wyjaśnia, że ​​dedukujemy typ wskaźnika, korzysta z pomocy kompilatora, aby upewnić się, że nie wydedukujemy typu innego niż wskaźnik, i daje większą kontrolę nad const.

Streszczenie

Przykro nam z powodu bólu głowy. Podsumujmy szybko najważniejsze punkty.

Stała najwyższego poziomu a stała niskiego poziomu:

  • Stała najwyższego poziomu dotyczy samego obiektu (np. const int x lub int* const ptr).
  • Stała niskiego poziomu dotyczy obiektu, do którego można uzyskać dostęp poprzez referencję lub wskaźnik (np. const int& ref, const int* ptr).

Jaki rodzaj dedukcji dedukuje:

  • Typ dedukcji najpierw usuwa wszelkie referencje (chyba że wywnioskowany typ jest zdefiniowany jako odniesienie). W przypadku stałego odniesienia, usunięcie odniesienia spowoduje, że stała (niskiego poziomu) stanie się stałą najwyższego poziomu.
  • Dedukcja typu następnie usuwa wszelkie stałe najwyższego poziomu (chyba że wydedukowany typ jest zdefiniowany jako const lub constexpr).
  • Constexpr nie jest częścią systemu typów, więc nigdy nie jest wydedukowany. Zawsze musi być jawnie zastosowany do wydedukowanego typ.
  • Dedukcja typu nie powoduje utraty wskaźników.
  • Zawsze jawnie definiuj wydedukowany typ jako odniesienie, const lub constexpr (jeśli ma to zastosowanie), nawet jeśli te kwalifikatory są zbędne, ponieważ mogłyby zostać wydedukowane. Pomaga to uniknąć błędów i wyjaśnia, jaki jest Twój zamiar.

Wpisz dedukcję i wskaźniki:

  • Podczas używania auto, typ wydedukowany będzie wskaźnikiem tylko wtedy, gdy inicjatorem jest wskaźnik. W przypadku użycia auto* typ wydedukowany jest zawsze wskaźnikiem, nawet jeśli inicjatorem nie jest wskaźnik.
  • auto const i const auto oba powodują, że wydedukowany wskaźnik jest wskaźnikiem stałym. Nie ma możliwości jawnego określenia stałej niskiego poziomu (wskaźnik do stałej) za pomocą auto.
  • auto* const również powoduje wydedukowany wskaźnik. wskaźnik stałej. const auto* czyni z wydedukowanego wskaźnika wskaźnik do stałej. Jeśli są one trudne do zapamiętania, int* const jest wskaźnikiem stałej (do int), więc auto* const musi być wskaźnikiem stałej. const int* jest wskaźnikiem do stałej (int), więc const auto* musi być wskaźnikiem pointer-to-const)
  • Rozważ użycie auto* za auto podczas dedukcji typu wskaźnika, ponieważ pozwala to na jawne ponowne zastosowanie zarówno stałej najwyższego, jak i niskiego poziomu, i spowoduje błąd, jeśli typ wskaźnika nie zostanie wydedukowany.
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:  
202 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze