サーチ…
前書き
C ++でのメタプログラミングとは、マクロやテンプレートを使用してコンパイル時にコードを生成することを指します。
一般的に、このロールではマクロが嫌になり、テンプレートは一般的ではありませんが、テンプレートが優先されます。
テンプレートのメタプログラミングは、テンプレートやconstexpr
関数を使用してコンパイル時の計算を利用してコードを生成するという目標を達成することがよくありますが、コンパイル時の計算自体はメタプログラミングではありません。
備考
メタプログラミング(より具体的には、テンプレートメタプログラミング)は、コンパイル時にテンプレートを使用して定数、関数、またはデータ構造を作成する方法です。これにより、各実行時ではなくコンパイル時に1回計算を実行することができます。
ファクタリゼーションの計算
Factorialは、テンプレートメタプログラミング技術を使用してコンパイル時に計算することができます。
#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
は構造体ですが、テンプレートメタプログラミングではテンプレートメタ関数として扱われます。習慣的に、テンプレートのメタ関数は、特定のメンバをチェックすることによって評価されます。 ::type
をもたらすメタ関数を、typeは::value
を生成するメタ関数を表します。
上記のコードでは、渡す必要があるパラメータでテンプレートをインスタンス化し、 ::value
を使用して評価の結果を取得することによって、 factorial
メタ関数を評価します。
メタ関数自体は、同じメタ関数を小さな値で再帰的にインスタンス化することに依存しています。 factorial<0>
特殊化は、終了条件を表す。テンプレートメタプログラミングは関数型プログラミング言語の制約の大部分を持っているので、再帰は主要な「ループ」構造です。
テンプレートメタ関数はコンパイル時に実行されるので、その結果はコンパイル時の値を必要とするコンテキストで使用できます。例えば:
int my_array[factorial<5>::value];
自動配列には、コンパイル時に定義されたサイズが必要です。メタ関数の結果はコンパイル時定数なので、ここで使用できます。
制限 :ほとんどのコンパイラでは、再帰の深さが制限を超えてはいけません。たとえば、デフォルトでg++
コンパイラは256レベルに再帰を制限します。 g++
場合、プログラマは-ftemplate-depth-X
オプションを使用して再帰深度を設定できます。
C ++ 11以降、この種のテンプレート計算にはstd::integral_constant
テンプレートを使用できます。
#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"
}
さらに、 constexpr
関数はよりクリーンな代替手段になります。
#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';
}
factorial()
の本体は単一のステートメントとして記述されています。これは、C ++ 11のconstexpr
関数では、言語のかなり限定されたサブセットしか使用できないためです。
C ++ 14以降、 constexpr
関数の多くの制限がconstexpr
、より便利に書けるようになりました。
constexpr long long factorial(long long n)
{
if (n == 0)
return 1;
else
return n * factorial(n - 1);
}
あるいは:
constexpr long long factorial(int n)
{
long long result = 1;
for (int i = 1; i <= n; ++i) {
result *= i;
}
return result;
}
c ++ 17以降はfold expressionを使って階乗を計算することができます:
#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;
}
パラメータパックの反復処理
多くの場合、バリデーションテンプレートのパラメータパックのすべての要素に対して操作を実行する必要があります。これを行うには多くの方法があり、ソリューションはC ++での読み書きが容易になります。パック内のすべての要素を単に出力したいと仮定します。最も簡単な解決策は、再帰することです。
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...);
}
代わりに、エキスパンダートリックを使用して、すべてのストリーミングを単一の機能で実行することができます。これには2番目の過負荷を必要としないという利点がありますが、読み易さに劣るという欠点があります。
template <class... Ts>
void print_all(std::ostream& os, Ts const&... args) {
using expander = int[];
(void)expander{0,
(void(os << args), 0)...
};
}
この仕組みの説明については、 TCの優れた答えをご覧ください。
C ++ 17では、この問題を解決するために、アーセナルに2つの強力な新しいツールが用意されています。 1つ目はfold-expressionです。
template <class... Ts>
void print_all(std::ostream& os, Ts const&... args) {
((os << args), ...);
}
if constexpr
はif constexpr
、元の再帰的解を一つの関数で書くことができます:
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...);
}
}
std :: integer_sequenceで繰り返す
C ++ 14以降、標準ではクラステンプレート
template <class T, T... Ints>
class integer_sequence;
template <std::size_t... Ints>
using index_sequence = std::integer_sequence<std::size_t, Ints...>;
そのためのメタ関数を生成します。
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>;
これはC ++ 14では標準ですが、これはC ++ 11ツールを使用して実装できます。
このツールを使用して、 std::tuple
の引数を持つ関数を呼び出すことができます( std::apply
としてC ++ 17で標準化されてい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)
タグディスパッチ
コンパイル時に関数間を簡単に選択する方法は、タグを1つ(通常は最後の引数)として使用するオーバーロードされた関数のペアに関数をディスパッチすることです。たとえば、 std::advance()
を実装するには、iteratorカテゴリにディスパッチできます。
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{} );
}
オーバーロードされたdetails::advance
関数のstd::XY_iterator_tag
引数は、未使用の関数パラメータです。実際の実装は重要ではありません(実際は完全に空です)。その唯一の目的は、コンパイラがタグクラスのdetails::advance
が呼び出されるときに基づいて、オーバーロードを選択できるようにすることです。
この例では、 advance
使用するiterator_traits<T>::iterator_category
メタ関数のいずれかを返したiterator_tag
実際のタイプに応じて、クラスをIter
。 iterator_category<Iter>::type
のデフォルトで構築されたオブジェクトは、コンパイラにdetails::advance
異なるオーバーロードの1つを選択さdetails::advance
ます。 (この関数のパラメータは空のstruct
デフォルトで構築されたオブジェクトであり、決して使用されないため、完全に最適化されている可能性があります)。
タグのディスパッチは、SFINAEとenable_if
を使用して同等のコードよりもはるかに読みやすいコードを与えることができます。
注意:C ++ 17では、特にif constexpr
がadvance
の実装を簡略化することができますが、タグのディスパッチとは異なり、オープンな実装には適していません。
式が有効かどうかを検出する
あるタイプに対してオペレータまたは関数を呼び出すことができるかどうかを検出することができます。クラスにstd::hash
オーバーロードがあるかどうかをテストするには、次のようにします。
#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以降、 std::void_t
はこのタイプの構造を単純化するために使用できます
#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
{};
ここで、 std::void_t
は次のように定義されます。
template< class... > using void_t = void;
operator<
などのoperator<
が定義されているかどうかを検出するには、構文はほぼ同じです。
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
{};
T
がstd::hash
オーバーロードを持っている場合、これらはstd::unordered_map<T>
を使用するために使用できますが、それ以外の場合は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>>;
C ++ 11以降での消費電力の計算
C ++では、コンパイル時に11以上の計算がはるかに簡単になります。たとえば、コンパイル時に与えられた数の累乗を計算すると、次のようになります。
template <typename T>
constexpr T calculatePower(T value, unsigned power) {
return power == 0 ? 1 : value * calculatePower(value, power-1);
}
キーワードconstexpr
は、コンパイル時に関数を計算する責任を負います。そのために必要な条件がすべて満たされている場合(constexprキーワード参照)、コンパイル時にすべての引数を知っている必要があります。
注意:C ++ 11 constexpr
関数は1つのreturn文からのみ作成する必要があります。
利点:これをコンパイル時間の標準的な計算方法と比較すると、このメソッドは実行時の計算にも役立ちます。つまり、関数の引数がコンパイル時に知られていない場合(例えば、値とパワーがユーザーからの入力として与えられた場合)、関数はコンパイル時に実行されるため、コードを複製する必要はありませんC ++の古い標準では強制されます)。
例えば
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.
}
コンパイル時に電力を計算するもう1つの方法は、次のようにfold式を使用できます。
#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;
}
任意の型が与えられたときの型の手動による区別T
std::enable_if
enable_ifを使用してSFINAEを実装する場合、ヘルパーテンプレートにアクセスして、特定の型T
が一連の条件に一致するかどうかを判断することが便利なことがよくあります。
これを手助けするために、標準では、 true
とfalse
2つのタイプのアナログ( std::true_type
とstd::false_type
がすでに用意されています。
次の例は、タイプT
がポインタであるかどうかを検出する方法を示していますis_pointer
テンプレートは、標準の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> { }
上記のコードには3つのステップがあります(2つだけ必要な場合もあります)。
is_pointer_
の最初の宣言がデフォルトの場合で 、std::false_type
から継承します。 デフォルトのケースは、 "false
condition"に類似しているので、常にstd::false_type
から継承する必要があります 。2番目の宣言は、
T
が本当に何であるかを気にせずにポインタT*
is_pointer_
テンプレートを特化します。このバージョンはstd::true_type
から継承しています。3番目の宣言(実際のもの)は、
T
から不要な情報を削除するだけです(この場合はconst
とvolatile
修飾子が削除されます)。その後、前の2つの宣言のいずれかに戻ります。
is_pointer<T>
はクラスなので、その値にアクセスするには次のいずれかが必要です。
-
::value
使用する::value
is_pointer<int>::value
-value
は、std::true_type
またはstd::false_type
から継承したbool
型の静的クラスメンバーです。 -
is_pointer<int>{}
- これは、std::is_pointer
がstd::true_type
またはstd::false_type
(constexpr
コンストラクタを持つ)とstd::true_type
とstd::false_type
両方からデフォルトのコンストラクタを継承するため、std::false_type
constexpr
変換演算子をbool
持っています。
値に直接アクセスできるようにする「ヘルパーヘルパーテンプレート」を用意することは良い習慣です:
template <typename T>
constexpr bool is_pointer_v = is_pointer<T>::value;
C ++ 17以降では、ほとんどのヘルパーテンプレートはすでに_v
バージョンを提供しています。
template< class T > constexpr bool is_pointer_v = is_pointer<T>::value;
template< class T > constexpr bool is_reference_v = is_reference<T>::value;
If-then-else
標準ライブラリヘッダー<type_traits>
のタイプstd::conditional
は、コンパイル時のブール値に基づいて、1つのタイプまたは他のタイプを選択できます。
template<typename T>
struct ValueOrPointer
{
typename std::conditional<(sizeof(T) > sizeof(void*)), T*, T>::type vop;
};
この構造体は、へのポインタを含むT
場合T
ポインタの大きさ、またはより大きいT
がポインタの大きさ以下である場合、それ自体。したがって、 sizeof(ValueOrPointer)
は常に<= sizeof(void*)
ます。
可変引数を持つ一般的な最小値/最大値
テンプレートのメタプログラミングによってさまざまな数値型と任意の引数の数を受け入れる汎用関数(たとえばmin
)を書くことができます。この関数は、2つの引数に対してmin
を宣言し、より多くの場合は再帰的に宣言します。
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);