0

I'm writing a Data-Oriented programming registry, which kind of looks like a ECS registry and works like this:

void KillEnemy(EnemyList &enemies, ProjectileList &projectiles);

int main() {
    auto registry
     = Registry{}
      .add_data(EnemyList{})
      .add_data(ProjectileList{})
      .add_data(TerrainList{});
    //automatically call KillEnemy with proper arguments.
    registry.call(KillEnemy);
}

I haven't figured out the add_data part yet, but I think it could be done with constexpr and std::tuple. My current implementation for Registry looks like this:

struct Registry {
    std::tuple<int, double, float> data;
    template<typename R, typename... Args>
    R call(R (*f)(Args...)) {
        return f(std::get<Args>(data)...);
    }
};

This works for function pointers, but it would be more ergonomic if I could use it with lambdas. I can't figure out a way to deduce the parameters. Is it possible?

Update: I just wrote add_data. This is my current implementation:


template <typename... DataType>
struct Reg {
    std::tuple<DataType...> data;
    template<typename R, typename... Args>
    R call(R (*f)(Args...)) {
        return f(std::get<std::remove_reference_t<Args>>(data)...);
    }
    template<typename T>
    constexpr Reg<T, DataType...> add_data(T new_data = {}) &&{
        return {std::tuple_cat(std::move(data), std::tuple<T>(new_data))};
    }
};

int main() {
    auto r = Reg<>{}
        .add_data<int>()
        .add_data<double>()
        .add_data<float>();
    r.call(system_with_duble);
    r.call(system_with_float);
    r.call(system_with_double_and_float);
}
  • You have the parameter types from the `add_data` calls. Can you make `Registry` a template class and just keep track of the parameters *there*? That would likely be far easier than trying to reconstruct them from the lambda. – Silvio Mayolo May 07 '23 at 02:56
  • @SilvioMayolo The ```Registry``` does keep track of them, but I didn't write a generic ```add_data``` and used a fixed ```std::tuple```. I updated a new generic edition just now. – RevolverOcelot May 07 '23 at 03:10
  • Does https://stackoverflow.com/questions/39714375/matching-a-c-lambda-expression-in-templates answer your question? I would prefer `std::function` and let user convert, more clear interface. – KamilCuk May 07 '23 at 03:41
  • @KamilCuk I don't think so. I need the types of parameters in the definition for ```std::get```. I have figured out a way using helper function and pointer. – RevolverOcelot May 07 '23 at 03:45

1 Answers1

0

I just figured it out:

struct Registry {
    std::tuple<DataType...> data;
    template<typename R, typename... Args>
    R call(R (*f)(Args...)) {
        return f(std::get<std::remove_reference_t<Args>>(data)...);
    }
    template<typename F>
    auto call(F functor) {
        return functor_call_helper(&functor, &F::operator());
    }
    template<typename T>
    Registry<T, DataType...> add_data() & = delete;
    template<typename T>
    constexpr Registry<T, DataType...> add_data(T new_data = {}) &&{
        return {std::tuple_cat(std::move(data), std::tuple<T>(new_data))};
    }

private:
    template <typename T, typename R, typename... Args>
    R functor_call_helper(T *callable, R (T::* f)(Args...) const) {
        return (callable->*f)(std::get<std::remove_reference_t<Args>>(data)...);
    }
};

int main() {
    auto r = Registry<>{}.add_data<int>().add_data<double>().add_data<float>();
    r.call(system_with_double);
    r.call(system_with_float);
    r.call(system_with_float_and_double);
    r.call([](int i){std::cout<<i<<'\n';});
}
  • 1
    It misses some `functor_call_helper` overloads to handle mutable lambda derived Functor (when `T* callable` doesn't match exactly with `(T::*)`) (and (empty) C-ellipsis ;-) ) – Jarod42 May 07 '23 at 08:47
  • @Jarod42 Thank you for pointing out! (Though to be honest I don't quite understand that since I'm still a C++ noob.) If anyone wants to do the same thing I hope they can improve this on that. I decided to scrub it, because my initial goal was to make a scheduler that automatically parallelizes systems by checking the types and ```const``` qualifiers (like bevy), and in doing so I found it too difficult and distract me from my plan (learning graphics and physics simulation). – RevolverOcelot May 07 '23 at 10:36
  • I mean `r.call([int i = 0](int) mutable { std::cout << ++i << '\n'; });` would require `R functor_call_helper(T *callable, R (T::* f)(Args...))`. – Jarod42 May 08 '23 at 07:23