SFINAE - Substitution Failure Is Not An Error - enable_if¶
Jednym z podstawowych narzędzi w meta-programowaniu w C++ jest szablon enable_if wykorzystujący mechanizm SFINAE.
Motywacją dla stosowania enable_if
jest chęć dostarczenia dla
klientów uniwersalnego interfejsu, bez zmuszania ich do podejmowania
decyzji o wyborze określonych implementacji. Decyzje takie są
podejmowane przez implementorów bibliotek. Dobór optymalnych
implementacji w zależności od typów danych dokonywany jest na etapie
kompilacji.
Chcemy uniknąć kodu, który często wygląda tak:
// Don't even dare to pass an array of complex obejcts to this function!!!
template <typename T>
T* copy_it(T const* src, size_t n)
{
// impl
}
SFINAE¶
Substitition Failure Is Not An Error (SFINAE) jest mechanizmem wykorzystywanym przez kompilator w trakcie dedukcji typów dla argumentów szablonu. Szablony, dla których nie udaje się podstawić określonego typu jako typu argumentu szablonu są ignorowane i nie powodują błędów kompilacji.
W praktyce jeśli dedukcja typów argumentów dla szablonu funkcji znajdzie przynajmniej jedno dopasowanie, błędne podstawienia są usuwane z listy funkcji kandydujących do wywołania (overload resolution) i nie zgłaszają błędów kompilacji.
Załóżmy następującą implementację szablonów funkcji:
template <typename T> void foo(T arg)
{}
template<typename T> void foo(T* arg)
{}
Wywołanie foo(42)
powoduje utworzenie instancji szablonu funkcji
foo<int>(int)
. Ponieważ kompilatorowi nie udaje się podstawić
prawidłowo typu int
dla drugiej implementacji działa SFINAE -
nieudane podstawienie nie generuje błędów kompilacji a implementacja
znika z listy funkcji kandydujących do wywołania. Jeśli brakowałoby
pierwszej implementacji funkcji (tej prawidłowo dopasowanej) kompilator
zgłosiłby błąd.
Szablon enable_if¶
Rozważmy dwie implementacje funkcji z identycznymi interfejsami, ale różnymi wymaganiami odnośnie typu danych:
template <typename T>
void do_stuff(T t) // for small objects
{
//~~~
}
template <typename T>
void do_stuff(T const& t) // for large objects
{
//~~~
}
Sygnatury tych funkcji są zbyt podobne, aby można było zastosować
klasyczne przeciążenie funkcji. Rozwiązaniem tego problemu jest
zastosowanie szablonu enable_if
i mechanizmu SFINAE.
Typowa implementacja szablonu enable_if
wygląda następująco:
template <bool Condition, typename T = void>
struct enable_if
{
using type = T;
};
template <typename T>
struct enable_if<false, T>
{};
Szablon ogólny jest sparametryzowany wartością logiczną (zwykle
wyrażenie logiczne, które jest ewaluowane na etapie kompilacji) oraz
typem (domyślnie void
). Wewnątrz struktury definiowany jest typ
type
. W wersji specjalizowanej dla wartości logicznej false
takiej definicji typu brakuje.
- Jeśli wyrażenie
Condition
przekazane jako pierwszy argument szablonu ewaluowane jest do wartościtrue
:enable_if<Condition>
ma składowątype
- i
enable_if<Condition>::type
odnosi się do tego typu
- Jeśli wyrażenie
Condition
ewaluowane jest dofalse
:enable_if<Condition>
nie ma składowejtype
- i
enable_if<Condition>::type
jest nieprawidłowym odwołaniem - w rezultacie podstawienie nie udaje się (działa SFINAE)
W celu ułatwienia korzystania z enable_if
C++14 definiuje poniższy
alias szablonu:
template <bool Condition, typename T = void>
using enable_if_t = typename enable_if<Condition, T>::type;
Wykorzystanie szablonu enable_if
do problemu wyboru implementacji
wygląda następująco:
#include <iostream>
template <typename T>
typename enable_if<(sizeof(T) < 8)>::type do_stuff(T t) // for small objects
{
std::cout << "do_stuff(Small Object)\n";
}
template <typename T>
enable_if_t<(sizeof(T) > 8)> do_stuff(T const& t) // for large objects
{
std::cout << "do_stuff(Large Object)\n";
}
#include <string>
do_stuff('A'); // OK, small object
do_stuff(std::string("abc")); // OK, large object
do_stuff(Small Object)
do_stuff(Large Object)
W zależności od rozmiaru typu wydedukowanego w procesie tworzenia
instancji szablonu jest tworzona i później wywoływana odpowiednia
instancja szablonu funkcji do_stuff()
.
Próba utworzenia instancji dla typu double
zgłaszany jest błąd
kompilacji:
do_stuff(3.14);
error: no matching function for call to do_stuff
note: candidate template ignored: disabled by enable_if [with T = double]
Kompilator nie jest w stanie wygenerować żadnej instancji szablonu dla typu o rozmiarze 8 bajtów.
Mechanizm SFINAE zapewnia dwufazowe dopasowanie funkcji do wywołania (two-phase overload resolution):
- W fazie pierwszej
enable_if
i SFINAE eliminują funkcje kandydujące, dla których nie można zrealizować podstawienia - W fazie drugiej dopasowana może zostać tylko jedna funkcja z grupy funkcji kandydujących do wywołania
Cechy typów i enable_if¶
Często jako w wyrażeniu logicznym, będącym pierwszym argumentem szablonu
enable_if
, wykorzystywane są cechy typów:
#include <type_traits>
template <typename T>
std::enable_if_t<std::is_pod<T>::value> store_item(T const& item)
{
// do something when T is POD
}
template <typename T>
std::enable_if_t<!std::is_pod<T>::value> store_item(T const& item)
{
// do something when T is not POD
}
SFINAE może zostać użyte również do wprowadzenia ograniczeń jeśli chodzi i zakres typów, dla których realizowane jest tworzenie instancji szablonu:
struct Shape
{
//...
};
struct Rectangle : Shape
{
//...
};
template <typename T>
enable_if_t<std::is_base_of<Shape, T>::value> do_stuff_with_shape(T const& shape)
{
//...
}
Domyślne argumenty szablonów funkcji¶
W C++11 argumenty szablonów funkcji mogą przyjmować wartości domyślne.
Pozwala to na czytelniejszą dla programisty implementację SFINAE z
enable_if
:
template <
typename T,
typename = std::enable_if_t<std::is_base_of<Shape, T>::value>
>
void do_other_stuff_with_shape(T const& shape)
{
//...
}
Aby podnieść jeszcze bardziej czytelność kodu możemy zdefiniować pomocniczy alias:
template <typename T>
using IsShape = std::enable_if_t<std::is_base_of<Shape, T>::value>;
i wykorzystać go do implementacji szablonu funkcji z ograniczeniem SFINAE:
template <
typename T,
typename = IsShape<T>
>
void do_other_stuff_with_shape_alt(T const& shape)
{
//...
}
Ograniczenia w szablonach klas¶
SFINAE oraz enable_if
mogą również zostać użyte dla szablonów klas.
Możemy na przykład ograniczyć możliwość tworzenia instancji szablonów dla typów zmiennoprzecinkowych:
template <typename T>
using FloatingPoint = std::enable_if_t<std::is_floating_point<T>::value>;
template <
typename T,
typename = FloatingPoint<T>
>
class Data
{
//...
};
Data<double> d1;
(Data<double> &) @0x7faca2165022
Data<int> d2;
error: no type named type in std::enable_if<false, void>
SFINAE i przeciążone konstruktory¶
SFINAE może rozwiązać problemy związane z przeciążonymi konstruktorami klas i ich czasami zaskakującym zachowaniem.
Załóżmy, że implementujemy klasę Heap
, która potrzebuje dwóch wersji
konstruktorów:
#include <iostream>
template <typename T>
class Heap
{
public:
Heap(size_t n, T const& v)
{
std::cout << "Heap(size_t, T)\n";
}
template <typename InIt>
Heap(InIt start, InIt end) // range init using pair of input iterators
{
std::cout << "Heap(InIt, InIt)" << std::endl;
}
};
Obecność konstruktora definiowanego jako szablon może dać zaskakujący rezultat:
Heap<int> h(5, 0); // range init constructor called
Heap(InIt, InIt)
Powyższe kod spowoduje wywołanie konstruktora przyjmującego jako argumenty parę iteratorów, ponieważ ta wersja konstruktora gwarantuje lepsze dopasowanie argumentów.
Możemy uniknąć takiego zachowania wprowadzając ograniczenie dla konstruktora wykorzystującego iteratory:
#include <iterator>
template <typename It>
using Category = typename std::iterator_traits<It>::iterator_category;
template <typename It>
using InputIterator = std::is_base_of<std::input_iterator_tag, Category<It>>;
template <typename It>
using IsInputIterator_t = std::enable_if_t<InputIterator<It>::value>;
namespace Sfinae
{
template <typename T>
class Heap
{
public:
Heap(size_t n, T const& v)
{
std::cout << "Heap(size_t, T)\n";
}
template <
typename InIt,
typename = IsInputIterator_t<InIt>
>
Heap(InIt start, InIt end) // range init using pair of input iterators
{
std::cout << "Heap(InIt, InIt)" << std::endl;
}
};
}
Teraz przy wywołaniu
Sfinae::Heap<int>(5, 0);
Heap(size_t, T)
konstruktor z iteratorami znika z funkcji kandydujących do wywołania.
Natomiast, gdy przekażemy konstruktorowi zakres zdefiniowany przez iteratory, to odpowiednia wersja konstruktora jest instancjonowana i później wywołana.
auto range = { 1, 2, 3 };
Sfinae::Heap<int>(range.begin(), range.end());
Heap(InIt, InIt)