Hoe maak je generieke berekeningen over heterogene argumentpakketten van een variadische sjabloonfunctie?

PREMIS:

Na een beetje met variadische sjablonen te hebben gespeeld, realiseerde ik me dat het al snel behoorlijk omslachtig wordt om iets te bereiken dat iets verder gaat dan de triviale meta-programmeertaken. In het bijzonder merkte ik dat ik een manier wenste om algemene bewerkingen uit te voeren over een argumentpakketzoals itereren, split, loopop een std::for_each-achtige manier, enzovoort.

Na het bekijken van deze lezing door Andrei Alexandrescuvan C++ and Beyond 2012 over de wenselijkheid van static ifin C++ (een constructie geleend van de D-programmeertaal) Ik had het gevoel dat een soort static forook van pas zou komen – en ik voel meer van deze staticconstructies kunnen voordelen opleveren.

Dus ik begon me af te vragen of er een manier is om iets als ditte bereiken voor argumentpakketten van een variadische sjabloonfunctie (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));
    }
}

Wat op het moment van compilerenzou worden vertaald in zoiets als dit:

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

In principe zou static_foreen nog uitgebreidere verwerking mogelijk maken:

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

Of voor een meer expressief idioom zoals dit:

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

GERELATEERD WERK:

Ik heb wat op internet gezocht en ontdekte dat ietsinderdaad bestaat:

  • Dit linkbeschrijft hoe je een parameterpakket omzet in een Boost.MPL-vector, maar dat gaat maar half (zo niet minder) naar het doel;
  • deze vraag over SOlijkt te vragen om een ​​vergelijkbare en enigszins verwante functie voor metaprogrammering (een argumentpakket in twee helften splitsen) – eigenlijk zijn er verschillende vragen over SO die verband lijken te houden met dit probleem, maar geen van de antwoorden die ik heb gelezen, lost het naar tevredenheid op IMHO;
  • Boost.Fusiondefinieert algoritmen voor het omzetten van een argumentpakket in een tuple, maar ik heb liever:
    1. geen onnodige tijdelijke te maken om argumenten vast te houden die perfect kunnen (en zouden moeten) worden doorgestuurd naar een aantal generieke algoritmen;
    2. hebben een kleine, op zichzelf staandebibliotheek om dat te doen, terwijl Boost.Fusion waarschijnlijk veel meer dingen zal bevatten dan nodig is om dit probleem aan te pakken.

VRAAG:

Is er een relatief eenvoudige manier, mogelijk door middel van een metaprogrammering van sjablonen, om te bereiken wat ik zoek zonder de beperkingen van de bestaande benaderingen op te lopen?


Antwoord 1, autoriteit 100%

Omdat ik niet tevreden was met wat ik vond, probeerde ik zelf een oplossing uit te werken en eindigde met het schrijven van een kleine bibliotheekwaarmee generieke bewerkingen op argumentpakketten kunnen worden geformuleerd. Mijn oplossing heeft de volgende kenmerken:

  • Hiermee kunnen alle of sommigeelementen van een argumentpakket worden herhaald, mogelijk gespecificeerd door berekenenhun indices op het pakket;
  • Hiermee kunnen berekende delen van een argumentpakket worden doorgestuurd naar variadische functors;
  • Vereist slechts één relatief kort headerbestand;
  • Maakt uitgebreid gebruik van perfect forwarding om zware inlining mogelijk te maken en vermijdt onnodige kopieën/verplaatsingen om een ​​minimaal prestatieverlies mogelijk te maken;
  • De interne implementatie van de itererende algoritmen is gebaseerd op Empty Base Class Optimization om het geheugenverbruik te minimaliseren;
  • Het is (relatief gezien de meta-programmering van sjablonen) gemakkelijk uit te breiden en aan te passen.

Ik zal eerst laten zien wat er gedaan kan wordenmet de bibliotheek, vervolgensde implementatieposten.

GEBRUIKSGEVALLEN

Hier is een voorbeeld van hoe de functie for_each_in_arg_pack()kan worden gebruikt om alle argumenten van een pakket te doorlopen en elk argument als invoer door te geven aan een door de klant geleverde functie (uiteraard de functor moet een generieke oproepoperator hebben als het argumentpakket waarden van heterogene typen bevat):

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

