TheNitesWhoSay / RareCpp

Creating a simpler, more intuitive means of C++ reflection
MIT License
124 stars 6 forks source link

getting and setting values by string #96

Closed SanPen closed 1 year ago

SanPen commented 2 years ago

Hi!

I was looking for some piece of code to have in C++ something similar to setattr / getattr from python, and you library seems to be it.

I've read the readme a couple of times and maybe you could clarify the following questions I have:

a) Can I get the value of an attribute by its name without iterating the fields?

class FuelTank {
public:
    float capacity;
    float currentLevel;
    float tickMarks[2];

    REFLECT(FuelTank, capacity, currentLevel, tickMarks)
};

FuelTank fuel_tank;

// this is what I'd like to do
float capacity = fuel_tank.someKindOfGetter("capacity");

fuel_tank.someKindOfSetter("capacity", 10.4);

b) The NOTE macro seems very nice as well, can I use it to do the following?

class FuelTank {
public:
    NOTE(_capacity, "myCustomCapacity")
    float _capacity;
    float _currentLevel;
    float _tickMarks[2];

    REFLECT(FuelTank, _capacity, _currentLevel, _tickMarks)
};

FuelTank fuel_tank;

// this is what I'd like to do
float capacity = fuel_tank.someKindOfGetter("myCustomCapacity");

fuel_tank.someKindOfSetter("myCustomCapacity", 10.4);

Thanks in advance, Santiago

TheNitesWhoSay commented 2 years ago

Hi Santiago,

Using fields by name and aliases is something I've done as part of Json, but there is not yet support in the reflection part of the library (hopefully I'll have something there soon). In the meantime, I've pulled some code out you could use: https://godbolt.org/z/xnKjd1Gq4

There is a theoretical problem with making convenient getters for reflected fields in C++: ideally you'd be able to go... auto x = get(obj, fieldIndex) (or auto x = get(obj, fieldName) ) with a runtime fieldIndex (or field name), but compilers require return types to be know at compile time, so you need to supply the type explicitly, return a variant (which only pushes the problem of using the value back a step), or use a visitor - which is why this library heavily uses lambda-visitor methods. Some example of what you can do...

float capacity = Fields::get<float>(fuelTank, "capacity"); Fields::visit(fuelTank, "capacity", [&](auto & value) { /* do stuff with value */ });

Setters don't need to return the value, so they can be done easily Fields::set(fuelTank, "currentLevel", 2.1);

I've also put an example of aliasing a field using notes in the same godbolt.

Hope this helps!

SanPen commented 2 years ago

Hi Justin,

The addition of Fields::set and Fields::get looks like what I need.

The Way I added the FieldsCache and the rest of your code from the example to my code was to extend Reflect.h with this:

// this goes after the ObjectMapper namespace
namespace Reflect::Fields {

    template <typename T>
    struct FieldCache {

        struct NameIndex  {
            std::string name;
            int index = 0;
        };

        static inline std::hash<std::string> strHash;
        static inline std::multimap<size_t, NameIndex> fieldHashToIndex = [](){
            std::multimap<size_t, NameIndex> fieldCache{};
            class_t<T>::ForEachField([&](auto & field) {
                using Field = std::remove_reference_t<decltype(field)>;
                if constexpr ( Field::template HasAnnotation<Alias> )
                {
                    const auto & fieldAlias = field.template getAnnotation<Alias>().value;
                    NameIndex nameIndex { std::string(fieldAlias), field.Index };
                    fieldCache.insert(std::make_pair(strHash(std::string(fieldAlias)), nameIndex));
                }
                NameIndex nameIndex { field.name, field.Index };
                fieldCache.insert(std::make_pair(strHash(std::string(field.name)), nameIndex));
            });
            return fieldCache;
        }();

