Frage Wie können generische Berechnungen über heterogene Argument-Packs einer variadischen Template-Funktion durchgeführt werden?


PRÄMISSE:

Nachdem ich ein wenig mit variantenreichen Templates herumgespielt habe, wurde mir klar, dass es ziemlich umständlich wird, etwas zu erreichen, was etwas über die trivialen Meta-Programmieraufgaben hinausgeht. Insbesondere wünschte ich mir einen Weg, etwas zu leisten generische Operationen über ein Argument-Pack sowie iterieren, Teilt, Schleife in einem std::for_eachwie Mode und so weiter.

Nach dem Anschauen dieser Vortrag von Andrei Alexandrescu von C ++ und Beyond 2012 über die Erwünschtheit von static if in C ++ (ein Konstrukt, das von der D Programmiersprache) Ich hatte das Gefühl, dass eine Art von static for wäre auch praktisch - und ich fühle mehr davon static Konstrukte könnten einen Nutzen bringen.

Also habe ich mich gefragt, ob es einen Weg zu erreichen gibt etwas wie das für Argumentpakete einer variadischen Template-Funktion (Pseudo-Code):

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

Was würde übersetzt werden zur Kompilierzeit in etwas wie das:

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

Allgemein gesagt, static_for würde eine noch aufwendigere Verarbeitung ermöglichen:

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

Oder für ein ausdrucksstarkes Idiom wie dieses:

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

Verwandte Arbeit:

Ich habe im Internet gesucht und das herausgefunden etwas gibt es tatsächlich:

  • Dieser Link beschreibt, wie man ein Parameter-Pack in einen Boost.MPL-Vektor umwandelt, aber nur halbwegs (wenn nicht weniger) zum Ziel hin;
  • diese Frage zu SO scheint ein ähnliches und leicht verwandtes Meta-Programming-Feature zu fordern (ein Argument-Pack in zwei Hälften zu teilen) - eigentlich gibt es einige Fragen zu SO, die mit diesem Problem zu tun haben, aber keine der Antworten, die ich gelesen habe, löst es befriedigend IMHO;
  • Boost.Fusion definiert Algorithmen zum Konvertieren eines Argumentpakets in ein Tupel, aber ich würde bevorzugen:
    1. nicht erstellen unnötige Provisorien Argumente zu halten, die perfekt an einige generische Algorithmen weitergeleitet werden können (und sollten);
    2. habe einen klein, in sich geschlossen Bibliothek, um dies zu tun, während Boost.Fusion wird wahrscheinlich viel mehr Dinge als nötig ist, um dieses Problem zu beheben.

FRAGE:

Gibt es einen relativ einfachen Weg, möglicherweise durch eine Template-Metaprogrammierung, um das zu erreichen, wonach ich suche, ohne die Grenzen der bestehenden Ansätze einzugehen?


75
2018-01-10 15:19


Ursprung


Antworten:


Da ich mit dem, was ich gefunden habe, nicht zufrieden war, habe ich selbst versucht, eine Lösung zu finden und am Ende eine kleine Bibliothek wodurch generische Operationen für Argumentpakete formuliert werden können. Meine Lösung hat folgende Eigenschaften:

  • Ermöglicht das Iterieren über alle oder etwas Elemente eines Argumentpakets, möglicherweise von rechnen ihre Indizes auf der Packung;
  • Ermöglicht das Weiterleiten von berechneten Teilen eines Argument-Packs an variable Funktoren;
  • Benötigt nur eine relativ kurze Header-Datei;
  • Umfangreiche Nutzung der perfekten Weiterleitung, um schweres Inlining zu ermöglichen und unnötige Kopien / Bewegungen zu vermeiden, um einen minimalen Leistungsverlust zu ermöglichen;
  • Die interne Implementierung der Iterationsalgorithmen basiert auf der Empty Base Class Optimization, um den Speicherverbrauch zu minimieren.
  • Es ist einfach (relativ, in Anbetracht seiner Template-Metaprogrammierung), zu erweitern und anzupassen.

Ich werde es zuerst zeigen was kann getan werden mit der Bibliothek, dann Post es Implementierung.

ANWENDUNGSFÄLLE

Hier ist ein Beispiel wie for_each_in_arg_pack() Funktion kann verwendet werden, um alle Argumente eines Pakets zu durchlaufen und jedes Argument in der Eingabe an einen vom Client bereitgestellten Funktor weiterzuleiten (der Funktor muss natürlich einen generischen Aufrufoperator haben, wenn das Argumentpack Werte heterogener Typen enthält):

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

