1
\$\begingroup\$

In a recent project I started passing dependencies to classes as constructor arguments, instead of using local instances. This makes unit testing straightforward (with mock objects), but manual instance creation can be quite tedious if you have more than a handful of objects.

I created a small class that's capable of automatically discovering dependencies compile time and instantiate them on demand. Since this involves a lot of template magic (by my standards) and it's the first time I'm doing TMP, it would be nice to have it reviewed by experts.

My goals were to minimize boilerplate code and avoid making too much dependency on the lib. Currently the only requirement is to have a static factory function in each class.

class Context
{
    // A single item in the context
    struct CtxItem
    {
        void* instancePtr = nullptr;                                    // object instance pointer
        bool marker = false;                                            // flag used to detect circular dependencies
        std::function<void(void)> factory;                              // factory fn. to create a new object instance
        void (*deleter)(void*) = nullptr;                               // delete fn. (calls proper destructor)
        std::type_index derivedType = std::type_index(typeid(void));    // a derived type (eg. implementation of an interface)

        // non-copyable, non-moveable
        CtxItem() = default;
        CtxItem(const CtxItem& rhs) = delete;
        CtxItem& operator=(const CtxItem& rhs) = delete;
        CtxItem(CtxItem&& rhs) = delete;
        CtxItem& operator=(CtxItem&& rhs) = delete;
    };

    // Factory signature
    template <class InstanceType, class... Args>
    using FactoryFunction = InstanceType*(*)(Args&...);

    // The object storage
    std::map<std::type_index, CtxItem> items;
    std::vector<CtxItem*> constructionOrder;

    // Recursively iterate over all bases
    template <typename T, typename std::enable_if< !T::empty::value >::type* = nullptr >
    void declareBaseTypes(std::type_index& derivedType)
    {
        items[ std::type_index(typeid( typename T::first::type )) ].derivedType = derivedType;
        declareBaseTypes<typename T::rest::type>( derivedType );
    }

    template <typename T, typename std::enable_if< T::empty::value >::type* = nullptr >
    void declareBaseTypes(std::type_index&) { }

    // Add factory method automatically if present in class
    template <typename T, typename std::enable_if< std::is_function<decltype(T::factory)>::value >::type* = nullptr>
    void addClassAuto(void*) // argument only used to disambiguate from vararg version
    {
        addFactoryPriv(T::factory);
    }

    template<typename T>
    void addClassAuto(...)
    {
        throw std::runtime_error(std::string("Class '") + typeid(T).name() + "' has no factory in context!");
    }

    // Add a factory function to context
    template <class InstanceType, class... Args>
    void addFactoryPriv(FactoryFunction<InstanceType, Args...> factoryFunction)
    {
        auto instanceTypeIdx = std::type_index(typeid(InstanceType));

        declareBaseTypes< typename std::tr2::bases<InstanceType>::type >( instanceTypeIdx );

        CtxItem& item = items[ instanceTypeIdx ];

        if (item.factory)
            throw std::runtime_error(std::string("Factory already registed for type: ") + typeid(InstanceType).name());

        item.factory = [factoryFunction, this]()
        {
            addInstance(factoryFunction( get<Args>()... ), true);
        };
    }

    template <typename T>
    void addFactoryPriv(T)
    {
        // Use a dummy is_void type trait to force GCC to display instantiation type in error message
        static_assert( std::is_void<T>::value, "Factory has incorrect signature, should take (const) references and return a pointer! Examlpe: Foo* Foo::factory(Bar& bar); ");
    }

    // Gets a ContextItem, tries adding a class factory if type not found in map
    template <class T>
    CtxItem& getItem()
    {
        auto it = items.find( std::type_index(typeid(T)) );

        if (it == items.end())
        {
            addClassAuto<T>(nullptr);
            it = items.find( std::type_index(typeid(T)) );
        }
        else
        {
            CtxItem& item = it->second;

            // fallback to derived type (no instance or factory, but a derived type is registered)
            if ( !item.instancePtr && !item.factory && (item.derivedType != std::type_index(typeid(void))) )
                it = items.find(item.derivedType);
        }

        return it->second;
    }

    // Add an already instantiated object to the context
    template <typename T>
    void addInstance(T* instance, bool takeOwnership = false)
    {
        if (instance == nullptr)
            throw std::runtime_error(std::string("Trying to add nullptr instance for type: ") + typeid(T).name());

        CtxItem& item = items[ std::type_index(typeid(T)) ];

        if (item.instancePtr != nullptr)
            throw std::runtime_error(std::string("Instance already in Context for type: ") + typeid(T).name());

        item.instancePtr = static_cast<void*>(instance);

        if (takeOwnership)
        {
            item.deleter = [](void* ptr) { delete( static_cast<T*>(ptr) ); };
            constructionOrder.push_back(&item);
        }
    }

public:
    Context()
    {
        addInstance(this);
    }

