Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Q: make REFL_AUTO, REFL_TYPE etc. callable from nested namespace #59

Open
RalphSteinhagen opened this issue Dec 24, 2021 · 7 comments
Open

Comments

@RalphSteinhagen
Copy link
Contributor

RalphSteinhagen commented Dec 24, 2021

Hi @veselink1,
thanks again for time and making refl-cpp public! We gained quite a bit more experience with your library and are happily using it for (de-)serialisation in our little lib.

From a usability point of view: what would it take to make REFL_TYPE callable from a nested namespace?

In the documentation you correctly pointed out that the macros need to be called from within the global namespace.
This becomes a bit unwieldy, a potential source of errors, and less readable in larger and/or structured projects where the structs are declared inside nested namespaces, e.g.:

namespace other_a {
namespace other_b {

struct Point {
  float x;
  float y;
  float z;
};
// REFL_AUTO(type(Point), field(x), field(y), field(z)) // <- would like to define it here

// [..] many unrelated lines of code [..]

} // namespace other_a
} // namespace other_b

REFL_AUTO(type(Point), field(x), field(y), field(z)) // <- have to define it here (global namespace)

Ideally one would like to declare the visitor macros right after the structs have been declared to minimise synchronisation errors.
Any idea how this could be extended (other than closing/reopening the other namespaces)? Any help would be much appreciated!

Season greetings and wish you a successful New Year 2022! 🎄 🎁 🎉

@veselink1
Copy link
Owner

veselink1 commented Dec 24, 2021

REFL_TYPE generates a specialisation of a hidden type called refl_impl::metadata::type_info__. Specialisations cannot be declared from within another non-enclosing namespace in standard C++. There are compiler extensions that allow that. IIRC, I could only get it to work on GCC with -fpermissive, I believe, but changes at the refl-cpp level would be necessary as well.

Happy holidays to you as well!

@RalphSteinhagen
Copy link
Contributor Author

@veselink1 any chance of addressing this issue?

We would like to use your little library in a wider range of projects (beyond our OpenCMW library), notably the new GNU Radio 4.0. Having to define the reflection macros always in the root namespace and not in the nested namespace where the aggregate type is defined is sort of a usability issue and error-prone/hard to explain to novice or occasional C++ developers.

At its core, the issue as you mentioned is:

namespace library { // library namespace -- cannot touch
  template <typename T>
  struct my_struct {};
}

// user code -- needs nested namespace
namespace user { 
  template<>
  struct library::my_struct<int> { // error: class template specialization of 'my_struct' not in a namespace enclosing 'library'
    int data;
  };
}

template<>
struct library::my_struct<double> { // valid
    double data;
};

At the same time, Niels Lohmann's Json library uses two similar types of macros NLOHMANN_DEFINE_TYPE_INTRUSIVE (declared within the class) and NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE (declared outside the class) where the namespacing doesn't seem to be an issue.

Is the difference because refl-cpp defines a templated struct that is being specialised for each user type and the other lib a templated function? 🤔

Could we iterate on this and -- if you like -- maybe find a solution together?

@RalphSteinhagen
Copy link
Contributor Author

RalphSteinhagen commented Feb 18, 2023

@veselink1 I may have found one possible solution to the problem above. Plasese see the compiler-explorer example for details.

The problem with template specialisation is that it needs to be done in the same and/or root namespace. However, this isn't necessary for function specialisation as commonly used, for example, by overloading the operator<< operator. Using this as an inspiration, one could create the refl_impl::metadata::type_info__ from within that function like (const T& is just needed for the template ADL):

namespace library {
template <typename T, typename... Members>
struct type_info {
    using value_type = T;
    constexpr static std::size_t N = sizeof...(Members);
    constexpr static std::string_view nameSpace = CURRENT_NAMESPACE;
    std::string_view className;    
    std::array<std::string_view, N> memberName;
    std::tuple<Members T::*...> memberReference;
};

template <typename T>
consteval static library::type_info<T> typeInfo(const T&) {
    return library::type_info<T>{
        .className = {"<unknown type>"}};
}
} // namespace library

