7 lutego 2020

[C++11/17] enable_if vs. if constexpr

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]

Brak komentarzy:

Prześlij komentarz