Szukaj…


Wprowadzenie

W C ++ Metaprogramowanie odnosi się do użycia makr lub szablonów do generowania kodu w czasie kompilacji.

Ogólnie rzecz biorąc, makra nie są mile widziane w tej roli i szablony są preferowane, chociaż nie są tak ogólne.

Metaprogramowanie szablonów często wykorzystuje obliczenia w czasie kompilacji, zarówno przy użyciu szablonów, jak i funkcji constexpr , aby osiągnąć swoje cele w generowaniu kodu, jednak obliczenia w czasie kompilacji same w sobie nie są metaprogramowaniem.

Uwagi

Metaprogramowanie (a dokładniej Metaprogramowanie szablonów) to praktyka polegająca na stosowaniu szablonów do tworzenia stałych, funkcji lub struktur danych w czasie kompilacji. Umożliwia to wykonywanie obliczeń raz w czasie kompilacji, a nie w każdym czasie wykonywania.

Obliczanie czynników czynnikowych

Czynniki można obliczać w czasie kompilacji przy użyciu technik metaprogramowania szablonu.

#include <iostream>

template<unsigned int n>
struct factorial
{
    enum
    {
        value = n * factorial<n - 1>::value
    };
};

template<>
struct factorial<0>
{
    enum { value = 1 };
};

int main()
{
    std::cout << factorial<7>::value << std::endl;    // prints "5040"
}

factorial jest strukturą, ale w metaprogramowaniu szablonu jest traktowana jako metafunkcja szablonu. Zgodnie z konwencją, metafunkcje szablonu są oceniane przez sprawdzenie konkretnego elementu, albo ::type dla metafunkcji, które dają typy, lub ::value dla metafunkcji, które generują wartości.

W powyższym kodzie oceniamy metafunkcję factorial tworząc instancję szablonu z parametrami, które chcemy przekazać, i używając ::value aby uzyskać wynik oceny.

Sama metafunkcja polega na rekurencyjnym tworzeniu tej samej metafunkcji przy użyciu mniejszych wartości. Specjalizacja factorial<0> reprezentuje warunek zakończenia. Metaprogramowanie szablonów ma większość ograniczeń funkcjonalnego języka programowania , więc rekursja jest podstawową konstrukcją „zapętlającą”.

Ponieważ metafunkcje szablonu są wykonywane w czasie kompilacji, ich wyniki można wykorzystać w kontekstach wymagających wartości czasu kompilacji. Na przykład:

int my_array[factorial<5>::value];

Tablice automatyczne muszą mieć rozmiar zdefiniowany w czasie kompilacji. Wynik metafunkcji jest stałą czasową kompilacji, więc można ją tutaj wykorzystać.

Ograniczenie : Większość kompilatorów nie zezwala na głębokość rekurencji poza limit. Na przykład kompilator g++ domyślnie ogranicza rekurencję do 256 poziomów. W przypadku g++ programista może ustawić głębokość rekurencji za pomocą opcji -ftemplate-depth-X .

C ++ 11

Od C ++ 11 szablon std::integral_constant może być używany do tego rodzaju obliczeń szablonów:

#include <iostream>
#include <type_traits>

template<long long n>
struct factorial :
  std::integral_constant<long long, n * factorial<n - 1>::value> {};

template<>
struct factorial<0> :
  std::integral_constant<long long, 1> {};

int main()
{
    std::cout << factorial<7>::value << std::endl;    // prints "5040"
}

Dodatkowo funkcje constexpr stają się czystszą alternatywą.

#include <iostream>

constexpr long long factorial(long long n)
{
  return (n == 0) ? 1 : n * factorial(n - 1);
}

int main()
{
  char test[factorial(3)];
  std::cout << factorial(7) << '\n';
}

Treść factorial() jest zapisywana jako pojedyncza instrukcja, ponieważ w C ++ 11 funkcje constexpr mogą używać tylko dość ograniczonego podzbioru języka.

C ++ 14

Od wersji C ++ 14 constexpr wiele ograniczeń dla funkcji constexpr i można je teraz pisać o wiele wygodniej:

constexpr long long factorial(long long n)
{
  if (n == 0)
    return 1;
  else
    return n * factorial(n - 1);
}

Lub nawet:

constexpr long long factorial(int n)
{
  long long result = 1;
  for (int i = 1; i <= n; ++i) {
    result *= i;
  }
  return result;
}
C ++ 17

Od wersji c ++ 17 można użyć wyrażenia krotnie do obliczenia silni:

#include <iostream>
#include <utility>

template <class T, T N, class I = std::make_integer_sequence<T, N>>
struct factorial;

template <class T, T N, T... Is>
struct factorial<T,N,std::index_sequence<T, Is...>> {
   static constexpr T value = (static_cast<T>(1) * ... * (Is + 1));
};

