C++
Metaprogramowanie
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
.
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.
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;
}
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:
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ść:
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 .
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
{};
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.
}
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):
Pierwsza deklaracja
is_pointer_
jest wielkością domyślną i dziedziczy postd::false_type
. Domyślny przypadek powinien zawsze dziedziczyć postd::false_type
ponieważ jest analogiczny do „warunkufalse
”.Druga deklaracja specjalizuje się w szablonie
is_pointer_
dla wskaźnikaT*
bez dbania o to, czym tak naprawdę jestT
Ta wersja dziedziczy postd::true_type
.Trzecia deklaracja (prawdziwa) po prostu usuwa niepotrzebne informacje z
T
(w tym przypadku usuwamyconst
ivolatile
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 typubool
odziedziczonym zstd::true_type
lubstd::false_type
; - Skonstruuj obiekt tego typu, np.
is_pointer<int>{}
- Działa to, ponieważstd::is_pointer
dziedziczy domyślnego konstruktora odstd::true_type
lubstd::false_type
(które mają konstruktoryconstexpr
) oraz zarównostd::true_type
istd::false_type
ma operatory konwersjiconstexpr
nabool
.
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;
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
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
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);