boostorg / describe

A C++14 reflection library
https://boost.org/libs/describe
67 stars 24 forks source link

Add reflection on attributes #13

Open sdebionne opened 3 years ago

sdebionne commented 3 years ago

Following up discussion on cpplang, it would be interesting to have a way to extend member descriptors with custom information (think units, text description). User-defined attributes seems the right tool to express this custom information:

[[units(cm), description("this is sparta")]] int x;

The emulation of the reflection would look something like:

BOOST_DESCRIBE_MEMATTR(x, units(cm), description("this is sparta")) int x;

We would have describe_member_attributes<D> returns L<A1, A2, …​, An>, where D is a member descriptor.

and Ai is an attribute descriptor of the form

struct Ai
{
        static constexpr char const * name = "units";
        static constexpr char const * value = "cm";   /* string attribute only for now */
};

As pointed out by @pdimov, there is a proposal about attribute reflection that introduces typed attributes.

pdimov commented 3 years ago

There seems to be a significant pushback in the committee against the idea of reflection on attributes. The objection is that attributes today can be silently ignored by the compiler, and this feature is deemed extremely important for some reason. The suggestion of those who object is that these annotations need to use a keyword, rather than the attribute syntax.

sdebionne commented 3 years ago

I dont understand why having attributes (today) silently ignored by the compiler make them unsuitable. Why not make them eventually silently ignored but mandatorily reflected? Why another keyword? That's questions for the committee... In the meantime, external annotations and #15 will do the trick.

denzor200 commented 3 years ago

Hello everyone! I recently discovered an interesting way to describe custom attributes in C++17. I propose to consider it.

#ifdef _STR
#  error _STR already defined
#endif
#define _STR(S) BOOST_METAPARSE_STRING(S){}

boost::describe::field<int, (units=cm), (description=_STR("this is sparta"))> x;

This short example shows that it is possible to implement it. At least - for one attribute :-) https://godbolt.org/z/ad1ojzch7

sdebionne commented 3 years ago

I would prefer a solution that does not require to use a wrapper type (eg field), something less intrusive. Here is what I came up with, loosely inspired from the Describe library.

The annotations (members do not need to have the same type/number of annotations):

struct position
{
    int distance;
    float timestamp;
};
BOOST_DESCRIBE_STRUCT(position, (), (distance, timestamp))

BOOST_ANNOTATE_MEMBER(position, distance,
    (unit, "meter"),
    (doc, "The distance travelled from the start line"))

BOOST_ANNOTATE_MEMBER(position, timestamp,
    (unit, "second"),
    (doc, "The EPOCH timstamp since the beginning of the run"))

And the usage:

// For each member descriptors
using Md = boost::describe::describe_members<T, boost::describe::mod_any_access>
boost::mp11::mp_for_each<Md>([&](auto D) {
    // Get annotation for the member descriptor
    using A = boost::describe::annotate_member<T, decltype(D)>;

    std::cout << "\nAnnotations:\n";
    boost::mp11::mp_for_each<A>([&](auto a) {
        std::cout << "." << a.name << " = " << a.value << std::endl;
    });
});

https://godbolt.org/z/7PbobnMxW

@pdimov Do you think something like this is worth adding to the library?

denzor200 commented 2 years ago

@sdebionne Is there really a need for a separate BOOST_ANNOTATE_MEMBER? Wouldn't it be better to package it all in one BOOST_DESCRIBE_STRUCT? Like this:


struct position
{
    int distance;
    float timestamp;
};
BOOST_DESCRIBE_STRUCT(position, (), (
    (distance,
        (unit, "meter"),
        (doc, "The distance travelled from the start line")
    ),
    (timestamp,
        (unit, "second"),
        (doc, "The EPOCH timstamp since the beginning of the run")
    )
))
sdebionne commented 2 years ago

The rationale was mostly to keep annotation as an extension of Boost.Describe. But I like your suggestion too.

I have a project that uses Annotation to generate a Json Schema from a described and annotated data structure. Here is the latest iteration of the annotation bits:

annotations.hpp.txt

If there is interest in this, I can move forward and make a PR with tests and doc...

denzor200 commented 2 years ago

If i understand you right, your solution can be used like this:

BOOST_ANNOTATE_MEMBER(position, distance,
    (unit, "meter"))
BOOST_ANNOTATE_MEMBER(position, distance,
    (doc, "The distance travelled from the start line"))

// member position::distance has two attributes - unit and doc

This is perfect together with my suggestion and don't contradict it.

denzor200 commented 2 years ago

If there is interest in this, I can move forward and make a PR with tests and doc...

It would be interesting for me, but i not a reviewer here:)

sdebionne commented 2 years ago

Side note, the member annotations can also be functions:

struct acquisition
{
    int nb_frames = 1;  
   ...
}

BOOST_ANNOTATE_MEMBER(acquisition, nb_frames,
    (desc, "number of frames"),
    (doc, "The number of frames to acquire (0 = continuous acquisition)"),
    (validate, [](int nb_frames) { return nb_frames >= 0;}))

Optional annotation can be handled pretty easily. Here we have a validate function annotation and a generic validate free function would use the annotation if available:

using Validate = BOOST_DESCRIBE_MAKE_NAME(validate);

template <typename L, typename T,
          std::enable_if_t<boost::describe::has_annotation_by_name<L, Validate>::value, bool> = true>
bool validate(T const& t)
{
    // With validation
    auto validate = boost::describe::annotation_by_name_v<L, Validate>;
    return validate(t);
}

template <typename L, typename T,
          std::enable_if_t<!boost::describe::has_annotation_by_name<L, Validate>::value, bool> = false>
bool validate(T const& t)
{
    // Without validation
    return true;
}