int main() {
   std::cout << factorial<int, 5>::value << std::endl;
}

Iteracja po pakiecie parametrów

Często musimy wykonać operację na każdym elemencie w pakiecie parametrów szablonu variadic. Można to zrobić na wiele sposobów, a rozwiązania stają się łatwiejsze do czytania i pisania w C ++ 17. Załóżmy, że chcemy po prostu wydrukować każdy element w paczce. Najprostszym rozwiązaniem jest powtórzenie:

C ++ 11
void print_all(std::ostream& os) {
    // base case
}

template <class T, class... Ts>
void print_all(std::ostream& os, T const& first, Ts const&... rest) {
    os << first;
    
    print_all(os, rest...);
}

Zamiast tego moglibyśmy użyć sztuczki ekspandera, aby wykonać wszystkie transmisje strumieniowe w jednej funkcji. Ma to tę zaletę, że nie wymaga drugiego przeciążenia, ale ma tę wadę, że jest mniej niż gwiezdna czytelność:

C ++ 11
template <class... Ts>
void print_all(std::ostream& os, Ts const&... args) {
    using expander = int[];
    (void)expander{0,
        (void(os << args), 0)...
    };
}

Aby uzyskać wyjaśnienie, jak to działa, zobacz doskonałą odpowiedź TC .

C ++ 17

Wraz z C ++ 17 otrzymujemy dwa nowe potężne narzędzia w naszym arsenale do rozwiązania tego problemu. Pierwszy to wyrażenie krotnie:

template <class... Ts>
void print_all(std::ostream& os, Ts const&... args) {
    ((os << args), ...);
}

Drugim jest if constexpr , który pozwala nam napisać nasze oryginalne rozwiązanie rekurencyjne w jednej funkcji:

template <class T, class... Ts>
void print_all(std::ostream& os, T const& first, Ts const&... rest) {
    os << first;

    if constexpr (sizeof...(rest) > 0) {        
        // this line will only be instantiated if there are further
        // arguments. if rest... is empty, there will be no call to
        // print_all(os). 
        print_all(os, rest...);
    }
}

Iteracja za pomocą std :: integer_sequence

Od C ++ 14 standard zapewnia szablon klasy

template <class T, T... Ints>
class integer_sequence;

template <std::size_t... Ints>
using index_sequence = std::integer_sequence<std::size_t, Ints...>;

i generująca go metafunkcja:

template <class T, T N>
using make_integer_sequence = std::integer_sequence<T, /* a sequence 0, 1, 2, ..., N-1 */ >;

template<std::size_t N>
using make_index_sequence = make_integer_sequence<std::size_t, N>;

Chociaż jest to standard w C ++ 14, można to zaimplementować za pomocą narzędzi C ++ 11.


Możemy użyć tego narzędzia do wywołania funkcji z argumentem std::tuple (standaryzowanym w C ++ 17 jako std::apply ):

namespace detail {
    template <class F, class Tuple, std::size_t... Is>
    decltype(auto) apply_impl(F&& f, Tuple&& tpl, std::index_sequence<Is...> ) {
        return std::forward<F>(f)(std::get<Is>(std::forward<Tuple>(tpl))...);
    }
}

template <class F, class Tuple>
decltype(auto) apply(F&& f, Tuple&& tpl) {
    return detail::apply_impl(std::forward<F>(f),
        std::forward<Tuple>(tpl),
        std::make_index_sequence<std::tuple_size<std::decay_t<Tuple>>::value>{});
}


// this will print 3
int f(int, char, double);

auto some_args = std::make_tuple(42, 'x', 3.14);
int r = apply(f, some_args); // calls f(42, 'x', 3.14)

Wysyłanie tagów

Prostym sposobem wyboru między funkcjami w czasie kompilacji jest wysłanie funkcji do przeciążonej pary funkcji, które przyjmują znacznik jako jeden (zwykle ostatni) argument. Na przykład, aby zaimplementować std::advance() , możemy wysłać do kategorii iteratora:

namespace details {
    template <class RAIter, class Distance>
    void advance(RAIter& it, Distance n, std::random_access_iterator_tag) {
        it += n;
    }

    template <class BidirIter, class Distance>
    void advance(BidirIter& it, Distance n, std::bidirectional_iterator_tag) {
        if (n > 0) {
            while (n--) ++it;
        }
        else {
            while (n++) --it;
        }
    }

    template <class InputIter, class Distance>
    void advance(InputIter& it, Distance n, std::input_iterator_tag) {
        while (n--) {
            ++it;
        }
    }    
}

template <class Iter, class Distance>
void advance(Iter& it, Distance n) {
    details::advance(it, n, 
            typename std::iterator_traits<Iter>::iterator_category{} );
}

