1. ホーム
  2. c++

可変型テンプレート関数の異種引数パックに対する汎用計算を行うには?

2023-08-11 23:59:50

質問

PREMISE:

可変個体テンプレートで少し遊んだ後、私は些細なメタプログラミングのタスクを少し超える何かを達成することはすぐにかなり面倒になることに気づきました。特に、私は自分自身が 引数パックに対する一般的な操作 というような 繰り返す , 分割 , ループ の中に std::for_each -のような形でループさせます。

視聴後 アンドレイ・アレキサンドレスク氏によるこの講演を の望ましい使い方について、C++ and Beyond 2012 で発表されました。 static if を C++ に導入することの必要性について述べています (この構成は D プログラミング言語 から借用したものです) 私は、ある種の static for のようなものも便利だろうと感じていました。 static の構成がもっと増えてもいいと思います。

というわけで、私は のようなものを実現する方法はないかと考え始めました。 を実現する方法はないかと考え始めました。 擬似コード ):

template<typename... Ts>
void my_function(Ts&&... args)
{
    static for (int i = 0; i < sizeof...(args); i++) // PSEUDO-CODE!
    {
        foo(nth_value_of<i>(args));
    }
}

どのように翻訳されるのでしょうか コンパイル時に はこのようなものに変換されます。

template<typename... Ts>
void my_function(Ts&&... args)
{
    foo(nth_value_of<0>(args));
    foo(nth_value_of<1>(args));
    // ...
    foo(nth_value_of<sizeof...(args) - 1>(args));
}

原則的には static_for を使えば、さらに精巧な処理が可能になります。

template<typename... Ts>
void foo(Ts&&... args)
{
    constexpr s = sizeof...(args);

    static for (int i = 0; i < s / 2; i++)
    {
        // Do something
        foo(nth_value_of<i>(args));
    }

    static for (int i = s / 2; i < s; i++)
    {
        // Do something different
        bar(nth_value_of<i>(args));
    }
}

あるいは、このような表現力のあるイディオムの場合。

template<typename... Ts>
void foo(Ts&&... args)
{
    static for_each (auto&& x : args)
    {
        foo(x);
    }
}

関連する作品。

Webで検索してみたところ、以下のようなことがわかりました。 何か は確かに存在することがわかりました。

  • このリンク には、パラメータパックをBoost.MPLベクトルに変換する方法が記載されていますが、これは目標に対して半分の道しか進んでいません(それ以下ではないとしても)。
  • に関するこの質問は、SO は、似ていてわずかに関連したメタプログラミング機能(引数パックを2つに分割する)を求めているようです。実際、この問題に関連していると思われるSO上のいくつかの質問がありますが、私が読んだ回答のどれも、IMHOでは満足のいく解決にはなっていません。
  • Boost.Fusion は、引数パックを タプル というのがありますが、私はどちらかというと
    1. を作成しない 不要なテンポラリー を作成し、いくつかの一般的なアルゴリズムに完璧に転送できる(べき)引数を保持するようにします。
    2. があります。 小さく、自己充足的な がある一方で、Boost.Fusionはこの問題に対処するために必要なものよりもずっと多くのものを含んでいる可能性があります。

QUESTIONです。

既存のアプローチの制限を受けることなく、私が探しているものを達成するための、比較的簡単な方法、おそらくいくつかのテンプレートのメタプログラミングを通じた方法はありますか?

どのように解決するのですか?

自分で見つけたものに満足できなかったので、自分で解決しようと思って、結局は 小さなライブラリ を書くことにしました。これは引数パックに対する一般的な操作を定式化することを可能にします。私の解決策は以下のような特徴を持っています。

  • に対する反復処理を可能にする。 いくつかの で指定された引数パックの要素にアクセスできるようになります。 コンピューティング で指定できるかもしれません。
  • 引数パックの計算された部分をvariadicファンクタに転送することを可能にする。
  • 比較的短いヘッダファイルを一つ含むだけでよい。
  • 完全な転送を多用することでインライン化を進め、不要なコピーや移動を避けることでパフォーマンスの低下を最小限に抑えます。
  • 反復アルゴリズムの内部実装は、メモリ消費を最小化するために Empty Base Class Optimization に依存しています。
  • 拡張や適応が(テンプレートメタプログラミングであることを考慮すると、比較的)容易である。

