14

I'm trying to implement a "property" system to convert C++ instances into JSON and vice versa. I took a part of the code from Guillaume Racicot's answer in this question (C++ JSON Serialization) and simplified it.

Here is how I proceed. I have a Property class:

template <typename Class, typename T>
struct Property {
    constexpr Property(T Class::* member, const char* name) : m_member(member), m_name(name) {}

    T Class::* m_member;
    const char* m_name;
};

m_member points to a specific member of Class

Let's say I want to define properties for a User class, I would like to be able to proceed like this, to be able to assign members a property name:

class User
{
public:
    int age;

    constexpr static auto properties = std::make_tuple(
        Property<User, int>(&User::age, "age")
    );
}

This code compiles and works correctly in GCC(http://coliru.stacked-crooked.com/a/276ac099068579fd) but not in Visual Studio 2015 Update 3. I get those errors:

main.cpp(19) : error C2327 : 'User::age' : is not a type name, static, or enumerator
main.cpp(19) : error C2065 : 'age' : undeclared identifier
main.cpp(20) : error C2672 : 'std::make_tuple' : no matching overloaded function found
main.cpp(20) : error C2119 : 'properties' : the type for 'auto' cannot be deduced from an empty initializer

Would there be a workaround to make it work in Visual Studio 2015 Update 3?

Community
  • 1
  • 1
Michael
  • 1,557
  • 2
  • 18
  • 38
  • Does the `Property` constructor take one argument or two? – aschepler Aug 22 '16 at 19:02
  • @aschepler I think he made a mistake in typing, because in the [shared-code](http://coliru.stacked-crooked.com/a/276ac099068579fd) the constructor has two arguments. – BiagioF Aug 22 '16 at 19:25
  • @aschepler Two arguments, corrected – Michael Aug 22 '16 at 19:32
  • Please post a full but short `main.cpp` file: http://sscce.org/ – pts Aug 22 '16 at 19:34
  • 1
    @pts Yes, I put the code here: http://coliru.stacked-crooked.com/a/276ac099068579fd – Michael Aug 22 '16 at 19:36
  • 1
    The exact same code does not compile in VS2015(update 3) – Michael Aug 22 '16 at 19:38
  • 2
    I don't think MSVC can see inside a class before it's fully defined (`User::age` is not known until `};` is parsed). – rustyx Aug 22 '16 at 19:40
  • @RustyX And it seems that "properties" absolutely needs to be defined inside the User class :( – Michael Aug 22 '16 at 19:48
  • Yep. Very curious if anyone can preserve its `constexpr`ness in MSVC. – rustyx Aug 22 '16 at 19:52
  • @Michael Why do you think it absolutely needs to be defined inside the User class? It just needs be accessible from the User class name. – Yakk - Adam Nevraumont Aug 22 '16 at 20:08
  • 1
    As to whether MSVC or GCC is correct, read the 2 answers [here](http://stackoverflow.com/questions/19564009/error-c2327-not-a-type-name-static-or-enumerator) – rustyx Aug 22 '16 at 21:02
  • This is not an answer so I'm not posting it as one, but there are much easier ways to do what you are trying to do. The member to function pointer does not actually buy you anything. You can do all of this with simple pointers, and substantially simplify your syntax without losing a single thing. The basic building block of reflection is just a function that given an instance of a class, returns a tuple of pairs of typed pointers and string literal names. That's all. Pointer to member just adds lots of needless syntactic complexity. – Nir Friedman Aug 22 '16 at 22:00
  • @NirFriedman True in this case, but if you want to do anything more complicated than JSON serialization, then having a _constexpr_ tuple of members can buy you simplicity and performance. – Oktalist Aug 22 '16 at 22:31
  • @Oktalist You can write a constexpr function that returns the tuple I described as well. Of course, it will take an instance of the object implicitly or explicitly, and so to use it in a constexpr context you need a constexpr instance of the object. But you cannot use the current properties for anything useful without a copy of the object either. I'm open to examples to the contrary, but I think this is just an unnecessary complication. – Nir Friedman Aug 23 '16 at 14:10
  • @NirFriedman Suppose reflection is used for several different operations, e.g. JSON, protobufs, lexicographical comparison, hashing. Suppose that certain members are to be excluded from certain operations. You can mark these members by putting flags in the tuple. In the operation's implementation you can skip the flagged members with `if constexpr`. You could do it with metaprogramming too, and as you're already using metaprogramming to unpack the tuple, the only thing you would lose is the separation of concerns, coupling together skipping with unpacking. So it depends. – Oktalist Aug 23 '16 at 15:29
  • With C++17 constexpr lambdas `&User::age` could be replaced with `[](auto&& x)->decltype(auto) { return x.age; }`. – Oktalist Aug 23 '16 at 15:45
  • @Michael I reported the bug here: https://connect.microsoft.com/VisualStudio/feedback/details/3106015/unable-to-form-a-pointer-to-member-in-some-constant-expressions – Guillaume Racicot Oct 07 '16 at 17:33
  • In the meantime, @Oktalist solution will work just fine – Guillaume Racicot Oct 07 '16 at 17:34

3 Answers3

7

My preferred workaround is just to replace the properties member data with a properties member function:

class User
{
public:
    int age;

    constexpr static auto properties() { return std::make_tuple(
        Property<User, int>(&User::age, "age")
    ); }
};

This works because in the definition of a member function, the class is considered to be completely defined. It also has the desirable attribute that properties doesn't need to be separately defined if odr-used.

Oktalist
  • 14,336
  • 3
  • 43
  • 63
3

MSVC doesn't know enough about User when it wants to calculate the type of properties to know it has a member age.

We can work around the problem.

template<class T>struct tag_t{constexpr tag_t(){};};
template<class T>constexpr tag_t<T> tag{};

template<class T>
using properties = decltype( get_properties( tag<T> ) );

class User
{
public:
  int age;

};
constexpr auto get_properties(tag_t<User>) {
  return std::make_tuple(
    Property<User, int>(&User::age, "age")
  );
}

In the JSON reflection code, simply replace std::decay_t<T>::properties with get_properties( tag<std::decay_t<T>> ).

This has a few advantages. First you can retrofit some classes you do not own or wish to modify with properties seamlessly. With careful namespace use and ADL enabling the point of call, you can even do so for (some) types within std (with oublic members only; pair at the least).

Second it avoids possible ODR-use requirements on properties. Properties is now a constexpr return value not some global data that may require storage.

Third it permits properties to be written out-of-line with the class definition like above, or inline as a friend within the class, for maximium flexibility.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
1

If it is absolutely needed to properties be defined in User class maybe you could make use of helper templated constexpr function like:

#include <tuple>

template <typename Class, typename T>
struct Property {
    constexpr Property(T Class::* const member) : m_member{ member } {}

    T Class::* const m_member;
};

template <class T, class V>
constexpr Property<T, V> get_age_property() {
    return  Property<T, V>(&T::age);
}

class User
{
public:
    int age;

    constexpr static std::tuple<Property<User, int>> properties = std::make_tuple(
         get_age_property<User, int>()
    );

};

int main()
{
}

It seems to compile in webcompiler i.e. VC++ 19.00.23720.0

W.F.
  • 13,888
  • 2
  • 34
  • 81