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: Link
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
AndandNotaliases 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_implfromArgs, 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_implisstd::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 frominttofoo. 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_implisstd::string, then the following may be inconvenient:It depends on whether
foois for instance meant to be a thin wrapper aroundfoo_impl. Here is what I think is a more annoying drawback, assumingfoo_implisstd::pair<int, double*>.I don’t feel like
explicitactually saves me from anything here: there are two arguments in the braces so it’s obviously not a conversion, and the typefooalready appears in the signature, so I’d like to spare with it when I feel it is redundant.std::tuplesuffers from that problem (although factories likestd::make_tupledo 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_implare 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>::valueandstd::is_constructible<foo, Ts...>::value == std::is_constructible<foo_impl, T>::valueboth hold (almost).Drawback?
foo f { 0, 1, 2, 3, 4 };doesn’t work iffoo_implis 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_fromto avoid overlap.The more obsequious amongst us will also make sure to discriminate narrow conversions against the other kind of conversions.