最初に示すのは 何ができるのか を紹介します。 では を投稿してください。 実装 .

使用例

以下は for_each_in_arg_pack() 関数は,パックの全ての引数を繰り返し処理し,クライアントが 提供するファンクタに入力として各引数を渡すために使われます(もちろん, 引数パックが異種型の値を含む場合,ファンクタは一般呼び出し演算子を 持っていなければなりません)。

// Simple functor with a generic call operator that prints its input. This is used by the
// following functors and by some demonstrative test cases in the main() routine.
struct print
{
    template<typename T>
    void operator () (T&& t)
    {
        cout << t << endl;
    }
};

// This shows how a for_each_*** helper can be used inside a variadic template function
template<typename... Ts>
void print_all(Ts&&... args)
{
    for_each_in_arg_pack(print(), forward<Ts>(args)...);
}

print ファンクタはより複雑な計算にも使うことができます。具体的には,以下のように サブセット (この場合 サブレンジ ) の引数をパックで返します。

// Shows how to select portions of an argument pack and 
// invoke a functor for each of the selected elements
template<typename... Ts>
void split_and_print(Ts&&... args)
{
    constexpr size_t packSize = sizeof...(args);
    constexpr size_t halfSize = packSize / 2;

    cout << "Printing first half:" << endl;
    for_each_in_arg_pack_subset(
        print(), // The functor to invoke for each element
        index_range<0, halfSize>(), // The indices to select
        forward<Ts>(args)... // The argument pack
        );

    cout << "Printing second half:" << endl;
    for_each_in_arg_pack_subset(
        print(), // The functor to invoke for each element
        index_range<halfSize, packSize>(), // The indices to select
        forward<Ts>(args)... // The argument pack
        );
}

時々、人はただ を転送したい場合があります。 を他のバリアディックファンクタに転送したい場合があります。 個別に を非可変ファンクタに渡します。これは forward_subpack() アルゴリズムでできることです。

// Functor with variadic call operator that shows the usage of for_each_*** 
// to print all the arguments of a heterogeneous pack
struct my_func
{
    template<typename... Ts>
    void operator ()(Ts&&... args)
    {
        print_all(forward<Ts>(args)...);
    }
};

// Shows how to forward only a portion of an argument pack 
// to another variadic functor
template<typename... Ts>
void split_and_print(Ts&&... args)
{
    constexpr size_t packSize = sizeof...(args);
    constexpr size_t halfSize = packSize / 2;

    cout << "Printing first half:" << endl;
    forward_subpack(my_func(), index_range<0, halfSize>(), forward<Ts>(args)...);

    cout << "Printing second half:" << endl;
    forward_subpack(my_func(), index_range<halfSize, packSize>(), forward<Ts>(args)...);
}

より具体的なタスクのために、パック内の特定の引数を インデックス作成 を使用することもできます。これは nth_value_of() 関数とそのヘルパーである first_value_of()last_value_of() :

// Shows that arguments in a pack can be indexed
template<unsigned I, typename... Ts>
void print_first_last_and_indexed(Ts&&... args)
{
    cout << "First argument: " << first_value_of(forward<Ts>(args)...) << endl;
    cout << "Last argument: " << last_value_of(forward<Ts>(args)...) << endl;
    cout << "Argument #" << I << ": " << nth_value_of<I>(forward<Ts>(args)...) << endl;
}

引数パックが 同質である である場合(つまり、すべての引数が同じ型である場合)、以下のような定式化が望ましいかもしれません。その場合 is_homogeneous_pack<> メタ関数は、パラメータパック内のすべての型が均質かどうかを判断することができ、 主に static_assert() ステートメントで使用されます。

// Shows the use of range-based for loops to iterate over a
// homogeneous argument pack
template<typename... Ts>
void print_all(Ts&&... args)
{
    static_assert(
        is_homogeneous_pack<Ts...>::value, 
        "Template parameter pack not homogeneous!"
        );

    for (auto&& x : { args... })
    {
        // Do something with x...
    }

    cout << endl;
}

最後に ラムダ は単なる 構文解析 であり、上記のアルゴリズムと組み合わせて使用することができます。 汎用ラムダ がC++でサポートされるまでは、これは 均質な 引数パックでのみ可能です。また、次の例では homogeneous-type<> メタファンクションの使用法も示しています。

 // ...
 static_assert(
     is_homogeneous_pack<Ts...>::value, 
     "Template parameter pack not homogeneous!"
     );
 using type = homogeneous_type<Ts...>::type;
 for_each_in_arg_pack([] (type const& x) { cout << x << endl; }, forward<Ts>(args)...);