Argumenty std::XY_iterator_tag przeciążonych funkcji details::advance są nieużywanymi parametrami funkcji. Rzeczywista implementacja nie ma znaczenia (w rzeczywistości jest całkowicie pusta). Ich jedynym celem jest umożliwienie kompilatorowi wybrania przeciążenia na podstawie tego, z którą klasą tagów details::advance jest wywoływane.

W tym przykładzie, advance używa iterator_traits<T>::iterator_category która zwraca jedną z klas iterator_tag , w zależności od faktycznego typu Iter . Domyślnie skonstruowany obiekt typu iterator_category<Iter>::type następnie pozwala kompilatorowi wybrać jedno z różnych przeciążeń details::advance . (Ten parametr funkcji prawdopodobnie zostanie całkowicie zoptymalizowany, ponieważ jest to domyślnie zbudowany obiekt pustej struct i nigdy nie jest używany).

Wysyłanie tagów może dać ci kod, który jest dużo łatwiejszy do odczytania niż odpowiedniki przy użyciu SFINAE i enable_if .

Uwaga: chociaż C ++ 17, if constexpr może w szczególności uprościć implementację advance , nie nadaje się do otwartych implementacji, w przeciwieństwie do wysyłania znaczników.

Sprawdź, czy wyrażenie jest prawidłowe

Możliwe jest wykrycie, czy można wywołać operator lub funkcję dla danego typu. Aby sprawdzić, czy klasa ma przeciążenie std::hash , można to zrobić:

#include <functional> // for std::hash
#include <type_traits> // for std::false_type and std::true_type
#include <utility> // for std::declval

template<class, class = void>
struct has_hash
    : std::false_type
{};

template<class T>
struct has_hash<T, decltype(std::hash<T>()(std::declval<T>()), void())>
    : std::true_type
{};
C ++ 17

Od wersji C ++ 17 std::void_t może być użyte do uproszczenia tego typu konstrukcji

#include <functional> // for std::hash
#include <type_traits> // for std::false_type, std::true_type, std::void_t
#include <utility> // for std::declval

template<class, class = std::void_t<> >
struct has_hash
    : std::false_type
{};

template<class T>
struct has_hash<T, std::void_t< decltype(std::hash<T>()(std::declval<T>())) > >
    : std::true_type
{};

gdzie std::void_t jest zdefiniowane jako:

template< class... > using void_t = void;

Aby wykryć, czy zdefiniowano operator, taki jak operator< , składnia jest prawie taka sama:

template<class, class = void>
struct has_less_than
    : std::false_type
{};

template<class T>
struct has_less_than<T, decltype(std::declval<T>() < std::declval<T>(), void())>
    : std::true_type
{};

Można ich użyć do użycia std::unordered_map<T> jeśli T ma przeciążenie dla std::hash , ale w innym przypadku spróbuj użyć std::map<T> :

template <class K, class V>
using hash_invariant_map = std::conditional_t<
    has_hash<K>::value,
    std::unordered_map<K, V>,
    std::map<K,V>>;    

Obliczanie mocy za pomocą C ++ 11 (i wyższych)

Dzięki C ++ 11 i wyższym obliczenia w czasie kompilacji mogą być znacznie łatwiejsze. Na przykład obliczenie mocy danej liczby w czasie kompilacji będzie następujące:

template <typename T>
constexpr T calculatePower(T value, unsigned power) {
    return power == 0 ? 1 : value * calculatePower(value, power-1);
}

Słowo kluczowe constexpr jest odpowiedzialne za obliczanie funkcji w czasie kompilacji, wtedy i tylko wtedy, gdy zostaną spełnione wszystkie wymagania (zobacz więcej w odnośniku słowa kluczowego constexpr), na przykład wszystkie argumenty muszą być znane w czasie kompilacji.

Uwaga: W C ++ 11 funkcja constexpr musi składać się tylko z jednej instrukcji return.

Zalety: W porównaniu ze standardowym sposobem obliczania czasu kompilacji, ta metoda jest również przydatna do obliczeń czasu wykonywania. Oznacza to, że jeśli argumenty funkcji nie są znane w czasie kompilacji (np. Wartość i moc są podawane jako dane wejściowe przez użytkownika), wówczas funkcja jest uruchamiana w czasie kompilacji, więc nie ma potrzeby duplikowania kodu (ponieważ byłoby wymuszone w starszych standardach C ++).

Na przykład

void useExample() {
    constexpr int compileTimeCalculated = calculatePower(3, 3); // computes at compile time,
                               // as both arguments are known at compilation time
                               // and used for a constant expression.
    int value;
    std::cin >> value;
    int runtimeCalculated = calculatePower(value, 3);  // runtime calculated,
                                    // because value is known only at runtime.
}
C ++ 17