    ~Context()
    {
        for (auto it = constructionOrder.rbegin(); it != constructionOrder.rend(); it++)
            (**it).deleter((**it).instancePtr);
    }

    // Get an instance from the context, runs factories recursively to satisfy all dependencies
    template <class T>
    T& get()
    {
        CtxItem& item = getItem<T>(); // may return derived type

        if (item.instancePtr == nullptr)
        {
            if (item.marker)
                throw std::runtime_error(std::string("Cyclic dependecy while instantiating type: ") + typeid(T).name());

            item.marker = true;
            item.factory();
            item.marker = false;
        }

        return *(static_cast<T*>(item.instancePtr));
    }
};

There's a live example on Coliru and the full source with some info on GitHub.

Some additional clarification:

In the Coliru example there are 3 classes with transitive dependencies (A->B->C). The primary purpose of the factory functions is to provide an unambiguous dependency list for each class that can be extracted with variadic template matching. They also also create class instances, but it's debatable if that's necessary at all.

\$\endgroup\$
1
\$\begingroup\$

Could you add some more explanation of what kind of pattern/construct this is intended to replace? I played around with your Coliru code a little bit, and it seems like this is the thing you're replacing:

C *c = C::factory();
B *b = B::factory(*c);
A *a = A::factory(*b);
a->run();
delete c;
delete b;
delete a;

That seems horrible — and not just in the sense of "too much typing, let's write a wrapper", I mean it seems horribly prone to memory leaks, dangling references, and so on, in real life, not just in unit tests.

So either I'm misunderstanding how you're intending to use this (because I don't understand how your existing code looks), or else IMHO you should be spending your time on cleaning up the semantics of your existing code before worrying about the syntax.

In modern C++, I'd expect something more like this:

C c; B b(c); A a(b); a.run();

or equivalently

A(B(C())).run();

That's assuming that you can use value semantics, i.e. you don't need polymorphism or are willing to hide the polymorphism behind type-erasure (a la std::function).

If you really want to use classic polymorphism / pointer semantics / the heap, I'd expect something more like

auto c = std::make_shared<C>();
auto b = std::make_shared<B>(c);  // or std::move(c), if b is taking ownership of it
auto a = std::make_shared<A>(b);
a->run();

or, presuming auto C::factory() { return make_shared<C>(); }, it might look like

auto a = A::factory(B::factory(C::factory()));
a->run();

Nit on your posted code: At one point you throw std::runtime_error("some message"). Wouldn't it make more sense to static_assert(false, "some message") instead, if that code is never supposed to be instantiated at all? Or is it really okay to instantiate it as long as it's never called?

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Thanks for the answer and sorry for the horror. :) I added some clarifications, my intention was to create something that auto discovers and walks the dependency tree. I agree that lifecycle management is not clean: objects are managed by the context, but created by external entities, I'll work on that. Also I'll consider using shared_ptr, as it will automatically result in correct destruction order. I do use polymorphism, because mock objects are derived from production ones, or from a common interface. I used static_assert where the problem was clearly compile time. \$\endgroup\$ – Gyorgy Szekely Mar 23 '16 at 12:28
  • \$\begingroup\$ I'm pretty sure you should still static_assert instead of that throw (on line 65 of your Coliru snippet). However, I now see that you have to do something like static_assert(sizeof(T) == 0) to make the condition "dependent" on T; if the condition is not dependent, then the compiler eagerly evaluates the static_assert even before the template needs instantiation. I'm not sure right now if that's expected behavior, or if a conformant and/or C++1z compiler might behave less eagerly there. \$\endgroup\$ – Quuxplusone Mar 24 '16 at 4:02
  • \$\begingroup\$ Your edited question gives an example of your expected use for e.g. A a{b}; — but you're not using the factory methods? Why introduce factory, then? Couldn't you SFINAE on the existence of a constructor just as well as on the existence of a method named factory? \$\endgroup\$ – Quuxplusone Mar 24 '16 at 4:04
  • \$\begingroup\$ You're right about the static_assert in the Coliru version, I removed some code to make it shorter. Depending on a class without a factory method is not an error, since 3rd party classes won't have one. In the GitHub version there's and addFactory() method to register self standing factory functions, which are not auto discoverable. So the exception says: you're depending on class without a factory and forgot to register one manually. AFAICT this can't be determined compile time, the GitHub version has unit tests for this behavior. \$\endgroup\$ – Gyorgy Szekely Mar 24 '16 at 8:04
  • \$\begingroup\$ The function named factory is used to extract the list of dependencies with the variadic template method addFactoryPriv (line 71 on Coliru). Can I do this with a constructor? Can I pass a pointer-to-a-constructor to a method? If a class has multiple constructors then which one is taken? \$\endgroup\$ – Gyorgy Szekely Mar 24 '16 at 10:00

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.