これは基本的にライブラリが許可していることですが、私が思うに を拡張することができます。 より複雑なタスクを実行するために拡張することもできると思います。

インプリメンテーション

さて、次は実装ですが、これ自体が少し厄介なので、コードを説明するためにコメントに頼って、この記事が長くなりすぎないようにします(おそらくすでに長くなっていると思います)。

#include <type_traits>
#include <utility>

//===============================================================================
// META-FUNCTIONS FOR EXTRACTING THE n-th TYPE OF A PARAMETER PACK

// Declare primary template
template<int I, typename... Ts>
struct nth_type_of
{
};

// Base step
template<typename T, typename... Ts>
struct nth_type_of<0, T, Ts...>
{
    using type = T;
};

// Induction step
template<int I, typename T, typename... Ts>
struct nth_type_of<I, T, Ts...>
{
    using type = typename nth_type_of<I - 1, Ts...>::type;
};

// Helper meta-function for retrieving the first type in a parameter pack
template<typename... Ts>
struct first_type_of
{
    using type = typename nth_type_of<0, Ts...>::type;
};

// Helper meta-function for retrieving the last type in a parameter pack
template<typename... Ts>
struct last_type_of
{
    using type = typename nth_type_of<sizeof...(Ts) - 1, Ts...>::type;
};

//===============================================================================
// FUNCTIONS FOR EXTRACTING THE n-th VALUE OF AN ARGUMENT PACK

// Base step
template<int I, typename T, typename... Ts>
auto nth_value_of(T&& t, Ts&&... args) ->
    typename std::enable_if<(I == 0), decltype(std::forward<T>(t))>::type
{
    return std::forward<T>(t);
}

// Induction step
template<int I, typename T, typename... Ts>
auto nth_value_of(T&& t, Ts&&... args) ->
    typename std::enable_if<(I > 0), decltype(
        std::forward<typename nth_type_of<I, T, Ts...>::type>(
            std::declval<typename nth_type_of<I, T, Ts...>::type>()
            )
        )>::type
{
    using return_type = typename nth_type_of<I, T, Ts...>::type;
    return std::forward<return_type>(nth_value_of<I - 1>((std::forward<Ts>(args))...));
}

// Helper function for retrieving the first value of an argument pack
template<typename... Ts>
auto first_value_of(Ts&&... args) ->
    decltype(
        std::forward<typename first_type_of<Ts...>::type>(
            std::declval<typename first_type_of<Ts...>::type>()
            )
        )
{
    using return_type = typename first_type_of<Ts...>::type;
    return std::forward<return_type>(nth_value_of<0>((std::forward<Ts>(args))...));
}

// Helper function for retrieving the last value of an argument pack
template<typename... Ts>
auto last_value_of(Ts&&... args) ->
    decltype(
        std::forward<typename last_type_of<Ts...>::type>(
            std::declval<typename last_type_of<Ts...>::type>()
            )
        )
{
    using return_type = typename last_type_of<Ts...>::type;
    return std::forward<return_type>(nth_value_of<sizeof...(Ts) - 1>((std::forward<Ts>(args))...));
}

//===============================================================================
// METAFUNCTION FOR COMPUTING THE UNDERLYING TYPE OF HOMOGENEOUS PARAMETER PACKS

// Used as the underlying type of non-homogeneous parameter packs
struct null_type
{
};

// Declare primary template
template<typename... Ts>
struct homogeneous_type;

// Base step
template<typename T>
struct homogeneous_type<T>
{
    using type = T;
    static const bool isHomogeneous = true;
};

// Induction step
template<typename T, typename... Ts>
struct homogeneous_type<T, Ts...>
{
    // The underlying type of the tail of the parameter pack
    using type_of_remaining_parameters = typename homogeneous_type<Ts...>::type;

    // True if each parameter in the pack has the same type
    static const bool isHomogeneous = std::is_same<T, type_of_remaining_parameters>::value;

    // If isHomogeneous is "false", the underlying type is the fictitious null_type
    using type = typename std::conditional<isHomogeneous, T, null_type>::type;
};

