Reputation: 1138
How to write a template, that would take as argument classes, whose constructors have mutually exclusive signatures?
class A
{
A(){};
public:
int a;
A(int i) : a(i) {};
};
class B{
B(){};
public:
int a,b;
B(int i,int j) : a(i), b(j) {};
};
template <class T> class C {
public:
T* _t;
C(int i[])
{ //???
_t=new T(i[0]); //if T has T(int) signature
_t=new T(i[0],i[1]); //if T has T(int,int) signature
}
~C() {delete _t;}
};
int main()
{
int Ai[]={1,2};
C<A> _c(Ai); // template should work instantiated with A and B
C<B> _c(Ai); //
return 0;
}
The signatures of A
and B
are fixed (cannot be changed to int[] e.g.). Context: I'm thinking about a wrapper, that would take a (specialized) container type as the template argument, e.g. T=vector<int>
, or T=map<int,int>
, and the problem arises when constructors need to be called.
Upvotes: 0
Views: 289
Reputation: 219205
I believe Kerrek SB's answer is partially right, but incomplete. It fails in that C<T>
's constructor is overly generic. That is, C<T>
will construct from anything if you just look at its constructor declaration. You don't find out otherwise until you've selected the constructor and you're instantiating. And by then it is too late.
Concrete example:
Let's say that C<T>
has:
friend bool operator<(const C&, const C&);
And now you want to make C<T>
the key in a map
:
std::map<C<A>, int> m;
// ...
m.erase(m.begin());
This is an error because there are two erase
overloads which now look like:
iterator erase(const_iterator position);
size_type erase(const key_type& k);
and m.begin()
is an iterator
. This iterator
will convert with equal ease to both const_iterator
and key_type
(aka C<A>
).
Now this can be fixed by calling:
m.erase(m.cbegin());
instead. But this is just the tip of the iceberg on problems that overly generic constructors cause. For example any code that branches on:
std::is_constructible<C<A>, any type and any number of them>::value
is likely to get false positives because the above will always return true.
The fix is a little messy, but very practical:
template<typename T>
struct C
{
template <class ...Args,
class = typename std::enable_if
<
std::is_constructible<T, Args...>::value
>::type
>
C(Args&& ...args)
: _t(new T(std::forward<Args>(args)...))
{
}
// ...
};
I.e. Add a constraint to the constructor such that it won't be instantiated if it isn't going to work. This is messy, ugly, whatever. Perhaps you want to dress it up with a macro. Fine. But it makes this class work where otherwise it is broken in the examples I mention above (and numerous other problems that tend to trickle in as bug reports one at a time over a period of years).
In addition to Kerrek SB's good advice on using unique_ptr<T>
over a raw pointer here, I would like to add:
That this constructor should probably be explicit
, at least until it is shown by actual use cases that it really needs to be implicit.
Consider storing a T
instead of a (possibly smart) pointer to T
. You don't need pointer semantics unless you're actually trying to point to a base class to achieve run-time polymorphism.
In summary: be wary of overly generic code, and downright paranoid of overly generic constructors.
Upvotes: 0
Reputation: 477378
Use a variadically-templated constructor:
template <typename T> struct C
{
template <typename ...Args> C(Args &&... args)
: _t(new T(std::forward<Args>(args)...))
{
}
// ... destructor? Rule of five? Don't use pointers!!!
private:
T * _t; // ouch!
};
Usage:
C<A> x(1);
C<B> y(2, 3);
(Real programmers would of course prefer a member std::unique_ptr<T> _t;
, with otherwise unchanged semantics, but allowing you to disregard all the comments.)
Upvotes: 4