Das print Funktor oben kann auch in komplexeren Berechnungen verwendet werden. Insbesondere würde man hier wie folgt vorgehen Teilmenge (in diesem Fall a Unterbereich) der Argumente in einem Paket:

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

Manchmal möchte man einfach nur einen Teil weiterleiten eines Argumentpakets zu einem anderen variadischen Funktor, statt dessen Elemente zu durchlaufen und jedes von ihnen zu übergeben individuell zu einem nicht-variadischen Funktor. Dies ist, was die forward_subpack() Algorithmus erlaubt tun:

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

Für spezifischere Aufgaben ist es natürlich möglich, bestimmte Argumente in einem Paket abzurufen Indizierung Sie. Dies ist, was die nth_value_of() Funktion ermöglicht es, zusammen mit seinen Helfern zu tun first_value_of() und 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;
}

Wenn das Argument pack ist homogen Auf der anderen Seite (d. h. alle Argumente haben den gleichen Typ), könnte eine Formulierung wie die folgende bevorzugt sein. Das is_homogeneous_pack<> Meta-Funktion ermöglicht es, zu bestimmen, ob alle Typen in einem Parameter-Pack homogen sind, und hauptsächlich für die Verwendung in static_assert() Aussagen:

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

Endlich, seit Lambda sind nur syntethischer Zucker Für Funktoren können sie auch in Kombination mit den obigen Algorithmen verwendet werden; jedoch bis generische Lambdas wird von C ++ unterstützt, dies ist nur möglich für homogen Argumentpakete. Das folgende Beispiel zeigt auch die Verwendung des homogeneous-type<> Meta-Funktion, die den Typ aller Argumente in einem homogenen Paket zurückgibt:

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

Das ist im Grunde, was die Bibliothek erlaubt, aber ich glaube es könnte sogar erweitert werden um komplexere Aufgaben zu erledigen.

IMPLEMENTIERUNG

Jetzt kommt die Implementierung, die an sich etwas knifflig ist, daher werde ich mich auf Kommentare verlassen, um den Code zu erklären und zu vermeiden, dass dieser Beitrag zu lang wird (vielleicht ist es das schon):

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

FAZIT

