Szablony w C++17¶
Dedukcja argumentów szablonu dla klas¶
C++17 wprowadza mechanizm dedukcji argumentów szablonu klasy (Class Template Argument Deduction).
Typy parametrów szablonu klasy mogą być dedukowane na podstawie argumentów przekazanych do konstruktora tworzonego obiektu.
Wcześniej mechanizm dedukcji typu był dostępny tylko dla szablonów funkcji i deklaracji auto
.
Dla klasy szablonowej:
template <typename T1, typename T2>
struct ValuePair
{
T1 fst;
T2 snd;
ValuePair(T1 f, T2 s) : fst{f}, snd{s}
{
}
};
Mechanizm dedukcji typów argumentów klasy szablonowej umożliwia prostsze tworzenie instancji. Zamiast jawnie specyfikować argumenty szablonu, możemy kompilatorowi zlecić dedukcję na podstawie wywołania konstruktora klasy:
ValuePair<int, double> vp1(1, 3.14); // OK - all versions of standard
ValuePair vp2(1, 3.14); // OK in C++17 - deduces ValuePair<int, double>
ValuePair vp3{1, 3.14); // OK in C++17 - deduced ValuePair<int, double>
auto vp4 = ValuePair(1, "text"); // OK in C++17 - deduces ValuePair<int, const char*>
auto vp5 = ValuePair{3.14, "pi"s); // OK in C++17 - deduces ValuePair<int, std::string>
Ostrzeżenie
Nie można częściowo dedukować argumentów szablonu klasy. Należy wyspecyfikować lub wydedukować wszystkie parametry z wyjątkiem parametrów domyślnych.
ValuePair vp6; // ERROR - T1 & T2 are undefined - deduction fails
ValuePair<int> vp7{1, 3.14}; // ERROR - partial deduction is not allowed
Inny przykład dedukcji typu argumentów dla szablony klasy:
template <typename T1 = int, typename T2 = T1>
struct Generic
{
T1 fst;
T2 snd;
explicit Generic(T1 f = T1{}, T2 s = T2{}) : fst{f}, snd{s}
{}
};
Generic<int, double> g0{1, 3.14}; // no deduction
Generic g1{1, 3.14};
static_assert(is_same_v<decltype(g1), Generic<int, double>>);
Generic<double> g2{3.14, 1}; // no deduction
static_assert(is_same_v<decltype(g2), Generic<double, double>>);
Generic g3{3.14};
static_assert(is_same_v<decltype(g3), Generic<double, double>>);
Generic<> g4; // no deduction
static_assert(is_same_v<decltype(g4), Generic<int, int>>);
Generic g5{};
static_assert(is_same_v<decltype(g5), Generic<int, int>>);
Stosowanie mechanizmu dedukcji argumentów szablonu klasy¶
Stosując mechanizm dedukcji argumentów szablonu klasy możemy zrezygnować ze stosowania funkcji pomocniczych definiowanych jako szablony funkcji. Zamiast tworzyć parę przy pomocy:
auto p1 = std::make_pair(1, 3.14);
auto p2 = std::pair<int, std::string>(1, "one");
Możemy napisać:
using namespace std::string_literals;
std::pair p1{1, 3.14};
std::pair p2{1, "one"s};
Inny praktyczny przykład dedukcji argumentów szablonu klasy:
std::timed_mutex mtx_one;
std::shared_mutex mtx_two;
std::scoped_lock lk{mtx_one, mtx_two}; // deduces std::scoped_lock<std::timed_mutex, std::shared_mutex>
Specjalny przypadek dedukcji argumentów klasy szablonowej¶
Jeżeli kod służący do dedukcji argumentów szablonu klasy może być zinterpretowany jako przypadek inicjalizacji poprzez kopię, to kompilator preferuje taką interpretację.
Rozważmy następujący przypadek dedukcji:
std::vector v{1, 2, 3}; // vector<int>
std::vector data1{v, v}; // vector<vector<int>>
Lecz w przypadku, gdy składnia inicjalizacji obiektu pasuje do wywołania konstruktora kopiującego, wtedy działa specjalna reguła dedukcji argumentu szablonu klasy:
std::vector data2{v}; // vector<int>!
Informacja
W powyższym kodzie dedukcja argumentów szablonu vector
zależy od ilości argumentów przekazanych do konstruktora!
Podpowiedzi dedukcyjne (deduction guides)¶
C++17 umożliwia tworzenie podpowiedzi dla kompilatora, jak powinny być dedukowane typy parametrów szablonu klasy na podstawie wywołania odpowiedniego konstruktora.
Daje to możliwość poprawy/modyfikacji domyślnego procesu dedukcji.
Dla szablonu:
template <typename T>
class S
{
private:
T value;
public:
S(T v) : value(v)
{}
};
Podpowiedź dedukcyjna musi zostać umieszczona w tym samym zakresie (przestrzeni nazw) i może mieć postać:
template <typename T> S(T) -> S<T>; // deduction guide
S x{12}; // OK -> S<int> x{12};
S y(12); // OK -> S<int> y(12);
auto z = S{12}; // OK -> auto z = S<int>{12};
S s1(1), s2{2}; // OK -> S<int> s1(1), s2{2};
S s3(42), s4{3.14}; // ERROR
gdzie:
S<T>
to tzw. typ zalecany (guided type)- nazwa podpowiedzi dedukcyjnej musi być niekwalifikowaną nazwą klasy szablonowej zadeklarowanej wcześniej w tym samym zakresie
- typ zalecany podpowiedzi musi odwoływać się do identyfikatora szablonu (template-id), do którego odnosi się podpowiedź
W deklaracji S x{12};
specyfikator S
jest nazywany symbolem zastępczym dla klasy (placeholder class type).
W przypadku użycia symbolu zastępczego dla klasy, nazwa zmiennej musi zostać podana jako następny element składni.
W rezultacie poniższa deklaracja jest błędem składniowym:
S* p = &x; // ERROR - syntax not permitted
Dany szablon klasy może mieć wiele konstruktorów oraz wiele podpowiedzi dedukcyjnych:
template <typename T>
struct Data
{
T value;
using type1 = T;
Data(const T& v)
: value(v)
{
}
template <typename ItemType>
Data(initializer_list<ItemType> il)
: value(il)
{
}
};
template <typename T>
Data(T)->Data<T>;
template <typename T>
Data(initializer_list<T>)->Data<vector<T>>;
Data(const char*) -> Data<std::string>;
//...
Data d1("hello"); // OK -> Data<string>
const int tab[10] = {1, 2, 3, 4};
Data d2(tab); // OK -> Data<const int*>
Data d3 = 3; // OK -> Data<int>
Data d4{1, 2, 3, 4}; // OK -> Data<vector<int>>
Data d5 = {1, 2, 3, 4}; // OK -> Data<vector<int>>
Data d6 = {1}; // OK -> Data<vector<int>>
Data d7(d6); // OK - copy by default rule -> Data<vector<int>>
Data d8{d6, d7}; // OK -> Data<vector<Data<vector<int>>>>
Podpowiedzi dedukcyjne nie są szablonami funkcji - służą jedynie dedukowaniu argumentów szablonu i nie są wywoływane. W rezultacie nie ma znaczenia czy argumenty w deklaracjach dedukcyjnych są przekazywane przez referencję, czy nie.
template <typename T>
struct X
{
//...
};
template <typename T>
struct Y
{
Y(const X<T>&);
Y(X<T>&&);
};
template <typename T> Y(X<T>) -> Y<T>; // deduction guide without references
W powyższym przykładzie podpowiedź dedukcyjna nie odpowiada dokładnie sygnaturom konstruktorów przeciążonych. Nie ma to znaczenia, ponieważ jedynym celem podpowiedzi jest umożliwienie dedukcji typu, który jest parametrem szablonu. Dopasowanie wywołania przeciążonego konstruktora odbywa się później.
Podpowiedzi dedukcyjne vs. Konstruktory¶
Podpowiedzi dedukcyjne rywalizują z konstruktorami klasy. Mechanizm dedukcji wykorzystuje konstruktor lub podpowiedź, która ma najwyższy priorytet zgodnie z regułami przeciążania funkcji. Jeśli konstruktor i podpowiedź pasują jednakowo dobrze, kompilator preferuje podpowiedź dedukcyjną.
Dla szablonu klasy:
template <typename T>
struct Thing
{
Thing(const T&)
{
}
};
Thing(int) -> Thing<long>;
Przy wywołaniu:
Thing t1{42}; // T deduced as long
preferowana jest podpowiedź dedukcyjna.
Ale, gdy w wywołaniu konstruktora użyjemy typu char
:
Thing t2{'a'}; // deduced as char
preferowany do dedukcji argumentu szablonu jest konstruktor (ponieważ nie jest wymagana konwersja typu).
Ponieważ przekazanie argumentu przez wartość, jest dopasowywane równie dobrze co przekazanie argumentu przez referencję oraz biorąc pod uwagę fakt, że podpowiedź dedukcyjna jest preferowana przy równie dobrym dopasowaniu, najczęściej wystarczy w sygnaturze podpowiedzi przekazać parametry przez wartość.
Niejawne podpowiedzi dedukcyjne¶
Ponieważ często podpowiedź dedukcyjna jest potrzebna dla każdego konstruktora klasy, standard C++17 wprowadza mechanizm niejawnych podpowiedzi dedukcyjnych (implicit deduction guides). Działa on w następujący sposób:
- Lista parametrów szablonu dla podpowiedzi zawiera listę parametrów z szablonu klasy - w przypadku szablonowego konstruktora klasy kolejnym elementem jest lista parametrów szablonu konstruktora klasy
- Parametry „funkcyjne” podpowiedzi są kopiowane z konstruktora lub konstruktora szablonowego
- Zalecany typ w podpowiedzi jest nazwą szablonu z argumentami, które są parametrami szablonu wziętymi z klasy szablonowej
Dla klasy szablonowej rozważanej powyżej:
template <typename T>
class S
{
private:
T value;
public:
S(T v) : value(v)
{}
};
niejawna podpowiedź dedukcyjna będzie wyglądać następująco:
template <typename T> S(T) -> S<T>; // implicit deduction guide
W rezultacie programista nie musi implementować jej jawnie.
Agregaty a dedukcja argumentów¶
Jeśli szablon klasy jest agregatem, to mechanizm automatycznej dedukcji argumentów szablonu wymaga napisania jawnej podpowiedzi dedukcyjnej.
Bez podpowiedzi dedukcyjnej dedukcja dla agregatów nie działa:
template <typename T>
struct Aggregate1
{
T value;
};
Aggregate1 agg1{8}; // ERROR
Aggregate1 agg2{"eight"}; // ERROR
Aggregate1 agg3 = 3.14; // ERROR
Gdy napiszemy dla agregatu podpowiedź, to możemy zacząć korzystać z mechanizmu dedukcji:
template <typename T>
struct Aggregate2
{
T value;
};
template <typename T>
Aggregate2(T) -> Aggregate2<T>;
Aggregate2 agg1{8}; // OK -> Aggregate2<int>
Aggregate2 agg2{"eight"}; // OK -> Aggregate2<const char*>
Aggregate2 agg3 = { 3.14 }; // OK -> Aggregate2<double>
Podpowiedzi dedukcyjne w bibliotece standardowej¶
Dla wielu klas szablonowych z biblioteki standardowej dodano podpowiedzi dedukcyjne w celu ułatwienia tworzenia instancji tych klas.
std::pair<T>¶
Dla pary STL dodana w standardzie podpowiedź to:
template<class T1, class T2>
pair(T1, T2) -> pair<T1, T2>;
pair p1(1, 3.14); // -> pair<int, double>
pair p2{3.14f, "text"s}; // -> pair<float, string>
pair p3{3.14f, "text"}; // -> pair<float, const char*>
int tab[3] = { 1, 2, 3 };
pair p4{1, tab}; // -> pair<int, int*>
std::tuple<T…>¶
Szablon std::tuple
jest traktowany podobnie jak std::pair
:
template<class... UTypes>
tuple(UTypes...) -> tuple<UTypes...>;
template<class T1, class T2>
tuple(pair<T1, T2>) -> tuple<T1, T2>;
//... other deduction guides working with allocators
int x = 10;
const int& cref_x = x;
tuple t1{x, &x, cref_x, "hello", "world"s}; // -> tuple<int, int*, int, const char*, string>
std::optional<T>¶
Klasa std::optional
jest traktowana podobnie do pary i krotki.
template<class T> optional(T) -> optional<T>;
optional o1(3); // -> optional<int>
optional o2 = o1; // -> optional<int>
Inteligentne wskaźniki¶
Dedukcja dla argumentów konstruktora będących wskaźnikami jest zablokowana:
int* ptr = new int{5};
unique_ptr uptr{ip}; // ERROR - ill-formed (due to array type clash)
Wspierana jest dedukcja przy konwersjach:
z
weak_ptr
/unique_ptr
doshared_ptr
:template <class T> shared_ptr(weak_ptr<T>) -> shared_ptr<T>; template <class T, class D> shared_ptr(unique_ptr<T, D>) -> shared_ptr<T>;
z
shared_ptr
doweak_ptr
template<class T> weak_ptr(shared_ptr<T>) -> weak_ptr<T>;
unique_ptr<int> uptr = make_unique<int>(3);
shared_ptr sptr = move(uptr); -> shared_ptr<int>
weak_ptr wptr = sptr; // -> weak_prt<int>
shared_ptr sptr2{wptr}; // -> shared_ptr<int>
std::function¶
Dozwolone jest dedukowanie sygnatur funkcji dla std::function
:
int add(int x, int y)
{
return x + y;
}
function f1 = &add;
assert(f1(4, 5) == 9);
function f2 = [](const string& txt) { cout << txt << " from lambda!" << endl; };
f2("Hello");
Kontenery i sekwencje¶
Dla kontenerów standardowych dozwolona jest dedukcja typu kontenera dla konstruktora akceptującego parę iteratorów:
vector<int> vec{ 1, 2, 3 };
list lst(vec.begin(), vec.end()); // -> list<int>
Dla std::array
dozwolona jest dedukcja z sekwencji:
std::array arr1 { 1, 2, 3 }; // -> std::array<int, 3>
Deklaracje using w variadic templates¶
W C++17 dodano możliwość wygodniejszego użycia deklaracji using
w przypadku
rozpakowywania paczki parametrów (parameter pack expansion) w szablonie wariadycznym.
#include <string>
#include <unordered_set>
class Customer
{
std::string name_;
public:
Customer(const std::string& name) : name_{name}
{}
std::string name() const
{
return name_;
}
};
struct CustomerEq
{
bool operator()(const Customer& c1, const Customer& c2) const
{
return c1.name() == c2.name();
}
};
struct CustomerHash
{
bool operator()(const Customer& c) const
{
return std::hash<std::string>{}(c.name());
}
};
// overloader
template <typename... Ts>
struct Overloader : Ts...
{
using Ts::operator()...; // since C++17
};
using CustomerComparer = Overloader<CustomerEq, CustomerHash>;
unordered_set<Customer, CustomerComparer, CustomerComparer> collection;
Parametry szablonu nie będące typami ze specyfikatorem auto
¶
C++17 wprowadza możliwość zadeklarowania parametru szablonu nie będącego typem jako auto
lub decltype(auto)
.
W rezultacie typ stałej jest automatycznie dedukowany wg odpowiedniego mechanizmu.
template <auto N>
struct S
{
//...
};
S<42> s1; // -> N in S is int
S<'a'> s2; // -> N in S is char
S<3.14> s3; // ERROR - template parametr type still cannot be double
// partial specialization
template <int N> struct S<N>
{
};
// list of heterogenous constant template arguments
template <auto... CS> struct ValueList { };
// list of homogenous constant template arguments
template <auto C, decltype(C)... CS> struct ValueList { };
Przykład szablonu z parametrem specyfikowanym jako decltype(auto)
:
template <decltype(auto) N>
struct S
{
S()
{
cout << "N has value: " << N << endl;
cout << "type of N is int&: " << is_same_v<decltype(N), int&> << endl;
}
void print()
{
cout << "N has value: " << N << endl;
}
};
constexpr auto x = 665;
int y{};
S<x> s0; // N is int
S<(y)> s1; // N is int& => prints: 'N has value 0'
y = 77;
S<(y)> s2; // N is int& => prints: 'N has value 77'
y = 88;
s1.print(); // prints: 'N has value 88'
s2.print(); // prints: 'N has value 88'
Variable templates ze specyfikatorem auto¶
// variable.hpp
#include <array>
template <typename T, auto N> std::array<T, N> FixedArray; // OK - since C++17
template <auto N> constexpr decltype(N) value = N; // OK - since C++17
// in test1.cpp
#include "variable.hpp"
void print();
int main()
{
FixedArray<double, 100u>[0] = 17;
FixedArray<int, 10>[0] = 42;
print();
std::cout << value<'c'> << "\n";
}
// in test2.cpp
#include "variable.hpp"
void print()
{
std::cout << FixedArray<double, 100u>[0] << std::endl;
for(auto i = 0u; i < FixedArray<int, 10>.size(); ++i)
{
cout << FixedArray<int, 10>[i] << std::endl;
}
}