Innym sposobem obliczenia mocy w czasie kompilacji może być użycie wyrażenia krotnie w następujący sposób:

#include <iostream>
#include <utility>

template <class T, T V, T N, class I = std::make_integer_sequence<T, N>>
struct power;

template <class T, T V, T N, T... Is>
struct power<T, V, N, std::integer_sequence<T, Is...>> {
   static constexpr T value = (static_cast<T>(1) * ... * (V * static_cast<bool>(Is + 1)));
};

int main() {
   std::cout << power<int, 4, 2>::value << std::endl;
}

Ręczne rozróżnianie typów, gdy podano dowolny typ T.

Podczas implementowania SFINAE za pomocą std::enable_if często przydatny jest dostęp do szablonów pomocniczych, które określają, czy dany typ T pasuje do zestawu kryteriów.

Aby nam w tym pomóc, standard udostępnia już dwa typy analogowe do true i false które są std::true_type i std::false_type .

Poniższy przykład pokazuje, jak wykryć, czy typ T jest wskaźnikiem, czy nie, szablon is_pointer naśladuje zachowanie standardowego pomocnika std::is_pointer :

template <typename T>
struct is_pointer_: std::false_type {};

template <typename T>
struct is_pointer_<T*>: std::true_type {};

template <typename T>
struct is_pointer: is_pointer_<typename std::remove_cv<T>::type> { }

Powyższy kod składa się z trzech kroków (czasami potrzebujesz tylko dwóch):

  1. Pierwsza deklaracja is_pointer_ jest wielkością domyślną i dziedziczy po std::false_type . Domyślny przypadek powinien zawsze dziedziczyć po std::false_type ponieważ jest analogiczny do „warunku false ”.

  2. Druga deklaracja specjalizuje się w szablonie is_pointer_ dla wskaźnika T* bez dbania o to, czym tak naprawdę jest T Ta wersja dziedziczy po std::true_type .

  3. Trzecia deklaracja (prawdziwa) po prostu usuwa niepotrzebne informacje z T (w tym przypadku usuwamy const i volatile kwalifikatory), a następnie wraca do jednej z dwóch poprzednich deklaracji.

Ponieważ is_pointer<T> jest klasą, aby uzyskać dostęp do jej wartości, musisz:

  • Use ::value , np. is_pointer<int>::value - value jest statycznym członkiem klasy typu bool odziedziczonym z std::true_type lub std::false_type ;
  • Skonstruuj obiekt tego typu, np. is_pointer<int>{} - Działa to, ponieważ std::is_pointer dziedziczy domyślnego konstruktora od std::true_type lub std::false_type (które mają konstruktory constexpr ) oraz zarówno std::true_type i std::false_type ma operatory konwersji constexpr na bool .

Dobrym nawykiem jest dostarczanie „szablonów pomocników”, które umożliwiają bezpośredni dostęp do wartości:

template <typename T>
constexpr bool is_pointer_v = is_pointer<T>::value;
C ++ 17

W C ++ 17 i nowszych większość szablonów pomocniczych zawiera już wersję _v , np .:

template< class T > constexpr bool is_pointer_v = is_pointer<T>::value;
template< class T > constexpr bool is_reference_v = is_reference<T>::value;

Jeśli-to-jeszcze

C ++ 11

Typ std::conditional w standardowym nagłówku biblioteki <type_traits> może wybrać jeden lub drugi typ, w oparciu o wartość logiczną czasu kompilacji:

template<typename T>
struct ValueOrPointer
{
    typename std::conditional<(sizeof(T) > sizeof(void*)), T*, T>::type vop;
};

Ta struktura zawiera wskaźnik do T jeśli T jest większy niż rozmiar wskaźnika, lub sam T , jeśli jest mniejszy lub równy rozmiarowi wskaźnika. Dlatego sizeof(ValueOrPointer) zawsze będzie wynosić <= sizeof(void*) .

Ogólne min / maks ze zmienną liczbą argumentów

C ++ 11

Możliwe jest napisanie funkcji ogólnej (na przykład min ), która akceptuje różne typy liczbowe i dowolną liczbę argumentów za pomocą metaprogramowania szablonu. Ta funkcja deklaruje min dla dwóch argumentów i rekurencyjnie dla więcej.

template <typename T1, typename T2>
auto min(const T1 &a, const T2 &b) 
-> typename std::common_type<const T1&, const T2&>::type
{
    return a < b ? a : b;
}

template <typename T1, typename T2, typename ... Args>
auto min(const T1 &a, const T2 &b, const Args& ... args)
-> typename std::common_type<const T1&, const T2&, const Args& ...>::type
{
    return min(min(a, b), args...);
}

auto minimum = min(4, 5.8f, 3, 1.8, 3, 1.1, 9);


Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow