4
\$\begingroup\$

I am working on cleaning up and making open source a C++ reflection library that has served me well over the last few years.

One of the most important classes of the library is Object, which is used to contain and work with1 any type derived from the object's template parameter T (or any type at all if T is void).

I am interested in getting some feedback on the constructor interface of the Object class (any feedback on other parts of the code is of course welcome as well), specifically:

  • Is the purpose of each constructor understandable? Would it be understandable without the accompanying comment? What would make it easier to understand?
  • Are there too many constructors? Would a single constructor that handles all cases (using tag dispatching) be preferable?
  • Compiler errors are decent but not great when trying to construct an Object with incompatible arguments. Any suggestions to improve them?
  • The constructor taking a std::reference_wrapper<Object<...>> and similar uses a templated r-value reference parameter to handle all variants of the reference_wrapper argument (e.g., std::reference_wrapper<const Object<...>>). This feels ugly to me, but the only alternative I can think of is to duplicate the constructor for each variant.
  • Is the EnableIf logic understandable?

object.h

#include "traits.h"

namespace Reflect {

// A class derived from Object that ensures the underlying object contains a
// value rather than a reference.
template <typename T = void>
class Value;

// A class derived from Object that ensures the underlying object contains a
// reference rather than a value.
template <typename T = void>
class Reference;

// A class that holds and grants access to a value of type T or a type derived
// therefrom.
// The value may be owned by the object, or merely be referenced by the object.
template <typename T = void>
class Object {
public:
    static_assert(std::is_same<T, typename std::decay<T>::type>::value,
                  "Object must be of unqualified type.");

    using element_type = T;

public:
    // Construct object containing a copy of other.
    // The reflected type of the object will be T_Derived.
    template <
        typename T_Derived,
        Detail::EnableIf<
            Detail::IsDerived<T_Derived, T>::value &&
            std::is_constructible<T_Derived, T_Derived>::value &&
            !Detail::IsSameTemplate<T_Derived, Object<T>>::value &&
            !Detail::IsSameTemplate<T_Derived, Value<T>>::value &&
            !Detail::IsSameTemplate<T_Derived, Reference<T>>::value &&
            !Detail::IsSameTemplate<T_Derived, std::reference_wrapper<T>>::value
        >...
    >
    Object(T_Derived &&other);

    // Construct object containing a copy of the other object's value.
    // The reflected type of the object will be equivalent to that of other.
    Object(Object const &other);

    // Construct object containing the other object's moved value.
    // The reflected type of the object will be equivalent to that of other.
    Object(Object &&other);

    // Construct object containing a copy of the other object's value.
    // The reflected type of the object will be equivalent to that of other.
    // Throws an exception if the other object's value is not derived from T.
    template <
        typename T_Related,
        Detail::EnableIf<
            Detail::IsRelated<T_Related, T>::value
        >...
    >
    Object(Object<T_Related> const &other);

    // Construct object containing the other object's moved value.
    // The reflected type of the object will be equivalent to that of other.
    // Throws an exception if the other object's value is not derived from T.
    template <
        typename T_Related,
        Detail::EnableIf<
            Detail::IsRelated<T_Related, T>::value
        >...
    >
    Object(Object<T_Related> &&other);

    // Construct object referencing the value of other.
    // The reflected type of the object will be T_Derived.
    template <
        typename T_Derived,
        Detail::EnableIf<
            Detail::IsDerived<T_Derived, T>::value
        >...
    >
    Object(std::reference_wrapper<T_Derived> other);

    // Construct object referencing the other object's value.
    // The reflected type of the object will be equivalent to that of other.
    // Throws an exception if the other object's value is not derived from T.
    template <
        typename T_Ref,
        Detail::EnableIf<
            Detail::IsSameTemplate<T_Ref, std::reference_wrapper<T>>::value &&
            Detail::IsRelated<typename T_Ref::type::element_type, T>::value &&
            (Detail::IsSameTemplate<typename T_Ref::type, Object<T>>::value ||
             Detail::IsSameTemplate<typename T_Ref::type, Value<T>>::value ||
             Detail::IsSameTemplate<typename T_Ref::type, Reference<T>>::value
            )
        >...
    >
    Object(T_Ref &&ref);

    // Construct object containing an instance of T, forwarding the provided
    // arguments to T's constructor.
    // The reflected type of the object will be T.
    template <
        typename ...T_Args,
        Detail::EnableIf<
            std::is_constructible<T, T_Args...>::value &&
            !Detail::IsSameTemplate<T_Args..., Object<T>>::value &&
            !Detail::IsSameTemplate<T_Args..., Value<T>>::value &&
            !Detail::IsSameTemplate<T_Args..., Reference<T>>::value &&
            !Detail::IsSameTemplate<T_Args..., std::reference_wrapper<T>>::value
        >...
    >
    Object(T_Args &&...args);

    // Destroy the object and its contents.
    ~Object();
};

} // namespace Reflect

traits.h

#include <type_traits>

