Dedukcja argumentów szablonu klasy (CTAD) C++17
Począwszy od C++17, podczas tworzenia instancji obiektu na podstawie szablonu klasy, kompilator może wywnioskować typy szablonów z typów inicjatora obiektu (nazywa się to w skrócie dedukcją argumentów szablonu klasy lub CTAD ). Na przykład:
#include <utility> // dla std::pair
int main()
{
std::pair<int, int> p1{ 1, 2 }; // jawnie określ szablon klasy std::pair<int, int> (C++11 i nowsze)
std::pair p2{ 1, 2 }; // CTAD użyte do wydedukowania std::pair<int, int> z inicjatorów (C++17)
return 0;
}CTAD jest wykonywane tylko wtedy, gdy nie istnieje żadna szablonowa lista argumentów. Dlatego oba poniższe przypadki są błędami:
#include <utility> // dla std::pair
int main()
{
std::pair<> p1 { 1, 2 }; // błąd: za mało argumentów szablonu, nie wydedukowano obu argumentów
std::pair<int> p2 { 3, 4 }; // błąd: za mało argumentów szablonu, nie wydedukowano drugiego argumentu
return 0;
}Nota autora
Wiele przyszłych lekcji na tej stronie korzysta z CTAD. Jeśli kompilujesz te przykłady przy użyciu standardu C++ 14 (lub starszego), pojawi się błąd dotyczący brakujących argumentów szablonu. Będziesz musiał jawnie dodać takie argumenty do przykładu, aby go skompilować.
Ponieważ CTAD jest formą dedukcji typu, możemy użyć sufiksów dosłownych, aby zmienić wydedukowany typ:
#include <utility> // dla std::pair
int main()
{
std::pair p1 { 3.4f, 5.6f }; // wydedukowano do pary<float, float>
std::pair p2 { 1u, 2u }; // wydedukowano do pary<unsigned int, unsigned int>
return 0;
}Przewodniki po dedukowaniu argumentów w szablonie C++17
W większości przypadków CTAD działa od razu po wyjęciu z pudełka. Jednak w niektórych przypadkach kompilator może potrzebować dodatkowej pomocy w zrozumieniu, jak prawidłowo wydedukować argumenty szablonu.
Możesz być zaskoczony, gdy odkryjesz, że następujący program (który jest prawie identyczny z przykładem, w którym zastosowano std::pair powyżej) nie kompiluje się w C++17 (tylko):
// zdefiniuj własny typ pary
template <typename T, typename U>
struct Pair
{
T first{};
U second{};
};
int main()
{
Pair<int, int> p1{ 1, 2 }; // ok: jawnie określamy argumenty szablonu
Pair p2{ 1, 2 }; // błąd kompilacji w C++17 (w porządku w C++20)
return 0;
}Jeśli skompilujesz to w C++17, prawdopodobnie pojawi się błąd o „dedukcja argumentów szablonu klasy nie powiodła się” lub „nie można wydedukować argumentów szablonu” lub „Brak wykonalnego konstruktora lub przewodnika po dedukcjach”. Dzieje się tak, ponieważ w C++17 CTAD nie wie, jak wywnioskować argumenty szablonu dla szablonów klas zagregowanych. Aby rozwiązać ten problem, możemy udostępnić kompilatorowi przewodnik po dedukcjach, który informuje kompilator, jak wydedukować argumenty szablonu dla danego szablonu klasy.
Oto ten sam program z przewodnikiem po dedukcjach:
template <typename T, typename U>
struct Pair
{
T first{};
U second{};
};
// Oto przewodnik po dedukcjach dla naszej para (wymagana tylko w C++17)
// Obiekty par zainicjowane argumentami typu T i U powinny wydedukować Pair<T, U>
template <typename T, typename U>
Pair(T, U) -> Pair<T, U>;
int main()
{
Pair<int, int> p1{ 1, 2 }; // jawnie określ szablon klasy Pair<int, int> (C++11 i nowsze)
Pair p2{ 1, 2 }; // CTAD użyte do wydedukowania Pair<int, int> z inicjatorów (C++17)
return 0;
}Ten przykład powinien zostać skompilowany w języku C++17.
Przewodnik po dedukcjach dla nasza Pair klasa jest dość prosta, ale przyjrzyjmy się bliżej, jak to działa.
// Oto przewodnik po dedukcjach dla naszej para (wymagana tylko w C++17)
// Obiekty par zainicjowane argumentami typu T i U powinny wydedukować Pair<T, U>
template <typename T, typename U>
Pair(T, U) -> Pair<T, U>;Najpierw używamy tej samej definicji typu szablonu, co w naszej klasie Pair . Ma to sens, ponieważ jeśli nasz przewodnik dedukcji ma powiedzieć kompilatorowi, jak wydedukować typy dla Pair<T, U>, musimy zdefiniować, czym T i U są (typy szablonów). Po drugie, po prawej stronie strzałki znajduje się typ, który pomagamy kompilatorowi wydedukować. W tym przypadku chcemy, aby kompilator potrafił wydedukować argumenty szablonu dla obiektów typu Pair<T, U>, więc właśnie to tutaj umieściliśmy. Na koniec, po lewej stronie strzałki, mówimy kompilatorowi, jakiego rodzaju deklaracji ma szukać. W tym przypadku mówimy mu, aby szukał deklaracji jakiegoś obiektu o nazwie Pair z dwoma argumentami (jeden typu T, drugi typu U). Moglibyśmy to również zapisać jako Pair(T t, U u) (gdzie t i u są nazwami parametrów, ale ponieważ nie używamy t i u, nie musimy nadawać im nazw).
Łącząc to wszystko, mówimy kompilatorowi, że jeśli zobaczy deklarację Pair z dwoma argumentami (z typy T i U ), powinien wywnioskować, że typem jest Pair<T, U>.
Więc kiedy kompilator zobaczy definicję Pair p2{ 1, 2 }; w naszym programie, powie: „och, to jest deklaracja Pair i istnieją dwa argumenty typu int i int, więc korzystając z przewodnika dedukcji, powinienem wywnioskować, że to jest Pair<int, int>“.
Oto podobny przykład pary, która przyjmuje pojedynczy typ szablonu:
template <typename T>
struct Pair
{
T first{};
T second{};
};
// Oto przewodnik po dedukcjach dla naszej para (wymagana tylko w C++17)
// Obiekty par zainicjowane argumentami typu T i T powinny wydedukować Pair<T>
template <typename T>
Pair(T, T) -> Pair<T>;
int main()
{
Pair<int> p1{ 1, 2 }; // jawnie określ szablon klasy Pair<int> (C++11 i nowsze)
Pair p2{ 1, 2 }; // CTAD użyte do wydedukowania Pair<int> z inicjatorów (C++17)
return 0;
}W tym przypadku nasz przewodnik po dedukcjach odwzorowuje Pair(T, T) (Pair z dwoma argumentami typu T) do Pair<T>.
Wskazówka
C++ 20 dodał możliwość kompilatora automatycznego generowania przewodników dedukcji dla agregatów, więc przewodniki po dedukcjach powinny być dostarczane jedynie w celu zapewnienia zgodności z C++ 17.
Z tego powodu wersja Pair bez przewodników dedukcyjnych powinna zostać skompilowana w C++ 20.
std::pair (i inne standardowe typy szablonów bibliotek) są dostarczane z predefiniowanymi przewodnikami dedukcyjnymi, dlatego też nasz powyższy przykład, w którym std::pair kompiluje się dobrze w C++ 17 bez konieczności samodzielnego dostarczania przewodników dedukcyjnych.
Dla zaawansowanych czytelników
Nieagregowane nie potrzebujesz przewodników dedukcyjnych w C++17, ponieważ obecność konstruktora służy temu samemu celowi.
Wpisz parametry szablonu z wartościami domyślnymi
Tak jak parametry funkcji mogą mieć domyślne argumenty, tak parametry szablonu mogą otrzymać wartości domyślne. Będą one używane, gdy parametr szablonu nie jest jawnie określony i nie można go wydedukować.
Oto modyfikacja naszego Pair<T, U> programu szablonu klasy powyżej, z parametrami szablonu typów T i U domyślnie ustawionymi na type int:
template <typename T=int, typename U=int> // domyślne T i U do wpisania int
struct Pair
{
T first{};
U second{};
};
template <typename T, typename U>
Pair(T, U) -> Pair<T, U>;
int main()
{
Pair<int, int> p1{ 1, 2 }; // jawnie określ szablon klasy Pair<int, int> (C++11 i nowsze)
Pair p2{ 1, 2 }; // CTAD użyte do wydedukowania Pair<int, int> z inicjatorów (C++17)
Pair p3; // uses default Pair<int, int>
return 0;
}Nasza definicja p3 nie określa jawnie typów parametrów szablonu typów, ani nie ma inicjatora dla tych typów, które można wydedukować od. Dlatego kompilator użyje typów określonych w ustawieniach domyślnych, co oznacza p3 będzie typu Pair<int, int>.
CTAD nie działa z inicjalizacją elementu niestatycznego
Podczas inicjowania elementu typu klasy przy użyciu niestatycznej inicjalizacji elementu członkowskiego, CTAD nie będzie działać w tym kontekście. Wszystkie argumenty szablonu muszą być wyraźnie określone:
#include <utility> // dla std::pair
struct Foo
{
std::pair<int, int> p1{ 1, 2 }; // ok, jawnie określono argumenty szablonu
std::pair p2{ 1, 2 }; // błąd kompilacji, CTAD nie można użyć w tym kontekście
};
int main()
{
std::pair p3{ 1, 2 }; // ok, można tu użyć CTAD
return 0;
}CTAD nie działa z parametrami funkcji
CTAD oznacza szablon klasy argument dedukcję, a nie szablon klasy parametr dedukcję, więc wydedukuje tylko typ argumentów szablonu, a nie parametry szablonu.
Dlatego CTAD nie można używać w parametrach funkcji.
#include <iostream>
#include <utility>
void print(std::pair p) // błąd kompilacji, nie można tu użyć CTAD
{
std::cout << p.first << ' ' << p.second << '\n';
}
int main()
{
std::pair p { 1, 2 }; // p wydedukowany do std::pair<int, int>
print(p);
return 0;
}W takich przypadkach należy zamiast tego użyć szablonu:
#include <iostream>
#include <utility>
template <typename T, typename U>
void print(std::pair<T, U> p)
{
std::cout << p.first << ' ' << p.second << '\n';
}
int main()
{
std::pair p { 1, 2 }; // p wydedukowany do std::pair<int, int>
print(p);
return 0;
}