and helper macros as (N.B. here w/o the macro expansions (wanted to keep is KISS)):

#define ENABLE_REFLECTION(...) ENABLE_REFLECTION_N(__VA_ARGS__, ENABLE_REFLECTION3, ENABLE_REFLECTION2, ENABLE_REFLECTION1)(__VA_ARGS__)

#define ENABLE_REFLECTION_N(_1, _2, _3, _4, NAME, ...) NAME

#define ENABLE_REFLECTION1(TypeName, Member1)  \
consteval static library::type_info<TypeName, decltype(std::declval<TypeName>().Member1)> typeInfo(const TypeName&) {    \
  return library::type_info<TypeName, decltype(std::declval<TypeName>().Member1)>{ \
    .className  = {#TypeName}, \
    .memberName = { #Member1 }, \
    .memberReference = std::make_tuple(&TypeName::Member1) \
    };  \
}
// [...]

This would allow the following:

namespace ns1 {
struct MyClass {
    int fieldA = 42;
    float fieldB = 3.14f;
};
ENABLE_REFLECTION(MyClass, fieldA, fieldB);

}  // namespace ns1

namespace ns2 {
struct MyClass {
    int value = 99;
};
ENABLE_REFLECTION(MyClass, value);

}  // namespace ns2

// global namespace
struct MyClass {
    int age = 99;
    std::string name = "pops";
};
ENABLE_REFLECTION(MyClass, age, name);

struct UnknownClass {
    int value = 123;
    float value2 = 1.23f;
};

template<typename T>
constexpr void print_type_info() {
    using namespace library;
    T data{};
    constexpr auto type_info = typeInfo(data);
    fmt::print("{}::{}\n", type_info.nameSpace, type_info.className);
    std::apply([&type_info, &data](auto&&... args) {
        std::size_t i = 0;
        ( (fmt::print("  {} = {}\n", type_info.memberName[i++], data.*args)), ...);
    }, type_info.memberReference);
    fmt::print("\n");
}

int main() {

    print_type_info<MyClass>();
    print_type_info<UnknownClass>();
    print_type_info<ns1::MyClass>();
    print_type_info<ns2::MyClass>();

    return 0;
}

printout:

::MyClass
  age = 99
  name = pops

library::<unknown type>

ns1::MyClass
  fieldA = 42
  fieldB = 3.14

ns2::MyClass
  value = 99

What do you think?

N.B. I played also a bit with structured bindings but this, unfortunately, fails once classes have parents. 🤔

For some weird reason (<->a hole in the C++ std) one can use the brace-initialiser with classes inheritance but not the other way around with the structured bindings.

@veselink1
Copy link
Owner

Hi @RalphSteinhagen,

Thanks for sharing your findings. The approach you've linked to is one that I am aware of and have considered both when writing the original implementation and when you've previously asked about this.

Where it breaks down is the inability to have a template method inside of a local class.

See this example: https://godbolt.org/z/9YWjGc41P

This is something that is used pervasively in the generated type and member descriptors.

@RalphSteinhagen
Copy link
Contributor Author

Does one really need templated functions inside the struct?

The access could be also a free-standing functions as illustrated in the second code-snippet I posted (albeit in tiny script). The free standing function could then access the tuple as long as it's publically defined:

template <std::size_t idx, typename Type>
auto& member(Type&& type) {
    return std::get<idx>(to_tuple(std::forward<Type&>(type)));
}

The to_tuple(...) is just syntactic sugar using structured bindings in case the members are not explicitly declared as done in the first sample as

std::tuple<Members T::*...> type_info<T>::memberReference

That could work, couldn't it?

@RalphSteinhagen
Copy link
Contributor Author

@veselink1 could we iterate on the above proposal? Maybe a direct chat/video/group call would be useful?

@veselink1
Copy link
Owner

Hi @RalphSteinhagen, I'm considering ways of implementing this, some along the lines of what you suggested, but I do not have a clear timeline just yet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants