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 wywnioskować jako 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 ma typ 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 ma typ 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 jest const double (const usunięty, const ponownie zastosowany)
constexpr auto e { c }; // e jest constexpr double (const usunięte, constexpr ponownie zastosowany)
return 0;
}Dedukcja typu usuwa odniesienia
Oprócz usunięcia const, dedukcja typu spowoduje również usunięcie referencji:
#include <string>
std::string& getRef(); // jakaś funkcja, która zwraca referencję
int main()
{
auto ref { getRef() }; // typ wydedukowany jako std::string (nie std::string&)
return 0;
}W powyższym przykładzie zmienna ref stosuje dedukcję typu. Chociaż funkcja getRef() zwraca 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(); // jakaś funkcja, która zwraca referencję
int main()
{
auto ref1 { getRef() }; // std::string (usunięto odwołanie)
auto& ref2 { getRef() }; // std::string& (odniesienie usunięte, odniesienie ponownie zastosowane)
return 0;
}Stały najwyższy poziom i niski poziom const
najwyższego poziomu const jest kwalifikatorem const, który ma zastosowanie do samego obiektu. Na przykład:
const int x; // ta stała dotyczy x, więc jest najwyższego poziomu
int* const ptr; // ta stała dotyczy ptr, więc tak jest najwyższego poziomu
// referencje nie mają składni const najwyższego poziomu, ponieważ domyślnie są const najwyższego poziomuZ 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; // ta stała dotyczy obiektu, do którego się odwołuje, więc jest niskiego poziomu
const int* ptr; // ta stała dotyczy obiektu, na który wskazuje, więc jest niskiego poziomuOdwoł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; // lewa const jest na niskim poziomie, prawa const jest na najwyższym poziomieKiedy 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(); // jakaś funkcja, która zwraca referencję const
int main()
{
auto ref1{ getConstRef() }; // std::string (usunięto odwołanie, następnie usunięto stałą najwyższego poziomu z wyniku)
return 0;
}W powyższym przykładzie, ponieważ getConstRef() zwraca const std::string&, odwołanie jest usuwane jako pierwsze, pozostawiając nas z 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(); // jakaś funkcja, która zwraca odwołanie do stałej
int main()
{
auto ref1{ getConstRef() }; // std::string (usunięto odwołanie i stałą najwyższego poziomu)
const auto ref2{ getConstRef() }; // const std::string (odniesienie zostało usunięte, const usunięte, const ponownie zastosowane)
auto& ref3{ getConstRef() }; // const std::string& (odniesienie zostało usunięte i ponownie zastosowane, const niskiego poziomu nie zostało usunięte)
const auto& ref4{ getConstRef() }; // const std::string& (odniesienie zostało usunięte i ponownie zastosowane, const niskiego poziomu nie zostało usunięte)
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() // funkcja to constexpr, zwraca const std::string_view&
{
return hello;
}
int main()
{
auto ref1{ getConstRef() }; // std::string_view (odniesienie usunięte i usunięto stałą najwyższego poziomu)
constexpr auto ref2{ getConstRef() }; // constexpr const std::string_view (usunięto odwołanie i constexpr najwyższego poziomu, zastosowano constexpr, domyślnie const)
auto& ref3{ getConstRef() }; // const std::string_view& (ponownie zastosowano odwołanie, nie usunięto stałej niskiego poziomu)
constexpr const auto& ref4{ getConstRef() }; // constexpr const std::string_view& (ponownie zastosowano odwołanie, nie usunięto stałej niskiego poziomu, zastosowano constexpr)
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(); // jakaś funkcja, która zwraca wskaźnik
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(); // jakaś funkcja, która zwraca wskaźnik
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(); // jakaś funkcja, która zwraca wskaźnik
int main()
{
auto ptr3{ *getPtr() }; // std::string (ponieważ my dereferencja getPtr())
auto* ptr4{ *getPtr() }; // nie kompiluje się (inicjator nie jest wskaźnikiem)
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(); // jakaś funkcja, która zwraca wskaźnik
int main()
{
const auto ptr1{ getPtr() }; // std::string* stała
auto const ptr2 { getPtr() }; // std::string* stała
const auto* ptr3{ getPtr() }; // stała std::string*
auto* const ptr4{ getPtr() }; // std::string* stała
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. const po lewej stronie oznacza „uczyń wydedukowany wskaźnik wskaźnikiem do stałej”, podczas gdy 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 }; // stała std::string*
auto* ptr2{ ptr }; // stała std::string*
auto const ptr3{ ptr }; // stała std::string* stała
const auto ptr4{ ptr }; // stała std::string* stała
auto* const ptr5{ ptr }; // stała std::string* stała
const auto* ptr6{ ptr }; // stała std::string*
const auto const ptr7{ ptr }; // błąd: const kwalifikatora nie można zastosować dwukrotnie
const auto* const ptr8{ ptr }; // stała std::string* stała
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 xlubint* 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
constlubconstexpr). - 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,
constlubconstexpr(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życiaauto*typ wydedukowany jest zawsze wskaźnikiem, nawet jeśli inicjatorem nie jest wskaźnik. auto consticonst autooba 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* constró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* constjest wskaźnikiem stałej (do int), więcauto* constmusi być wskaźnikiem stałej.const int*jest wskaźnikiem do stałej (int), więcconst auto*musi być wskaźnikiem pointer-to-const)- Rozważ użycie
auto*zaautopodczas 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.

