Free Wildebeest
Free Wildebeest

Reputation: 7562

Overloaded functions with variadic template arguments

I have some code that I want to convert from using overloaded functions to using variadic template arguments.

The existing code has multiple templated functions for the different parameter counts:

#include <iostream>
#include <string>

struct Boo
{
    Boo(const std::string& name)
    {
        std::cout << "Boo constructor: " << name << std::endl;
    };   
    static std::string CreateName(const std::string& name)
    {
        return "BooID:" + name;   
    }
};

struct Foo
{
    Foo(const std::string& name)
    {
        std::cout << "Foo constructor: " << name << std::endl;
    };
};

template <typename T>
T Construct(const std::string& name)
{
   return T(T::CreateName(name));
}

template <typename T>
T Construct(const std::string& name1, const std::string& name2)
{
   return T(T::CreateName(name1, name2));
}

// Class Foo doesn't have CreateName available
template<>
Foo Construct<Foo>(const std::string& name)
{
    return Foo{"ID:" + name};
}

int main()
{
    Construct<Boo>(std::string("123"));
    Construct<Foo>(std::string("456"));
}

This outputs:

Boo constructor: BooID:123
Foo constructor: ID:456

If I try to replace the two templated Construct() functions with a single variadic templated version then I can't work out how I should specify the special version just for the Foo class (tried on GCC 4.9.2, 8.1 and Visual Studio 2015).

template <class T, typename ...Args>
T Construct(Args... args)
{
   return T(T::CreateName(args...));
}

// Class Foo needs some extra information when constructed with strings...
template<>
Foo Construct<Foo>(const std::string& name)
{
    return Foo{"ID:" + name};
}

Fails with the compiler trying to use the variadic template version (so no CreateName in Foo). As does:

template <class T, typename ...Args>
T Construct(Args... args)
{
   return T(T::CreateName(args...));
}

// Class Foo needs some extra information when constructed with strings...
template<>
Foo Construct<Foo, const std::string&>(const std::string& name)
{
   return Foo{"ID:" + name};
}

I can get it to work by making a generic template and using std::enable_if_t to restrict the types:

template <class T, typename ...Args>
T Construct(Args... args)
{
   return T(T::CreateName(args...));
}

// Class Foo needs some extra information when constructed with strings...
template<typename T, typename = std::enable_if_t<std::is_base_of<Foo, T>::value>>
T Construct(const std::string& name)
{
    return T{"ID:" + name};
}

Which isn't as easy to read as the original template<> Foo Construct<Foo>(...) version. Is using std::enable_if_t with variadic template the only option, or is there some way of overloading that will give the desired behaviour?

Upvotes: 0

Views: 80

Answers (3)

Barry
Barry

Reputation: 303890

Don't specialize function templates. The easiest approach is to just defer to a class template:

template <typename T, typename... Args>
T Construct(Args&&... args) {
    return ConstructorImpl<T>::apply(std::forward<Args>(args)...);
}

That makes it straightforward to see how to specialize:

template <typename T>
struct ConstructorImpl {
    template <typename... Args>
    static T apply(Args&&... args) {
        return T(T::CreateName(std::forward<Args>(args)...));
    }
};

template <>
struct ConstructImpl<Foo> {
    static Foo apply(std::string const& name) {
        return Foo("ID:" + name});
    }
};

Applying constraints to Construct that ConstructorImpl<T>::apply is invokable with Args... follows naturally using this approach. It's just as easy as:

template <typename T, typename... Args>
auto Construct(Args&&... args)
    -> decltype(ConstructorImpl<T>::apply(std::forward<Args>(args)...))
{
    return ConstructorImpl<T>::apply(std::forward<Args>(args)...);
}

(Although granted this doesn't check that it actually returns a T)

Upvotes: 1

max66
max66

Reputation: 66230

The problem is that you can't partial specialize a template function.

I propose to pass from a template struct with a template (or not, in Boo specialization) method func()

template

struct Constr
 {
   template <typename ... Args>
   static T func (Args const & ... names)
    { return T(T::CreateName(names...)); }
 };

template <>
struct Constr<Foo>
 {
   static Foo func (std::string const & name)
    { return Foo{"ID:" + name}; }
 };

The calls become

Constr<Boo>::func(std::string("123"));
Constr<Foo>::func(std::string("456"));

Upvotes: 0

super
super

Reputation: 12978

The problem here is that a specialization must match the base template.

Your base template will be deduced to Construct<Foo, std::string> and your specialization must match this to be seen as valid.

If you change the specialization to

template<>
Foo Construct<Foo, std::string>(std::string name)
{
    return Foo{"ID:" + name};
}

it works ok on gcc.

Upvotes: 2

Related Questions