Variadic sequence of pointer to recursive member of struct/class as template parameter
Asked Answered
C

3

9

I'm struggling with some template programming and I hope you can give me some help. I coded a C++11 interface that, given some structs like:

struct Inner{
  double a;
};
struct Outer{
  double x, y, z, r;
  Inner in;
};

Implements a getter/setter to the real data that is customized to the specified struct members:

MyData<Outer, double, &Outer::x,
                               &Outer::y, 
                               &Outer::z,
                               &Outer::in::a //This one is not working
              > state();

Outer foo = state.get();
//...  
state.set(foo);

I managed to implement this for simple structs in the following way:

template <typename T, typename U, U T::* ... Ms>
class MyData{
   std::vector<U *> var;
  public:
    explicit MyData();
    void set(T const& var_);
    T get() const;
};

template <typename T, typename U, U T::* ... Ms>
MyData<T, U, Ms ... >::Struct():var(sizeof...(Ms))
{
}

template <typename T, typename U, U T::* ... Ms>
void MyData<T, U, Ms ...>::set(T const& var_){
  unsigned i = 0;
  for ( auto&& d : {Ms ...} ){
    *var[i++] = var_.*d;
  }
}

template <typename T, typename U, U T::* ... Ms>
T MyData<T, U, Ms ...>::get() const{
  T var_;
  unsigned i = 0;
  for ( auto&& d : {Ms ...} ){
    var_.*d = *var[i++];
  }
  return var_;
}

But it fails when I pass a member of a nested struct. Ideally, I'd like to implement a generic pointer to member type that allows me to be compatible with several levels of scope resolutions. I found this approach, but I'm not sure if this should be applied to my problem or if there exists some implementation ready to use. Thanks in advance!

Related posts:

Implicit template parameters

Pointer to inner struct

Coryza answered 26/11, 2018 at 13:32 Comment(3)
*var[i++] when the objects are not allocated seems fishy?Amelita
By "MyData<TestStruct ..." do you mean "MyData<Outer ...", so that the return type T of get() is Outer?Leavitt
@MatthieuBrucher The vector var points to existing data, but that gets out of the scope of the question. Thanks for the interest though!Coryza
U
7

You might wrap member pointer into struct to allow easier chaining:

template <typename...> struct Accessor;

template <typename T, typename C, T (C::*m)>
struct Accessor<std::integral_constant<T (C::*), m>>
{
    const T& get(const C& c) { return c.*m; }
    T& get(C& c) { return c.*m; }
};

template <typename T, typename C, T (C::*m), typename ...Ts>
struct Accessor<std::integral_constant<T (C::*), m>, Ts...>
{
    auto get(const C& c) -> decltype(Accessor<Ts...>().get(c.*m))
    { return Accessor<Ts...>().get(c.*m); }

    auto get(C& c) -> decltype(Accessor<Ts...>().get(c.*m))
    { return Accessor<Ts...>().get(c.*m); }
};

template <typename T, typename U, typename ...Ts>
class MyData
{
    std::vector<U> vars{sizeof...(Ts)};

    template <std::size_t ... Is>
    T get(std::index_sequence<Is...>) const
    {
        T res;
        ((Ts{}.get(res) = vars[Is]), ...); // Fold expression C++17
        return res;
    }
    template <std::size_t ... Is>
    void set(std::index_sequence<Is...>, T const& t)
    {
        ((vars[Is] = Ts{}.get(t)), ...); // Fold expression C++17
    }

public:
    MyData() = default;

    T get() const { return get(std::index_sequence_for<Ts...>()); }
    void set(const T& t) { return set(std::index_sequence_for<Ts...>(), t); }

};

With usage similar to

template <auto ...ms> // C++17 too
using Member = Accessor<std::integral_constant<decltype(ms), ms>...>;

MyData<Outer, double, Member<&Outer::x>,
                           Member<&Outer::y>,
                           Member<&Outer::z>,
                           Member<&Outer::in, &Inner::a>
       > state;

std::index_sequence is C++14 but can be implemented in C++11.
Folding expression from C++17 can be simulated too in C++11.
typename <auto> (C++17) should be replaced by typename <typename T, T value>.

Demo

Underpinnings answered 26/11, 2018 at 14:11 Comment(2)
Since I expect to have multiple levels of scope resolution, Is it possible to wrap Member<&Outer::in, &Inner::a> to Member<&Outer::in::a> ? Thanks in advance :)Coryza
&Outer::in::a is invalid unfortunately (currently it is parsed as member a of inner class Outer::in, but Outer::in is not a class, but a member). So you have to split it. MACRO would allow to write it MEMBER(Outer, in, a).Underpinnings
C
3