// Meta-function to determine if a parameter pack is homogeneous
template<typename... Ts>
struct is_homogeneous_pack
{
    static const bool value = homogeneous_type<Ts...>::isHomogeneous;
};

//===============================================================================
// META-FUNCTIONS FOR CREATING INDEX LISTS

// The structure that encapsulates index lists
template <unsigned... Is>
struct index_list
{
};

// Collects internal details for generating index ranges [MIN, MAX)
namespace detail
{
    // Declare primary template for index range builder
    template <unsigned MIN, unsigned N, unsigned... Is>
    struct range_builder;

    // Base step
    template <unsigned MIN, unsigned... Is>
    struct range_builder<MIN, MIN, Is...>
    {
        typedef index_list<Is...> type;
    };

    // Induction step
    template <unsigned MIN, unsigned N, unsigned... Is>
    struct range_builder : public range_builder<MIN, N - 1, N - 1, Is...>
    {
    };
}

// Meta-function that returns a [MIN, MAX) index range
template<unsigned MIN, unsigned MAX>
using index_range = typename detail::range_builder<MIN, MAX>::type;

//===============================================================================
// CLASSES AND FUNCTIONS FOR REALIZING LOOPS ON ARGUMENT PACKS

// Implementation inspired by @jogojapan's answer to this question:
// http://stackoverflow.com/questions/14089637/return-several-arguments-for-another-function-by-a-single-function

// Collects internal details for implementing functor invocation
namespace detail
{
    // Functor invocation is realized through variadic inheritance.
    // The constructor of each base class invokes an input functor.
    // An functor invoker for an argument pack has one base class
    // for each argument in the pack

    // Realizes the invocation of the functor for one parameter
    template<unsigned I, typename T>
    struct invoker_base
    {
        template<typename F, typename U>
        invoker_base(F&& f, U&& u) { f(u); }
    };

    // Necessary because a class cannot inherit the same class twice
    template<unsigned I, typename T>
    struct indexed_type
    {
        static const unsigned int index = I;
        using type = T;
    };

    // The functor invoker: inherits from a list of base classes.
    // The constructor of each of these classes invokes the input
    // functor with one of the arguments in the pack.
    template<typename... Ts>
    struct invoker : public invoker_base<Ts::index, typename Ts::type>...
    {
        template<typename F, typename... Us>
        invoker(F&& f, Us&&... args)
            :
            invoker_base<Ts::index, typename Ts::type>(std::forward<F>(f), std::forward<Us>(args))...
        {
        }
    };
}

// The functor provided in the first argument is invoked for each
// argument in the pack whose index is contained in the index list
// specified in the second argument
template<typename F, unsigned... Is, typename... Ts>
void for_each_in_arg_pack_subset(F&& f, index_list<Is...> const& i, Ts&&... args)
{
    // Constructors of invoker's sub-objects will invoke the functor.
    // Note that argument types must be paired with numbers because the
    // implementation is based on inheritance, and one class cannot
    // inherit the same base class twice.
    detail::invoker<detail::indexed_type<Is, typename nth_type_of<Is, Ts...>::type>...> invoker(
        f,
        (nth_value_of<Is>(std::forward<Ts>(args)...))...
        );
}

// The functor provided in the first argument is invoked for each
// argument in the pack
template<typename F, typename... Ts>
void for_each_in_arg_pack(F&& f, Ts&&... args)
{
    for_each_in_arg_pack_subset(f, index_range<0, sizeof...(Ts)>(), std::forward<Ts>(args)...);
}

// The functor provided in the first argument is given in input the
// arguments in whose index is contained in the index list specified
// as the second argument.
template<typename F, unsigned... Is, typename... Ts>
void forward_subpack(F&& f, index_list<Is...> const& i, Ts&&... args)
{
    f((nth_value_of<Is>(std::forward<Ts>(args)...))...);
}

// The functor provided in the first argument is given in input all the
// arguments in the pack.
template<typename F, typename... Ts>
void forward_pack(F&& f, Ts&&... args)
{
    f(std::forward<Ts>(args)...);
}

結論

もちろん、この問いに自分なりの答えを出したとしても、(そして実際に のため

この事実のために)、私は、質問の "関連作品" セクションで言及されたものを除いて、私が見逃している代替またはより良いソリューションが存在するかどうかを聞きたいと思っています。