c++templatestemplate-meta-programming

How to use enable_if to do template specialisation with indefinite args


What I want to achieve

The motivation is I want to have template with form

template<typename T, typename ...Args>

where T is some class type. And I want a specialised version of this template if T is BaseA or any derived class of BaseA.

(The C++ standard I'm under is C++20)

The problem

If the second template param is definite, this is easy to achieve:

template <typename T, typename U, typename Enable = void>
class Factory
{
    public:
        Factory(U arg)
        {
            m_spInner = std::make_shared<T>(arg);
        }
        void print() const
        {
            printf("Factory normal: ");
            m_spInner->print();
        }
    private:
        std::shared_ptr<T> m_spInner;
};

template <typename T, typename U>
class Factory<T, U, typename std::enable_if<std::is_base_of<BaseA, T>::value>::type>
{
public:
    Factory(U arg)
    {
        m_spInner = std::make_shared<T>(arg);
    }
    void print() const
    {
        printf("Factory BaseA: ");
        m_spInner->print();
    }
private:
    std::shared_ptr<T> m_spInner;
};

// callers
int main()
{
    Factory<DerivedA1, int> factoryA1(1);
    factoryA1.print();

    Factory<DerivedA2, int> factoryA2(2);
    factoryA2.print();

    Factory<DerivedB, char> factoryB('B');
    factoryB.print();

    Factory<C, string> factoryC("C");
    factoryC.print();

    //Factory<int> factoryInt(1); // compile error
    //factoryInt.print();

    return 0;
}

does the job.

But when the second param is indefinite, like

template <typename T, typename ...Args, typename Enable = void>

it does not compile with error message that the indefinite param must be the last param.

If I put the Enable before the ...Args, the template itself can compile but the calling doesn't work, as int doesn't fit the Enable template param.

So I wonder if it is possible to make enable_if work with class template with indefinite param. Or is there any other elegant way to achieve the need to specialise template for a class and all its child class?

Any suggestion is highly welcomed. Thanks a lot!

Appendix

For convenience, just post my testing code below. If INDEFINITE_PARAM defined, it will compile the INDEFINITE_PARAM version, which lead to error as I couldn't figure out how to properly put enable_if. if INDEFINITE_PARAM not defined, it can compile the definite param version and gives expected result.

#include <type_traits>
#include <cstdio>
#include <cstdlib>
#include <memory>
#include <string>

//#define INDEFINITE_PARAM

using namespace std;


class BaseA
{
public:
    BaseA(int id): m_id(id) {}
    virtual void print() const
    {
        printf("BaseA\n");
    }
private:
    int m_id;
};

class BaseB
{
public:
    BaseB(char id): m_id(id) {}
    virtual void print() const
    {
        printf("BaseB\n");
    }
private:
    char m_id;
};

class DerivedA1 : public BaseA
{
public:
    DerivedA1(int id) : BaseA(id) {}
    void print() const override
    {
        printf("DerivedA1\n");
    }
};

class DerivedA2 : public BaseA
{
public:
    DerivedA2(int id) : BaseA(id) {}
    void print() const override
    {
        printf("DerivedA2\n");
    }
};

class DerivedB : public BaseB
{
public:
    DerivedB(char id) : BaseB(id) {}
    void print() const override
    {
        printf("DerivedB\n");
    }
};

class C
{
public:
    C(string id) : m_id(id) {}
    void print() const
    {
        printf("C\n");
    }
private:
    string m_id;
};

#ifdef INDEFINITE_PARAM

template<typename T, typename Base, typename ...Args>
class Factory
{
public:
    Factory(Args&&... args)
    {
        m_spInner = std::make_shared<T>(std::forward<Args>(args)...);
    }
    void print() const
    {
        printf("%s\n", typeid(T::Base).name());
        printf("Factory normal: ");
        m_spInner->print();
    }
private:
    std::shared_ptr<T> m_spInner;
};


template <typename T, typename ...Args>
class Factory<T, BaseA, Args...>
{
public:
    Factory(Args&&... args)
    {
        m_spInner = std::make_shared<T>(std::forward<Args>(args)...);
    }
    void print() const
    {
        printf("%s\n", typeid(T::Base).name());
        printf("Factory BaseA: ");
        m_spInner->print();
    }
private:
    std::shared_ptr<T> m_spInner;
};


template <typename T, typename... Args>
class Factory<T, typename std::enable_if<std::is_base_of<BaseA, T>::value, BaseA>::type, Args...>
{
public:
    Factory(Args&&... args)
    {
        m_spInner = std::make_shared<T>(std::forward<Args>(args)...);
    }
    void print() const
    {
        printf("%s", std::is_base_of<BaseA, T>::value);
        printf("Factory BaseA: ");
        m_spInner->print();
    }
private:
    std::shared_ptr<T> m_spInner;
};
/*
template <typename T, typename... Args>
class Factory<T, void, Args...>
{
public:
    Factory(Args&&... args)
    {
        m_spInner = std::make_shared<T>(std::forward<Args>(args)...);
    }
    void print() const
    {
        printf("Factory Normal: ");
        m_spInner->print();
    }
private:
    std::shared_ptr<T> m_spInner;
};
*/

#else
template <typename T, typename U, typename Enable = void>
class Factory
{
    public:
        Factory(U arg)
        {
            m_spInner = std::make_shared<T>(arg);
        }
        void print() const
        {
            printf("Factory normal: ");
            m_spInner->print();
        }
    private:
        std::shared_ptr<T> m_spInner;
};

template <typename T, typename U>
class Factory<T, U, typename std::enable_if<std::is_base_of<BaseA, T>::value>::type>
{
public:
    Factory(U arg)
    {
        m_spInner = std::make_shared<T>(arg);
    }
    void print() const
    {
        printf("Factory BaseA: ");
        m_spInner->print();
    }
private:
    std::shared_ptr<T> m_spInner;
};
#endif

int main()
{
    Factory<DerivedA1, int> factoryA1(1);
    factoryA1.print();

    Factory<DerivedA2, int> factoryA2(2);
    factoryA2.print();

    Factory<DerivedB, char> factoryB('B');
    factoryB.print();

    Factory<C, string> factoryC("C");
    factoryC.print();

    //Factory<int> factoryInt(1); // compile error
    //factoryInt.print();

    return 0;
}

Solution

  • Since C++20, you no longer need Enabler template parameter and std::enable_if/std::void_t (which indeed is not usable with variadic template for class). You can use constraints and subsumption:

    template<typename T, typename ...Args>
    class Factory
    {
        //...
    };
    
    template<typename T, typename ...Args>
    requires(std::is_base_of<BaseA, T>::value)
    class Factory
    {
        //...
    };
    

    Demo

    Note: In your case, the extra Args might just be for the constructor:

    template <typename T>
    requires(std::is_base_of<BaseA, T>::value)
    class Factory<T>
    {
    public:
        template <typename... Args>
        Factory(Args&&... args)
        {
            m_spInner = std::make_shared<T>(std::forward<Args>(args)...);
        }
    // ...
    };