enable_if pojawiło się w C++11 i miało za zadanie uelastycznić mechanizm metaprogramowania w szablonach, pozwalając na tworzenie specjalizacji typu, tylko gdy typ spełniał określone warunki. Wraz z
type_traits dało się stworzyć naprawdę trudny w zrozumieniu i utrzymaniu kod. Obecnie sam
core guadline zaleca, aby jego używanie ograniczyć do minimum, wymieniając
enable_if jako
przykład złego kodu. Na szczęście wszystko wskazuje na to, że społeczność chce wreszcie
zabić nieszczęsne metaprogramowanie w szablonach. Do tego jeszcze jednak długa droga.
Oryginalnym mechanizmem metaprogramowania w C++ były znane z języka C makra, lecz pewnego dnia,
Erwin Unruh, przez
przypadek odkrył mechanizm dziś znany jako
SFINAE (Substitution Failure Is Not An Error). W skrócie, jeżeli kompilatorowi nie uda się wyspecjalizować szablonu będzie próbował dalej. W jego przykładzie wywołując rekursywną specjalizację szablonów, dokonał obliczeń w czasie kompilacji, choć kompilacja zakończyła się niepowodzeniem. Być może, właśnie przez to, że mechanizm ten pojawił się przypadkiem, a nie został wprowadzony z premedytacją, nie jest on przemyślany składniowo, sprawia że wiele osób odrzuca i dzieli społeczność C++ na dwa obozy.
Przykłady poniżej oparłem na prezentacji Nicolai Josuttis -
C++17 - The Best Features. Na początek wersja kodu wykorzystująca
enable_if przez zwracany typ (drugi parametr - w naszym przypadku
std::string).
#include <iostream>
#include <type_traits>
using namespace std;
template<typename T>
std::enable_if_t<std::is_arithmetic_v<T>,
std::string>
as_string1(T x) {
return std::to_string(x) + " [is_arithmetic_v]";
}
template<typename T>
std::enable_if_t<std::is_same_v<T, std::string>,
std::string>
as_string1(T x) {
return x + " [is_same_v]";
}
template<typename T>
std::enable_if_t<!std::is_same_v<T, std::string> && !std::is_arithmetic_v<T>,
std::string>
as_string1(T x) {
return std::string(x) + " [!is_same_v && !is_arithmetic_v]";
}
int main() {
cout << as_string1(11) << endl;
cout << as_string1(std::string("12")) << endl;
cout << as_string1("13") << endl;
}
Wynik:
11 [is_arithmetic_v]
12 [is_same_v]
13 [!is_same_v && !is_arithmetic_v]
Inna wersja, gdzie
enable_if pojawia się jako parametr szablonowy. Domyślne argumenty szablonowe, nie są częścią sygnatury funkcji szablonowej, trzeba więc do
enable_if przekazać jeszcze dummy typ (
int), nie wiem czemu trzeba przypisać do tego 0. Dziwny hack.
#include <iostream>
#include <type_traits>
using namespace std;
template<typename T, std::enable_if_t<std::is_arithmetic_v<T>,
int> = 0>
std::string as_string2(T x) {
return std::to_string(x) + " [is_arithmetic_v]";
}
template<typename T, std::enable_if_t<std::is_same_v<T, std::string>,
int> = 0>
std::string as_string2(T x) {
return x + " [is_same_v]";
}
template<typename T, std::enable_if_t<!std::is_same_v<T, std::string> && !std::is_arithmetic_v<T>,
int> = 0>
std::string as_string2(T x) {
return std::string(x) + " [!is_same_v && !is_arithmetic_v]";
}
int main() {
cout << as_string2(21) << endl;
cout << as_string2(std::string("22")) << endl;
cout << as_string2("23") << endl;
}
Wynik:
21 [is_arithmetic_v]
22 [is_same_v]
23 [!is_same_v && !is_arithmetic_v]
Są jeszcze inne formy zapisu
enable_if np. jako
parametr funkcji, ale w tej chwili nie jest to istotne. W C++17 pojawił się ciekawy mechanizm
if constexpr, który pozwala radzić sobie z dużą liczbą takich specjalizacji, a kod wygląda znacznie bardziej czytelnie.
#include <iostream>
#include <type_traits>
using namespace std;
template<typename T>
std::string as_string3(T x) {
if constexpr(std::is_arithmetic_v<T>) {
return std::to_string(x) + " [is_arithmetic_v]";
} else if constexpr(std::is_same_v<T, std::string>) {
return x + " [is_same_v]";
} else {
return std::string(x) + " [!is_same_v && !is_arithmetic_v]";
}
}
int main() {
cout << as_string3(31) << endl;
cout << as_string3(std::string("32")) << endl;
cout << as_string3("33") << endl;
}
Wynik:
31 [is_arithmetic_v]
32 [is_same_v]
33 [!is_same_v && !is_arithmetic_v]