Overloading variadic templated methods

Viewed 82

My compiler is having a hard time to understand this code, I am struggling for hours to find what's wrong.

#include <utility>
#include <string>

template<typename Derived>
struct AssetLoader {
    template<typename... Args>
    void doLoad(Args&& ... args) const {
        static_cast<const Derived *>(this)->load(std::forward<Args>(args)...);
    }
};

struct TextureLoader : public AssetLoader<TextureLoader> {
    void load(const std::string &path) const {
        // some code
    }
};

struct SomeOtherLoader : public AssetLoader<SomeOtherLoader> {
    void load(const std::string &path) const {
        // some code
    }
};

template<typename DefaultLoader>
class Resources {
    AssetLoader<DefaultLoader> m_defaultLoader;

public:
    Resources(AssetLoader<DefaultLoader> defaultLoader):
        m_defaultLoader(std::move(defaultLoader)) {}

    template<typename... Args>
    void load(Args&& ... args) {
        load(m_defaultLoader, std::forward<Args>(args)...);
    }

    template<typename Loader, typename... Args>
    void load(const AssetLoader<Loader>& loader, Args&& ... args) {
        loader.doLoad(std::forward<Args>(args)...);
    }
};

int main() {
    Resources<TextureLoader> resources(TextureLoader{});
    resources.load("image.png");
    resources.load(SomeOtherLoader{}, "example.jpg");
    return 0;
}

I got this error:

fatal error: template instantiation depth exceeds maximum of 900 (use -ftemplate-depth= to increase the maximum)
         return load(m_defaultLoader, std::forward<Args>(args)...);
                                      ~~~~~~~~~~~~~~~~~~^~~~~~

My real code is a lot more complex but I trimed it down to this, but I got the same error.

If I comment the first overload, it works well but I can't call the load() method without passing a Loader. I want the overload for the default loader, so I can do resources.load("image.png");

I use mingw64 8.1

Any idea?

2 Answers

The compiler is telling you exactly where the problem is -- infinite recursion here:

template<typename... Args>
void load(Args&& ... args) {
    load(m_defaultLoader, std::forward<Args>(args)...);
}

This function is calling itself infinitely. The other overload is never selected, because Args&& synthesizes a better match than AssetLoader<Loader> const& (specifically, TextureLoader const&).

Give the other overload a different name to disambiguate...

template<typename... Args>
void load(Args&& ... args) {
    load2(m_defaultLoader, std::forward<Args>(args)...);
}

template<typename Loader, typename... Args>
void load2(const AssetLoader<Loader>& loader, Args&& ... args) {
    loader.doLoad(std::forward<Args>(args)...);
}

After digging, I found finally a solution. Basically I had to check that the first type of Args... is not a loader itself, so the compiler will be forced to choose the 2nd overload instead.

Here is the code

#include <utility>
#include <string>

template<typename T1, typename...>
struct first {
    typedef T1 type;
};

template<typename... T>
using first_t = typename first<T...>::type;

template<typename Derived>
struct AssetLoader {
    template<typename... Args>
    void doLoad(Args&& ... args) const {
        static_cast<const Derived *>(this)->load(std::forward<Args>(args)...);
    }
};

struct TextureLoader : public AssetLoader<TextureLoader> {
    void load(const std::string& path) const {
        // some code
    }
};

struct SomeOtherLoader : public AssetLoader<SomeOtherLoader> {
    void load(const std::string& path) const {
        // some code
    }
};

template<typename DefaultLoader,
    std::enable_if_t<
        std::is_base_of_v<
            AssetLoader<DefaultLoader>,
            DefaultLoader
        >,
        int
    > = 0
>
class Resources {
    DefaultLoader m_defaultLoader;

public:
    Resources(DefaultLoader defaultLoader):
        m_defaultLoader(std::move(defaultLoader)) {}

    template<
        typename... Args,
        std::enable_if_t<
            sizeof...(Args) == 0 ||
            !std::is_base_of_v<
                AssetLoader<std::decay_t<first_t<Args...>>>,
                std::decay_t<first_t<Args...>>
            >,
            int
        > = 0
    >
    void load(Args&& ... args) {
        load(m_defaultLoader, std::forward<Args>(args)...);
    }

    template<typename Loader, typename... Args>
    void load(const AssetLoader<Loader>& loader, Args&& ... args) {
        loader.doLoad(std::forward<Args>(args)...);
    }
};

int main() {
    Resources<TextureLoader> resources(TextureLoader{});
    resources.load("image.png");
    resources.load(SomeOtherLoader{}, "example.jpg");
    return 0;
}
Related