Pertanyaan Bagaimana cara membuat perhitungan generik atas paket argumen heterogen dari fungsi template variadic?


PREMISE:

Setelah bermain-main dengan template variadic sedikit, saya menyadari bahwa mencapai apa pun yang berjalan sedikit di luar tugas-tugas meta-pemrograman sepele segera menjadi cukup rumit. Secara khusus, saya menemukan diri saya berharap cara untuk melakukan operasi generik atas paket argumen seperti iterate, membagi, lingkaran di sebuah std::for_each-seperti mode, dan sebagainya.

Setelah menonton kuliah ini oleh Andrei Alexandrescu dari C ++ dan Beyond 2012 pada keinginan static if ke dalam C + + (sebuah konstruk yang dipinjam dari Bahasa Pemrograman D) Saya punya perasaan semacam itu static for akan berguna juga - dan saya merasa lebih dari ini static konstruksi bisa membawa manfaat.

Jadi saya mulai bertanya-tanya apakah ada cara untuk mencapainya sesuatu seperti ini untuk paket argumen dari fungsi template variadic (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));
    }
}

Yang akan diterjemahkan pada saat kompilasi menjadi sesuatu seperti ini:

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

Pada prinsipnya, static_for akan memungkinkan pemrosesan yang lebih rumit lagi:

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

Atau untuk idiom yang lebih ekspresif seperti ini:

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

PEKERJAAN YANG BERHUBUNGAN:

Saya melakukan pencarian di Web dan menemukan itu sesuatu memang ada:

  • Link ini menjelaskan cara mengonversi paket parameter menjadi vektor Boost.MPL, tetapi itu hanya berjalan setengah jalan (jika tidak kurang) menuju sasaran;
  • pertanyaan ini pada SO nampaknya meminta fitur meta-programming yang sama dan sedikit terkait (memisahkan paket argumen menjadi dua bagian) - sebenarnya, ada beberapa pertanyaan di SO yang tampaknya terkait dengan masalah ini, tetapi tidak ada jawaban yang saya baca memecahkannya IMHO memuaskan;
  • Boost.Fusion mendefinisikan algoritma untuk mengonversi paket argumen menjadi tuple, tapi saya lebih suka:
    1. tidak menciptakan jaman yang tidak perlu untuk menahan argumen yang dapat (dan seharusnya) secara sempurna diteruskan ke beberapa algoritme generik;
    2. punya kecil, mandiri perpustakaan untuk melakukan itu, sementara Boost.Fusion kemungkinan akan memasukkan lebih banyak barang daripada yang diperlukan untuk mengatasi masalah ini.

PERTANYAAN:

Apakah ada cara yang relatif sederhana, mungkin melalui beberapa template meta-programming, untuk mencapai apa yang saya cari tanpa menimbulkan keterbatasan dalam pendekatan yang ada?


75
2018-01-10 15:19


asal


Jawaban:


Karena saya tidak senang dengan apa yang saya temukan, saya mencoba mencari solusi sendiri dan akhirnya menulis a perpustakaan kecil yang memungkinkan merumuskan operasi generik pada paket argumen. Solusi saya memiliki beberapa fitur berikut:

  • Memungkinkan pengulangan atas semua atau beberapa elemen dari paket argumen, mungkin ditentukan oleh komputasi indeks mereka pada paket;
  • Memungkinkan meneruskan bagian-bagian terkomputasi dari paket argumen ke fungsi variadic;
  • Hanya membutuhkan termasuk satu file header yang relatif singkat;
  • Membuat ekstensif penggunaan forwarding yang sempurna untuk memungkinkan inlining yang berat dan menghindari salinan / gerakan yang tidak perlu untuk memungkinkan kerugian kinerja minimum;
  • Implementasi internal dari algoritma iterasi bergantung pada Optimasi Kosong Basis Kelas untuk meminimalkan konsumsi memori;
  • Ini mudah (relatif, mengingat itu template meta-programming) untuk memperpanjang dan beradaptasi.

Saya pertama kali akan tampil apa yang bisa dilakukan dengan perpustakaan, kemudian posting pelaksanaan.

GUNAKAN KASUS

Berikut ini contoh bagaimana for_each_in_arg_pack() fungsi dapat digunakan untuk iterasi melalui semua argumen paket dan meneruskan setiap argumen dalam input ke beberapa fungsi yang disediakan klien (tentu saja, functor harus memiliki operator panggilan generik jika paket argumen berisi nilai-nilai tipe heterogen):

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

