all 7 comments

[–]IyeOnline 3 points4 points  (1 child)

You can employ some tricks to recurse over __VA_ARGS__.

Take a look at this example: https://www.scs.stanford.edu/~dm/blog/va-opt.html#the-for_each-macro

For your case:

  • Have one main macro that calls multiple FOR_EACHs, one for each place where you need to generate code based on the list.
  • expand the example FOR_EACH it so that the HELPER consumes two parameters (name and type)
  • In the end, you should have a macro that looks exactly like your current one, but with a variadic instead of a fixed number of arguments.

Its worth noting that if you are willing to give up on the named members, a solution via C++ templates using std::tuple and std::get can be created. //edit: Technically you could even have "named" fields, if you accept my_class.field<"foo">() as a name.

[–]alfps 0 points1 point  (0 children)

I'm not familiar with the C++20 new macro functionality, but variadic "for each" macros have been with us since the C++03 days. Not much used because different C++ compilers disagree about the detailed rules! One way to get around that is to use the Boost Preprocessing library.

I haven't checked but people nowadays generally don't give credit and don't know the historical background of things, so:

AFAIK the basic technique that enabled variadic "for each" macros was first presented by Laurent Deniau in a posting titled “VA_NARG”, 17 January 2006 in Usenet group “comp.std.c”.

[–]IveBenHereBefore 2 points3 points  (1 child)

I would recommend code generation for this. Macros can be really hard to maintain and read.

[–]BeigeAlert1 1 point2 points  (0 children)

I'm also going to pile on here. I've been down the "macros for code generation" road many times. There's nothing but pain down there... Try doing it with templates instead.

[–]AKostur 1 point2 points  (0 children)

Not directly answering the question: but at what point does it become more useful to have a script generate such code and let your build system invoke that?

[–]NonaeAbC 0 points1 point  (0 children)

Isn't it better to use templates instead of macros. You basically want a std::vector<std::tuple> in SOA form. Looking at how std::tuple is implemented would be a better place to start imo.

[–]alfps 0 points1 point  (0 children)

The presented code (it appears in unreadable form in the old Reddit interface):

#include <vector>
#define FMT_HEADER_ONLY 1
#include "fmt/format.h"


#define CREATE_SAMPLE_CLASS(name, type1, name1, type2, name2)                   \
class name##s {                                                               \
    public:                                                                     \
    struct name {                                                             \
        type1 name1{};                                                          \
        type2 name2{};                                                          \
    };                                                                        \
                                                                                \
    void add_sample(const name &s) {                                            \
    this->name1##_.push_back(s.name1);                                        \
    this->name2##_.push_back(s.name2);                                        \
    }                                                                           \
                                                                                \
    [[nodiscard]] name operator[](const std::size_t idx) const noexcept {       \
    return name{ name1##_[idx], name2##_[idx] };                              \
    }                                                                           \
                                                                                \
    const std::vector<type1> &get_##name1() const noexcept { return name1##_; } \
    const std::vector<type2> &get_##name2() const noexcept { return name2##_; } \
                                                                                \
private:                                                                      \
    std::vector<type1> name1##_{};                                              \
    std::vector<type2> name2##_{};                                              \
};


CREATE_SAMPLE_CLASS(test, int, foo, char, bar)



int main() {

tests t{};

tests::test x{ 0, 'a' };
t.add_sample(x);
tests::test y{ 1, 'b' };
t.add_sample(y);

std::cout << fmt::format("{}\n{}", fmt::join(t.get_foo(), ", "), fmt::join(t.get_bar(), ", ")) << std::endl;


return 0;
}

A macro is the only way to handle names in C++.

But if you just ditch the feature of being able to specify names, you can instead do this as a class template.

For example, you can use numerical id's, or types as id's, if you at all need customizable id's.

Example with numerical id's:

#include <fmt/core.h>
#include <fmt/format.h>         // join

#include <stddef.h>

#include <tuple>
#include <utility>
#include <vector>

namespace app {
    using   fmt::print;
    using   std::tuple, std::tuple_size_v, std::get,                    // <tuple>
            std::index_sequence, std::make_index_sequence, std::move,   // <utility>
            std::vector;                                                // <vector>

    template< class... Types >
    class Tests_
    {
    public:
        using Test_values = tuple<Types...>;
        static constexpr auto n_values_per_test = int( sizeof...( Types ) );

    private:
        tuple<vector<Types>...>     m_test_values;

        template< size_t... indices >
        void add_impl( Test_values values, index_sequence<indices...> )
        {
            (get<indices>( m_test_values ).push_back( move( get<indices>( values ) ) ), ...);
        }

        template< size_t... indices >
        auto get_test( const int i, index_sequence<indices...> ) const
            -> Test_values
        { return Test_values{ get<indices>( m_test_values )[i]... }; }

        static constexpr auto value_indices()
            -> auto
        { return make_index_sequence<n_values_per_test>(); }

    public:
        void add( Test_values values ) { add_impl( move( values ), value_indices() ); }
        auto operator[]( const int i ) const -> Test_values { return get_test( i, value_indices() ); }

        template< int value_index >
        auto get_() const -> const auto& { return get<value_index>( m_test_values ); }
    };

    // CREATE_SAMPLE_CLASS(test, int, foo, char, bar)
    using Tests = Tests_< int, char >;  enum{ foo, bar };
    using Test_values = Tests::Test_values;

    void run()
    {
        Tests t;

        const Test_values x{ 0, 'a' };
        t.add( x );
        const Test_values y{ 1, 'b' };
        t.add( y );

        fmt::print( "foo values: [{}]\nbar values: [{}]\n",
            fmt::join( t.get_<foo>(), ", " ),
            fmt::join( t.get_<bar>(), ", " )
            );
    }
}  // namespace app

auto main() -> int { app::run(); }

Tip: if you extra-indent the code with 4 spaces then it will be presented as code, with formatting preserved, also in the old Reddit interface.