Reputation: 7562
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
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
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
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