Itu print functor di atas juga dapat digunakan dalam perhitungan yang lebih kompleks. Secara khusus, di sini adalah bagaimana seseorang akan beralih pada subset (dalam hal ini, a sub-jangkauan) dari argumen dalam 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
        );
}

Terkadang, seseorang mungkin hanya ingin meneruskan sebagian dari paket argumen ke beberapa functor variadic lain daripada iterasi melalui elemen-elemennya dan melewati masing-masing secara individual ke non-variadic functor. Inilah yang forward_subpack() algoritma memungkinkan melakukan:

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

Untuk tugas yang lebih spesifik, tentu saja mungkin untuk mengambil argumen spesifik dalam paket oleh pengindeksan mereka. Inilah yang nth_value_of() fungsi memungkinkan melakukan, bersama dengan para pembantunya first_value_of() dan 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;
}

Jika paket argumen homogen di sisi lain (yaitu semua argumen memiliki tipe yang sama), formulasi seperti yang di bawah ini mungkin lebih disukai. Itu is_homogeneous_pack<> meta-fungsi memungkinkan menentukan apakah semua jenis dalam paket parameter bersifat homogen, dan terutama dimaksudkan untuk digunakan static_assert() pernyataan:

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

Akhirnya, sejak itu lambdas hanya saja gula sintaksis untuk fungsi, mereka dapat digunakan juga dalam kombinasi dengan algoritma di atas; Namun, hingga lambdas generik akan didukung oleh C ++, ini hanya dimungkinkan untuk homogen paket argumen. Contoh berikut juga menunjukkan penggunaan homogeneous-type<> meta-function, yang mengembalikan tipe semua argumen dalam paket homogen:

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

Ini pada dasarnya adalah apa yang diizinkan oleh perpustakaan, tetapi saya percaya bahkan bisa diperpanjang untuk melakukan tugas yang lebih rumit.

PELAKSANAAN

Sekarang sampai pada implementasinya, yang agak sedikit rumit sehingga saya akan mengandalkan komentar untuk menjelaskan kode dan menghindari membuat posting ini terlalu lama (mungkin sudah ada):

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

KESIMPULAN

Tentu saja, meskipun saya memberikan jawaban saya sendiri untuk pertanyaan ini (dan sebenarnya karena fakta ini), saya ingin tahu apakah solusi alternatif atau yang lebih baik ada yang saya lewatkan - terlepas dari yang disebutkan di bagian "Pekerjaan Terkait" dari pertanyaan itu.


63
2018-01-10 15:19



Biarkan saya memposting kode ini, berdasarkan diskusi:

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

Saya memeriksa kode yang dihasilkan dengan g++ -std=c++11 -O1 dan main hanya berisi 3 panggilan ke print, tidak ada jejak pembantu ekspansi.


9
2018-01-10 19:17



Menggunakan sebuah menghitung solusi (ala Python).

Pemakaian:

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

Kode:

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

Pembangun jangkauan (yang diambil dari solusi Anda):

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



Notasi ... memang memiliki beberapa opsi menarik, seperti:

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

Sayangnya, saya tidak tahu cara apa pun untuk menegakkan urutan di mana fungsi cetak dipanggil (sebaliknya, di kompilator saya). Perhatikan bahwa cetak perlu mengembalikan sesuatu.

Trik ini dapat berguna jika Anda tidak peduli dengan pesanan.


0
2018-01-15 06:46



Setelah membaca beberapa posting lain dan mengotak-atik untuk sementara saya datang dengan yang berikut (agak mirip dengan di atas, tetapi pelaksanaannya sedikit berbeda). Saya menulis ini menggunakan compiler Visual Studio 2013.

Penggunaan menggunakan ekspresi lambda -

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

Kelemahan saat menggunakan lambda adalah parameter harus dari jenis yang sama yang dideklarasikan dalam daftar parameter lambda. Ini berarti hanya akan bekerja dengan satu jenis. Jika Anda ingin menggunakan fungsi templated, Anda dapat menggunakan contoh berikut.

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

Ini memungkinkan Anda untuk lulus dalam jenis apa pun yang Anda inginkan dan mengoperasikannya menggunakan functor. Saya menemukan ini cukup bersih dan bekerja dengan baik untuk apa yang saya inginkan. Anda juga dapat menggunakannya dengan paket parameter fungsi seperti ini -

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

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

Berikut ini implementasinya -

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

Semoga orang-orang menemukan ini berguna :-)


0
2017-11-07 21:56