ZWI#3 – jak wywołać specjalizację szablonu funkcji dla wszystkich typów z listy

Tym razem podzielę się pytaniem jakie otrzymałem na IRC-u:

<cauchy> mam sobie funkcyjke z templejtem
<cauchy> i chce sobie ta fn wywolac dla jakiejs tam listy typow
<cauchy> np dla int, in64_t, string, vector<int>, vector<string>, itd
<cauchy> jakos fajnie daloby sie to zrobic w petli zebym nie musial explicitnie dawac po kolei
<cauchy> fn<int>();
<cauchy> fn<string>();
<cauchy> …

Oczywiście, że by się dało! Jest nawet kilka sposobów. Zacznijmy od tego, który przedstawiłem jako rozwiązanie problemu:

Lekkie nadużycie listy inicjalizacyjnej i dedukcji typów

template<typename T>
void foo()
{
    BARK;
}
 
template<typename... Ts>
void caller(std::tuple<Ts...>* = nullptr)
{
    int arr[]{
        (foo<Ts>(), 0)...
    };
    (void)arr;
}
 
int main()
{
    using type_list = std::tuple<int, double, string, thread, vector<int>>*;
 
    caller(type_list{});
}

[wandbox]

Co tutaj się dzieje? Sprawa jest całkiem prosta: do funkcji caller() przekazujemy instancję typu type_list1. Jest to wskaźnik na std::tuple, którego lista argumentów jest listą typów, dla których chcemy wywołać docelowy szablon funkcji foo(). Dzięki temu, że używamy wskaźnika, nie ma konieczności inicjalizacji żadnego z tych typów (co by było kłopotliwe dla typów czysto abstrakcyjnych lub wymagających specjalnej konstrukcji) – przekazujemy tylko domyślnie zainicjalizowany wskaźnik (równy nullptr), jego jedyną istotną cechą jest jego typ.

Podczas rozwiązywania przeładowań kompilator sprawdza wszystkie funkcje2 caller(), w przypadku szablonów funkcji stosując pattern matching i sprawdzając, czy sygnatura funkcji pasuje do podanych argumentów. W tym przypadku funkcja caller() jest tylko jedna i w jej przypadku podstawienie się udaje: przyjmuje ona std::tuple<Ts…>*, a przekazanym argumentem jest:
std::tuple<int, double, string, thread, vector<int>>.
Wobec tego parametrami szablonu są:
int, double, string, thread, vector<int>
Dzięki takiemu wyłuskaniu możemy się do nich odnosić pojedynczo.

Mając już w funkcji paczkę typów, możemy ją rozszerzyć za pomocą operatora . Niestety, nie da się po prostu napisać foo<Ts>()…; – nie jest to poprawny kod. Można za to zauważyć, że szablonową paczkę typów można rozwijać wewnątrz inicjalizatorów obiektów, np. tablic. Pozostaje tutaj tylko jeden problem: foo() z naszego przykładu zwraca void, a to nie jest typ, którym można cokolwiek zainicjalizować. Na szczęście ten problem można łatwo ominąć stosując operator,, którego wartością jest jego prawy operand.

int arr[]{
    (foo<Ts>(), 0)...
};

Powyższy kod utworzy sizeof…(Ts)-elementową tablicę, zainicjalizowaną zerami.

W przypadku bardziej generycznego kodu, powinniśmy zabezpieczyć się przed patologicznymi typami zwracanymi przez foo(), które mogłyby mieć przeładowany operator,, powodujący błąd kompilacji przy wywołaniu w tym kontekście. Na szczęście to jest proste – wystarczy rzutować wynik na void:

int arr[]{
    ((void)foo<Ts>, 0)...
};

Następna linia, (void)arr;, jest tam tylko po to aby uciszyć narzekanie kompilatora na nieużytą nigdzie tablicę arr. Dobrze to wiemy, ale była nam niezbędna.

Zaletą tego sposobu jest niewielkie zagnieżdżenie wywołań, nawet dla dużych list typów, oraz wsparcie wśród nawet starych kompilatorów.

Pseudo rekurencja

Tak naprawdę zmienia się tylko implementacja caller():

void caller(std::tuple<>*){}
 
template<typename T, typename... Us>
void caller(std::tuple<T, Us...>* = nullptr)
{
    foo<T>();
 
    using tail = std::tuple<Us...>*;
 
    caller(tail{});
}

[wandbox]

Myślę, że tu sprawa jest dość oczywista. Dedukujemy pierwszy typ do osobnego argumentu, aby można było go użyć do wywołania foo(), a resztę przekazujemy do innej instancji caller(). “Rekurencja” zakończy się, gdy skończą nam się typy w ogonie, i wywołamy caller z std::tuple<>* jako argumentem.

W C++17 można to uprościć z if constexpr:

template<typename T, typename... Us>
void caller(std::tuple<T, Us...>* = nullptr)
{
    foo<T>();
    if constexpr(sizeof...(Us) > 0) {
        using tail = std::tuple<Us...>*;
        caller(tail{});
    }
}

Fold expressions

C++17 jest obecnym standardem, więc bezproblemowo można użyć fold expressions. Dzięki zastosowaniu operatora ,, cała funkcja sprowadza się do:

template<typename... Ts>
void caller(std::tuple<Ts...>* = nullptr)
{
    ((void)foo<Ts>(), ...);
}

[wandbox]
1Prawdopodobnie ładniej by było stworzyć własną klasę type_list, ale na dobrą sprawę nie ma to znaczenia, ponieważ nie dochodzi do żadnych inicjalizacji.
2Oraz szablony, oraz obiekty… To jest temat na książkę. Serio.

Leave a Reply

Your email address will not be published.