Synopsis
Given a type with a variadic template constructor that forwards the arguments to an implementation class, is it possible to restrict the types being forwarded with SFINAE?
Details
First, consider the non-variadic case with a constructor taking a universal reference. Here one can disable forwarding of a non-const lvalue reference via SFINAE to use the copy constructor instead.
struct foo
{
foo() = default;
foo(foo const&)
{
std::cout << "copy" << std::endl;
}
template <
typename T,
typename Dummy = typename std::enable_if<
!std::is_same<
T,
typename std::add_lvalue_reference<foo>::type
>::value
>::type
>
foo(T&& x)
: impl(std::forward<T>(x))
{
std::cout << "uref" << std::endl;
}
foo_impl impl;
};
This restriction of the universal reference is useful because otherwise the implementation class would receive a non-const lvalue reference of type foo
, which it does not know about.
Full example at LWS.
Question
But how does this work with variadic templates? Is it possible at all? If so, how? The naive extension does not work:
template <
typename... Args,
typename Dummy = typename std::enable_if<
!std::is_same<
Args...,
typename std::add_lvalue_reference<foo>::type
>::value
>::type
>
foo(Args&&... args)
: impl(std::forward<Args>(args)...)
{
std::cout << "uref" << std::endl;
}
(Also at LWS.)
EDIT: I found that R. Martinho Fernandez blogged about a variation of this issue in 2012: http://flamingdangerzone.com/cxx11/2012/06/05/is_related.html
Here are the different ways to write a properly constrained constructor template, in increasing order of complexity and corresponding increasing order of feature-richness and decreasing order of number of gotchas.
This particular form of EnableIf will be used but this is an implementation detail that doesn't change the essence of the techniques that are outlined here. It's also assumed that there are
And
andNot
aliases to combine different metacomputations. E.g.And<std::is_integral<T>, Not<is_const<T>>>
is more convenient thanstd::integral_constant<bool, std::is_integral<T>::value && !is_const<T>::value>
.I don't recommend any particular strategy, because any constraint is much, much better than no constraint at all when it comes to constructor templates. If possible, avoid the first two techniques which have very obvious drawbacks -- the rest are elaborations on the same theme.
Constrain on self
Benefit: avoids the constructor from participating in overload resolution in the following scenario:
Drawback: participates in every other kind of overload resolution
Constrain on construction expression
Since the constructor has the moral effects of constructing a
foo_impl
fromArgs
, it seems natural to express the constraints on those exact terms:Benefit: This is now officially a constrained template, since it only participates in overload resolution if some semantic condition is met.
Drawback: Is the following valid?
If, for instance,
foo_impl
isstd::vector<double>
, then yes, the code is valid. Becausestd::vector<double> v(42);
is a valid way to construct a vector of such type, then it is valid to convert fromint
tofoo
. In other words,std::is_convertible<T, foo>::value == std::is_constructible<foo_impl, T>::value
, putting aside the matter of other constructors forfoo
(mind the swapped order of parameters -- it is unfortunate).Constrain on construction expression, explicitly
Naturally, the following comes immediately to mind:
A second attempt that marks the constructor
explicit
.Benefit: Avoids the above drawback! And it doesn't take much either -- as long as you don't forget that
explicit
.Drawbacks: If
foo_impl
isstd::string
, then the following may be inconvenient:It depends on whether
foo
is for instance meant to be a thin wrapper aroundfoo_impl
. Here is what I think is a more annoying drawback, assumingfoo_impl
isstd::pair<int, double*>
.I don't feel like
explicit
actually saves me from anything here: there are two arguments in the braces so it's obviously not a conversion, and the typefoo
already appears in the signature, so I'd like to spare with it when I feel it is redundant.std::tuple
suffers from that problem (although factories likestd::make_tuple
do ease that pain a bit).Separately constrain conversion from construction
Let's separately express construction and conversion constraints:
Usage:
Benefit: Construction and conversion of
foo_impl
are now necessary and sufficient conditions for construction and conversion offoo
. That is to say,std::is_convertible<T, foo>::value == std::is_convertible<T, foo_impl>::value
andstd::is_constructible<foo, Ts...>::value == std::is_constructible<foo_impl, T>::value
both hold (almost).Drawback?
foo f { 0, 1, 2, 3, 4 };
doesn't work iffoo_impl
is e.g.std::vector<int>
, because the constraint is in terms of a construction of the stylestd::vector<int> v(0, 1, 2, 3, 4);
. It is possible to add a further overload takingstd::initializer_list<T>
that is constrained onstd::is_convertible<std::initializer_list<T>, foo_impl>
(left as an exercise to the reader), or even an overload takingstd::initializer_list<T>, Ts&&...
(constraint also left as an exercise to the reader -- but remember that 'conversion' from more than one argument is not a construction!). Note that we don't need to modifyis_perfectly_convertible_from
to avoid overlap.The more obsequious amongst us will also make sure to discriminate narrow conversions against the other kind of conversions.
You can put
Args
inside more complex expressions and expand this likeexpression(Args)...
. ThereforeWill give you a comma seperated list of
is_same
for each argument. You could use that as template arguments to a template combining the values accordingly, giving you something like the following.I'm not entirely sure how exactly you want to restrict the arguments. Therefore I'm not sure if this will do what you want, but the you should be able to use the principle.