        static inline size_t getFieldIndex(const std::string & fieldName) {
            size_t fieldIndex = std::numeric_limits<size_t>::max();
            size_t fieldNameHash = strHash(fieldName);
            auto fieldHashMatches = fieldHashToIndex.equal_range(fieldNameHash);
            for ( auto it = fieldHashMatches.first; it != fieldHashMatches.second; ++it )
            {
                if ( it->second.name.compare(fieldName) == 0 )
                    fieldIndex = it->second.index;
            }
            return fieldIndex;
        }
    };

    template <typename T>
    struct TypeName {
        static const char* Get() {
            return typeid(T).name();
        }
    };

    template <typename T, typename Obj>
    struct FieldNotFound : std::exception {

        std::string msg;

        FieldNotFound(const std::string & fieldName) {
            // msg = std::string("Field \"") + fieldName + "\" of type \"" + TypeToStr<T>() + "\" not found in \"" + TypeToStr<Obj>() + "\"";  // TODO this is C++20
            msg = std::string("Field \"") + fieldName + "\" of type \"" + TypeName<T>() + "\" not found in \"" + TypeName<Obj>() + "\"";
        }

        virtual const char* what() const noexcept {
            return msg.c_str();
        }
    };

    template <typename T, typename Obj>
    const T & get(const Obj & obj, const std::string & fieldName) {

        size_t fieldIndex = FieldCache<Obj>::getFieldIndex(fieldName);
        const T* valuePtr = nullptr;

        if ( fieldIndex != std::numeric_limits<size_t>::max() ) {
            class_t<Obj>::FieldAt(fieldIndex, [&](auto & field){
                using FieldType = typename std::remove_reference_t<decltype(field)>::Type;
                if constexpr ( std::is_same_v<FieldType, T> )
                    valuePtr = &(obj.*field.p);
            });
        }

        if ( valuePtr != nullptr ) {
            return *valuePtr;
        } else {
            throw FieldNotFound<T, Obj>{fieldName};
        }
    };

    template <typename Obj, typename Function>
    void visit(Obj & obj, const std::string & fieldName, Function function) {
        size_t fieldIndex = FieldCache<Obj>::getFieldIndex(fieldName);
        if ( fieldIndex != std::numeric_limits<size_t>::max() ) {
            class_t<Obj>::FieldAt(obj, fieldIndex, [&](auto & field, auto & value) {
                function(value);
            });
        }
    }

    template <typename T, typename Obj>
    void set(Obj & obj, const std::string & fieldName, const T & value) {
        size_t fieldIndex = FieldCache<Obj>::getFieldIndex(fieldName);
        if ( fieldIndex != std::numeric_limits<size_t>::max() ) {
            class_t<Obj>::FieldAt(obj, fieldIndex, [&](auto & field, auto & fieldValue) {
                ObjectMapper::map(fieldValue, value);
            });
        }
    }

}

When using Fields::get I'm having the following error:

Reflect.h:1027:51: error: ‘struct ns::CalculationNode::Class’ is private within this context where CalculationNode is the class I'm reflecting.

Not sure how to fix this. Also tried to replicate the bug in godbolt and I got an entirely different issue.

Any idea?

TheNitesWhoSay commented 2 years ago

While I can't see your code, I'm guessing in CalculationNode need to ensure the REFLECT macro is at the end of the class in a section marked "public:" (you can mark multiple sections public as needed).

SanPen commented 2 years ago

Bingo, It works.

I've run a couple of tests in ubuntu and macos and all goes well.

Thanks a lot.

Would you like me to send you the version of Reflect.h I have? I did a couple of corrections

TheNitesWhoSay commented 2 years ago

You can place it on a fork, though it's unlikely I'll have much use for it (the code I gave you won't work for all fields since it's based on pointers and reference fields have no proper field pointer, and there's a lot of refactoring that will go into making it work more generally).

TheNitesWhoSay commented 1 year ago

Accessing fields by names is part of release 2.0.0 e.g. https://godbolt.org/z/7zqnW6hnT , getters and setters won't get first-class support per design decisions