サーチ…


前書き

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

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

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

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 ++での読み書きが容易になります。パック内のすべての要素を単に出力したいと仮定します。最も簡単な解決策は、再帰することです。

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...);
}

代わりに、エキスパンダートリックを使用して、すべてのストリーミングを単一の機能で実行することができます。これには2番目の過負荷を必要としないという利点がありますが、読み易さに劣るという欠点があります。

C ++ 11
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

C ++ 17では、この問題を解決するために、アーセナルに2つの強力な新しいツールが用意されています。 1つ目はfold-expressionです。

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

if constexprif 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実際のタイプに応じて、クラスをIteriterator_category<Iter>::typeのデフォルトで構築されたオブジェクトは、コンパイラにdetails::advance異なるオーバーロードの1つを選択さdetails::advanceます。 (この関数のパラメータは空のstructデフォルトで構築されたオブジェクトであり、決して使用されないため、完全に最適化されている可能性があります)。

タグのディスパッチは、SFINAEとenable_ifを使用して同等のコードよりもはるかに読みやすいコードを与えることができます。

注意:C ++ 17では、特にif constexpradvanceの実装を簡略化することができますが、タグのディスパッチとは異なり、オープンな実装には適していません。

式が有効かどうかを検出する

あるタイプに対してオペレータまたは関数を呼び出すことができるかどうかを検出することができます。クラスに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

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
{};

Tstd::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.
}
C ++ 17

コンパイル時に電力を計算するもう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が一連の​​条件に一致するかどうかを判断することが便利なことがよくあります。

これを手助けするために、標準では、 truefalse 2つのタイプのアナログ( std::true_typestd::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つだけ必要な場合もあります)。

  1. is_pointer_の最初の宣言がデフォルトの場合でstd::false_typeから継承します。 デフォルトのケースは、 " false condition"に類似しているので、常にstd::false_typeから継承する必要があります

  2. 2番目の宣言は、 Tが本当に何であるかを気にせずにポインタT* is_pointer_テンプレートを特化します。このバージョンはstd::true_typeから継承しています。

  3. 3番目の宣言(実際のもの)は、 Tから不要な情報を削除するだけです(この場合はconstvolatile修飾子が削除されます)。その後、前の2つの宣言のいずれかに戻ります。

is_pointer<T>はクラスなので、その値にアクセスするには次のいずれかが必要です。

  • ::value使用する::value is_pointer<int>::value - valueは、 std::true_typeまたはstd::false_typeから継承したbool型の静的クラスメンバーです。
  • is_pointer<int>{} - これは、 std::is_pointerstd::true_typeまたはstd::false_typeconstexprコンストラクタを持つ)とstd::true_typestd::false_type両方からデフォルトのコンストラクタを継承するため、 std::false_type constexpr変換演算子をbool持っています。

値に直接アクセスできるようにする「ヘルパーヘルパーテンプレート」を用意することは良い習慣です:

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

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

C ++ 11

標準ライブラリヘッダー<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*)ます。

可変引数を持つ一般的な最小値/最大値

C ++ 11

テンプレートのメタプログラミングによってさまざまな数値型と任意の引数の数を受け入れる汎用関数(たとえば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);


Modified text is an extract of the original Stack Overflow Documentation
ライセンスを受けた CC BY-SA 3.0
所属していない Stack Overflow