Natürlich, obwohl ich meine Antwort auf diese Frage gegeben habe (und tatsächlich durch Ich bin neugierig zu hören, ob alternative oder bessere Lösungen existieren, die ich übersehen habe - abgesehen von den im Abschnitt "Verwandte Arbeiten" der Frage erwähnten.


63
2018-01-10 15:19



Lassen Sie mich diesen Code basierend auf der Diskussion veröffentlichen:

#include <initializer_list>
#define EXPAND(EXPR) std::initializer_list<int>{((EXPR),0)...}

// Example of use:
#include <iostream>
#include <utility>

void print(int i){std::cout << "int: " << i << '\n';}
int print(double d){std::cout << "double: " << d << '\n';return 2;}

template<class...T> void f(T&&...args){
  EXPAND(print(std::forward<T>(args)));
}

int main(){
  f();
  f(1,2.,3);
}

Ich habe den generierten Code mit überprüft g++ -std=c++11 -O1 und main enthält nur 3 Aufrufe an printVon den Expansionshelfern gibt es keine Spur.


9
2018-01-10 19:17



Mit einem aufzählen Lösung (ala Python).

Verwendung:

void fun(int i, size_t index, size_t size) {
    if (index != 0) {
        std::cout << ", ";
    }

    std::cout << i;

    if (index == size - 1) {
        std::cout << "\n";
    }
} // fun

enumerate(fun, 2, 3, 4);

// Expected output: "2, 3, 4\n"
// check it at: http://liveworkspace.org/code/1cydbw$4

Code:

// Fun: expects a callable of 3 parameters: Arg, size_t, size_t
// Arg: forwarded argument
// size_t: index of current argument
// size_t: number of arguments
template <typename Fun, typename... Args, size_t... Is>
void enumerate_impl(Fun&& fun, index_list<Is...>, Args&&... args) {
    std::initializer_list<int> _{
        (fun(std::forward<Args>(args), Is, sizeof...(Is)), 0)...
    };
    (void)_; // placate compiler, only the side-effects interest us
}

template <typename Fun, typename... Args>
void enumerate(Fun&& fun, Args&&... args) {
    enumerate_impl(fun,
                   index_range<0, sizeof...(args)>(),
                   std::forward<Args>(args)...);
}

Der Range Builder (aus Ihrer Lösung):

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

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

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

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

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

5
2018-02-13 16:56



Die ... Notation hat einige interessante Optionen, wie:

template<typename T>
int print(const T& x) {
  std::cout << "<" << x << ">";
  return 0;
}

void pass(...) {}

template<typename... TS>
void printall(TS... ts){
  pass(print(ts)...);
}

Leider kann ich die Reihenfolge, in der die Druckfunktionen aufgerufen werden, nicht durchsetzen (umgekehrt, auf meinem Compiler). Beachten Sie, dass der Druck etwas zurückgeben muss.

Dieser Trick kann nützlich sein, wenn Ihnen die Reihenfolge egal ist.


0
2018-01-15 06:46



Nachdem ich ein paar andere Posts gelesen und eine Weile herumgebastelt hatte, kam ich zu folgendem Ergebnis (ähnlich wie oben, aber die Implementierung ist etwas anders). Ich schrieb dies mit dem Visual Studio 2013 Compiler.

Verwendung eines Lambda-Ausdrucks -

static_for_each()(
    [](std::string const& str)
    {
        std::cout << str << std::endl;
    }, "Hello, ", "Lambda!");

Der Nachteil bei der Verwendung eines Lambda ist, dass die Parameter vom selben Typ sein müssen, der in der Parameterliste des Lambda angegeben ist. Dies bedeutet, dass es nur mit einem Typ funktioniert. Wenn Sie eine Vorlagenfunktion verwenden möchten, können Sie das nächste Beispiel verwenden.

Verwendung mit Struct Wrapper Funktor -

struct print_wrapper
{
    template <typename T>
    void operator()(T&& str)
    {
        std::cout << str << " ";
    }
};

// 
// A little test object we can use.
struct test_object
{
    test_object() : str("I'm a test object!") {}
    std::string str;
};

std::ostream& operator<<(std::ostream& os, test_object t)
{
    os << t.str;
    return os;
}

//
// prints: "Hello, Functor! 1 2 I'm a test object!"
static_for_each()(print_wrapper(), "Hello,", "Functor!", 1, 2.0f, test_object());

Auf diese Weise können Sie beliebige Typen eingeben und mit dem Funktor bearbeiten. Ich fand das ziemlich sauber und funktioniert gut für das, was ich wollte. Sie können es auch mit einem Funktionsparameterpaket wie diesem verwenden -

template <typename T, typename... Args>
void call(T f, Args... args)
{
    static_for_each()(f, args...);
}

call(print_wrapper(), "Hello", "Call", "Wrapper!");

Hier ist die Implementierung -

// 
// Statically iterate over a parameter pack 
// and call a functor passing each argument.
struct static_for_each
{
private:
    // 
    // Get the parameter pack argument at index i.
    template <size_t i, typename... Args>
    static auto get_arg(Args&&... as) 
    -> decltype(std::get<i>(std::forward_as_tuple(std::forward<Args>(as)...)))
    {
        return std::get<i>(std::forward_as_tuple(std::forward<Args>(as)...));
    }

    //
    // Recursive template for iterating over 
    // parameter pack and calling the functor.
    template <size_t Start, size_t End>
    struct internal_static_for
    {
        template <typename Functor, typename... Ts>
        void operator()(Functor f, Ts&&... args)
        {
            f(get_arg<Start>(args...));
            internal_static_for<Start + 1, End>()(f, args...);
        }
    };

    //
    // Specialize the template to end the recursion.
    template <size_t End>
    struct internal_static_for<End, End>
    {
        template <typename Functor, typename... Ts>
        void operator()(Functor f, Ts&&... args){}
    };

public:
    // 
    // Publically exposed operator()(). 
    // Handles template recursion over parameter pack.
    // Takes the functor to be executed and a parameter 
    // pack of arguments to pass to the functor, one at a time.
    template<typename Functor, typename... Ts>
    void operator()(Functor f, Ts&&... args)
    {
        // 
        // Statically iterate over parameter
        // pack from the first argument to the
        // last, calling functor f with each 
        // argument in the parameter pack.
        internal_static_for<0u, sizeof...(Ts)>()(f, args...);
    }
};

Hoffe Leute finden das nützlich :-)


0
2017-11-07 21:56