C++ allows constructors to be templated, but there is no syntax for explicitly specializing the constructor. Here’s a rather artificial example:
// Assume derived classes by convention have a constructor
// whose first parameter is an ObjectManager&.
struct CommonBase
{
virtual ~CommonBase(){}
virtual void initialize(int reason) = 0;
};
struct ObjectManager
{
// Concrete should derive from CommonBase
template<typename Concrete, typename...Args>
ObjectManager(int reason, Args&&...args) :
m_base(std::make_unique<Concrete>(
*this, std::forward<Args>(args)...))
{
m_base->initialize(reason);
}
std::unique_ptr<CommonBase> m_base;
};
The idea here is that you have some type, and you want to templatize the constructor. It is legal to have a templated constructor, but there is no way to explicitly specialize a constructor.
struct Widget : CommonBase
{
Widget(int param);
⟦ ... ⟧
};
// This is not allowed¹
auto manager = ObjectManager::ObjectManager<Widget>(42);
So how do you tell the constructor, “I want you to use this type for Concrete?”
Your only option is type inference, so you’ll have to make it inferrable from a parameter.
Enter std:: and friends.
We start with std::, which is an empty type that takes a single type as a template parameter. You can use this as a dummy parameter and deduce the template type parameter from it.
struct ObjectManager
{
// Concrete should derive from CommonBase
template<typename Concrete, typename...Args>
ObjectManager(int reason,
std::in_place_type_t<Concrete>,
Args&&...args) :
m_base(std::make_unique<Concrete>(
*this, std::forward<Args>(args)...))
{
m_base->initialize(reason);
}
std::unique_ptr<CommonBase> m_base;
};
// Example usage:
auto manager = ObjectManager(9, std::in_place_type_t<Derived>{}, 42);
The in_ is an empty class that is default-constructible. As a convenience, the standard library also defines a premade value:
template<T>
inline constexpr std::in_place_type_t in_place_type{};
Which lets you simplify the usage to
auto manager = ObjectManager(9, std::in_place_type<Derived>, 42);
Note that there is no member type type inside the std::, so you have to use deduction to pull it out. You can’t say
// Concrete should derive from CommonBase
template<typename Trait, typename...Args>
ObjectManager(int reason,
Trait,
Args&&...args) :
m_base(std::make_unique<typename Trait::type>(
*this, std::forward<Args>(args)...))
{
m_base->initialize(reason);
}
You might be tempted to use std::² as the type holder:
// Concrete should derive from CommonBase
template<typename Concrete, typename...Args>
ObjectManager(int reason,
std::type_identity<Concrete>,
Args&&...args) :
m_base(std::make_unique<Concrete>(
*this, std::forward<Args>(args)...))
{
m_base->initialize(reason);
}
but that is not allowed.
According to the C++ standard, std:: is a Cpp17TransformationTrait, and [meta.rqmts] spells out the requirements of various trait types in the standard library.
| Trait | Constructible? | Copyable? | Special member |
|---|---|---|---|
| Cpp17UnaryTypeTrait | Yes | Yes | value |
| Cpp17BinaryTypeTrait | Yes | Yes | value |
| Cpp17TransformationTrait | No | No | type |
Since a Cpp17TransformationTrait is not constructible, and the language does not provide any pre-made instances, there is no legal way of gaining access to an instance of a Cpp17TransformationTrait. An implemention would be within its rights to define type_ as
template<typename T>
struct type_identity
{
using type = T;
// not constructible
type_identity() = delete;
// not copyable
type_identity(type_identity const&) = delete;
}
¹ Another place you cannot specialize a templated function is operator overloading.
struct ObjectMaker
{
ObjectMaker(std::string name) : m_name(std::move(name)) {}
template<typename Concrete>
Concrete operator()() { return Concrete(m_name); }
std::string m_name;
};
void sample()
{
ObjectMaker maker("adam");
// You can't do this
auto thing1 = maker<Thing1>();
auto thing2 = maker<Thing2>();
}
You have to use more cumbersome syntax to specialize the overloaded operator:
void sample()
{
ObjectMaker maker("adam");
// You have to write it like this
auto thing1 = maker.operator()<Thing1>();
auto thing2 = maker.operator()<Thing2>();
}
It’s cumbersome, but at least it’s possible.
But if you’re going to do that, you may as well give it a name:
struct ObjectMaker
{
ObjectMaker(std::string name) : m_name(std::move(name)) {}
template<typename Concrete>
Concrete make() { return Concrete(m_name); }
std::string m_name;
};
void sample()
{
ObjectMaker maker("adam");
auto thing1 = maker.make<Thing1>();
auto thing2 = maker.make<Thing2>();
}
² For further reading: What’s the deal with std::?”
Great article!
Thank you.