De functie printhierboven kan ook worden gebruikt in complexere berekeningen. Hier is in het bijzonder hoe men een subset(in dit geval een subbereik) van de argumenten in een pakket zou herhalen:

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

Soms wil je misschien gewoon een deelvan een argumentpakket doorsturen naar een andere variadische functor in plaats van de elementen te doorlopen en ze allemaal individueeldoor te geven aan een niet-variadische functor. Dit is wat het forward_subpack()-algoritme mogelijk maakt:

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

Voor meer specifieke taken is het natuurlijk mogelijk om specifieke argumenten in een pakket op te halen door ze te indexeren. Dit is wat de functie nth_value_of()mogelijk maakt, samen met zijn helpers first_value_of()en 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;
}

Als het argumentenpakket daarentegen homogeenis (d.w.z. alle argumenten hebben hetzelfde type), kan een formulering zoals hieronder de voorkeur hebben. De metafunctie is_homogeneous_pack<>maakt het mogelijk te bepalen of alle typen in een parameterpakket homogeen zijn, en is voornamelijk bedoeld om te worden gebruikt in static_assert()-statements:

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

Ten slotte, aangezien lambda’sslechts syntactische suikerzijn voor functors, kunnen ze ook worden gebruikt in combinatie met de bovenstaande algoritmen; totdat generic lambdasechter wordt ondersteund door C++, is dit alleen mogelijk voor homogeneargumentpakketten. Het volgende voorbeeld toont ook het gebruik van de homogeneous-type<>meta-functie, die het type van alle argumenten in een homogeen pakket retourneert:

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

Dit is in principe wat de bibliotheek toestaat, maar ik geloof dat het zelfs kan worden uitgebreidom complexere taken uit te voeren.

IMPLEMENTATIE

Nu komt de implementatie, die op zichzelf een beetje lastig is, dus ik zal vertrouwen op opmerkingen om de code uit te leggen en te voorkomen dat dit bericht te lang wordt (misschien is het dat al):

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

CONCLUSIE

Natuurlijk, hoewel ik mijn eigen antwoord op deze vraag heb gegeven (en eigenlijk vanwegedit feit), ben ik benieuwd of er alternatieve of betere oplossingen zijn die ik heb gemist – afgezien van degene die worden genoemd in het gedeelte “Verwante werken” van de vraag.


Antwoord 2, autoriteit 15%

Laat me deze code posten, gebaseerd op de discussie:

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

Ik heb de gegenereerde code gecontroleerd met g++ -std=c++11 -O1en mainbevat slechts 3 aanroepen naar print, daar is geen spoor van de uitbreidingshelpers.


Antwoord 3, autoriteit 8%

Een enumerateoplossing gebruiken (ala Python).

Gebruik:

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

De bereikbouwer (overgenomen van uw oplossing):

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

Antwoord 4

De … notatie heeft enkele interessante opties, zoals:

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

Helaas ken ik geen enkele manier om de volgorde af te dwingen waarin de printfuncties worden aangeroepen (omgekeerd, op mijn compiler). Houd er rekening mee dat print iets moet retourneren.

Deze truc kan handig zijn als je niet om bestelling geeft.


Antwoord 5

Na het lezen van een paar andere berichten en een tijdje knutselen kwam ik op het volgende (enigszins vergelijkbaar met het bovenstaande, maar de implementatie is een beetje anders). Ik heb dit geschreven met de Visual Studio 2013-compiler.

Gebruik met een lambda-expressie –

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

Het nadeel bij het gebruik van een lambda is dat de parameters van hetzelfde type moeten zijn als aangegeven in de parameterlijst van de lambda. Dit betekent dat het maar met één type werkt. Als u een sjabloonfunctie wilt gebruiken, kunt u het volgende voorbeeld gebruiken.

Gebruik met struct wrapper functor –

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

Hiermee kunt u alle typen doorgeven die u wilt en ze bedienen met behulp van de functor. Ik vond dit vrij schoon en werkt goed voor wat ik wilde. Je kunt het ook gebruiken met een functieparameterpakket zoals dit –

template <typename T, typename... Args>
void call(T f, Args... args)
{
    static_for_each()(f, args...);
}
call(print_wrapper(), "Hello", "Call", "Wrapper!");

Hier is de implementatie –

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

Ik hoop dat mensen dit nuttig vinden 🙂

Other episodes