A generalization of a member pointer is a function that can map T to X& at compile time.

In it isn't hard to wire things up thanks to auto. In it gets harder. But the basic idea is that you don't actually pass member pointers, you pass types, and those types know how to take your class and get a reference out of them.

template<class T, class D, class...Fs>
struct MyData {
  std::array<D*, sizeof...(Fs)> var = {};
  explicit MyData()=default;
  void set(T const& var_) {
    var = {{ Fs{}(std::addressof(var_))... }};
  }
  T get() {
    T var_;
    std::size_t index = 0;
    using discard=int[];
    (void)discard{ 0, (void(
      *Fs{}(std::addressof(var_)) = *var[index++]
    ),0)... };
    return var_;
  }
};

it remains to write a utility that makes writing the Fs... easy for the member pointer case

template<class X, X M>
struct get_ptr_to_member_t;
template<class T, class D, D T::* M>
struct get_ptr_to_member_t< D T::*, M > {
  D const* operator()( T const* t )const{
    return std::addressof( t->*M );
  }
};
#define TYPE_N_VAL(...) \
  decltype(__VA_ARGS__), __VA_ARGS__
#define MEM_PTR(...) get_ptr_to_member_t< TYPE_N_VAL(__VA_ARGS__) >

now the basic case is

MyData< Outer, double, MEM_PTR(&Outer::x), MEM_PTR(&Outer::y) >

The more complex case can now be handled.

An approach would be to teach get_ptr_to_member to compose. This is annoying work, but nothing fundamental. Arrange is so that decltype(ptr_to_member_t * ptr_to_member_t) returns a type that instances right, applies it, then takes that pointer and runs the left hand side on it.

template<class First, class Second>
struct composed;

template<class D>
struct composes {};

#define RETURNS(...) \
  noexcept(noexcept(__VA_ARGS__)) \
  decltype(__VA_ARGS__) \
  { return __VA_ARGS__; }

template<class First, class Second>
struct composed:composes<composed<First, Second>> {
  template<class In>
  auto operator()(In&& in) const
  RETURNS( Second{}( First{}( std::forward<In>(in) ) ) )
};

template<class First, class Second>
composed<First, Second> operator*( composes<Second> const&, composes<First> const& ) {
  return {};
}

then we upgrade:

template<class X, X M>
struct get_ptr_to_member_t;
template<class T, class D, D T::* M>
struct get_ptr_to_member_t< D T::*, M >:
  composes<get_ptr_to_member_t< D T::*, M >>
{
  D const* operator()( T const* t )const{
    return std::addressof( t->*M );
  }
};

and now * composes them.

MyData<TestStruct, double, MEM_PTR(&Outer::x),
                           MEM_PTR(&Outer::y), 
                           MEM_PTR(&Outer::z),
                           decltype(MEM_PTR(&Inner::a){} * MEM_PTR(&Outer::in){})
          > state();

answre probably contains many typos, but design is sound.

In most of the garbage evaporates, like the macros.

Claybourne answered 26/11, 2018 at 14:56 Comment(0)
T
1

I would use lambda approach to implement similar functionalities in C++17(C++14 is also ok, just change the fold expression):

auto access_by() {
    return [] (auto &&t) -> decltype(auto) {
        return decltype(t)(t);
    };
}

template<class Ptr0, class... Ptrs>
auto access_by(Ptr0 ptr0, Ptrs... ptrs) {
    return [=] (auto &&t) -> decltype(auto) {
        return access_by(ptrs...)(decltype(t)(t).*ptr0);
    };
}

auto data_assigner_from = [] (auto... accessors) {
    return [=] (auto... data) {
        return [accessors..., data...] (auto &&t) {
            ((accessors(decltype(t)(t)) = data), ...);
        };
    };
};

Let's see how to use these lambdas:

struct A {
    int x, y;
};

struct B {
    A a;
    int z;
};

access_by function can be used like:

auto bax_accessor = access_by(&B::a, &A::x);
auto bz_accessor = access_by(&B::z);

Then for B b;, bax_accessor(b) is b.a.x; bz_accessor(b) is b.z. Value category is also preserved, so you can assign: bax_accessor(b) = 4.

data_assigner_from() will construct an assigner to assign a B instance with given accessors:

auto data_assigner = data_assigner_from(
        access_by(&B::a, &A::x),
        access_by(&B::z)
     );

data_assigner(12, 3)(b);

assert(b.z == 3 && b.a.x == 12);
Thallophyte answered 26/11, 2018 at 14:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.