namespace Reflect { namespace Detail {

// Simplified enable_if for better usability.
enum struct EnableIfType { };
template <bool condition>
using EnableIf = typename std::enable_if<condition, EnableIfType>::type;

// Indicate whether the first type is derived from the second type.
// Evaluates to false if more than two types are specified.
// Note that void is considered a base of all types.
template <typename, typename ...>
struct IsDerived : std::false_type { };

template <typename T_Derived, typename T_Base>
struct IsDerived<T_Derived, T_Base>
: std::conditional<
    std::is_void<typename std::decay<T_Base>::type>::value ||
    std::is_base_of<typename std::decay<T_Base>::type,
                    typename std::decay<T_Derived>::type>::value ||
    std::is_same<typename std::decay<T_Base>::type,
                 typename std::decay<T_Derived>::type>::value,
    std::true_type, std::false_type
>::type { };

// Indicate whether the first type is related to the second type in a way that
// could be resolved by the reflection system.
// Evaluates to false if more than two types are specified.
template <typename, typename ...>
struct IsRelated : std::false_type { };

template <typename T_Lhs, typename T_Rhs>
struct IsRelated<T_Lhs, T_Rhs>
: std::conditional<
    IsDerived<T_Lhs, T_Rhs>::value ||
    IsDerived<T_Rhs, T_Lhs>::value,
    std::true_type, std::false_type
>::type { };

// Indicate whether the first type is of the same template as the second type.
// Evaluates to false if more than two types are specified.
template <typename, typename ...>
struct IsSameTemplateImpl : std::false_type { };

template <template <typename ...> class T, typename ...U, typename ...V>
struct IsSameTemplateImpl<T<U...>, T<V...>> : std::true_type { };

template <typename ...T_Args>
struct IsSameTemplate : IsSameTemplateImpl<typename std::decay<T_Args>::type...>
{ };

} } // namespace Reflect::Detail

The code is compilable (but will fail linking).

main.cpp

#include "object.h"

struct Base { };
struct Derived : Base { };

int main() {
    // Create an object of any type containing reflected type int with value 42.
    Reflect::Object<> object1 = 42;

    // Create an object of Base type containing reflected type Derived.
    Reflect::Object<Base> object2 = Derived();

    // Error, cannot create Object<int> from string.
    //Reflect::Object<int> object("hello");
}

Please note that the code is written for C++11 or later. Some of the template metaprogramming is therefore messier than necessary for newer C++ versions.

  1. To "work with" the object means to access its reflected properties, methods, etc., through the reflection system. For example, given an Object<void> obj containing a std::string, obj.getProperty("length") would return a Reference<void> to the length of the string. obj.call("append", " world") would append " world" to the string, etc. This is what sets Object apart from e.g. std::any or a base class pointer.
\$\endgroup\$
  • \$\begingroup\$ I don't think obj.getProperty("length") with the given semantics is implementable. You are using a runtime property ("length") to cause a compile time effect (instantiating a template member function). The only way to get that to work is to cause all compile time effects at compile time and only selecting the effect at runtime, which means you need a list of all the members of std::string and every other class, and C++ doesn't support that. \$\endgroup\$ – nwp Apr 17 '18 at 11:06
  • \$\begingroup\$ @nwp It works because this is all part of a reflection library, whose primary feature is to enable exactly this :) Classes used in this manner have to be registered with the reflection library during compile time, but can then be introspected arbitrarily (within the bounds of what was registered) at runtime. Thanks for the warning though! \$\endgroup\$ – zennehoy Apr 17 '18 at 11:48
2
\$\begingroup\$

Is the EnableIf logic understandable?

Well, I wonder how it’s different from std::enable_if and related stuff. I assume there is something different about it so that’s a mental burden right there.

Is the purpose of each constructor understandable? Would it be understandable without the accompanying comment? What would make it easier to understand?

I understand what a copy constructor and move constructor are supposed to do, without any commentary. If there is comments, I wonder why they are subtly different from what those functions are supposed to do, or are documenting caveats.

As for the move constructor, I’m puzzled by the ability to move a reference.

I think the ctors would be easier to read, in general, if the huge blocks of contraints were factored out into a single named constraint. And you can use the _t aliases rather than ::type all over the place.

namespace Reflect { namespace Detail {

You can now write simply:

namespace Reflect::Detail {

…which is used to contain and work with any type derived from the object's template parameter T (or any type at all if T is void).

This may be beyond the scope of the review, but I wonder how the T=void case is different from std::any, and the constrained case different from a normal base-class pointer. How is “related in a way that can be resolved by the reflection system” different from dynamic_cast?

\$\endgroup\$
  • \$\begingroup\$ Interesting to see the take that if something is commented it implies caveats. The intent was really to specify the difference between constructors without requiring the user to delve into the EnableIf logic. What move constructor are you puzzled by? The constructor taking T_Ref && is not a move constructor, it just takes any type of std::reference_wrapper<Object<...>> etc. \$\endgroup\$ – zennehoy Apr 17 '18 at 9:34
  • \$\begingroup\$ Specifically, the copy constructor and move constructor (likewise for assignment). Spelling out “Construct object containing a copy of the other object's value. The reflected type of the object will be equivalent to that of other.” is just noise that must be understood and then the reader realizes that this is just the normal definition of what a copy constructor does. \$\endgroup\$ – JDługosz Apr 18 '18 at 5:41

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.