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?
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.
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
and Not
aliases to combine different metacomputations. E.g. And<std::is_integral<T>, Not<is_const<T>>>
is more convenient than std::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.
template<typename T>
using Unqualified = typename std::remove_cv<
typename std::remove_reference<T>::type
>::type;
struct foo {
template<
typename... Args
, EnableIf<
Not<std::is_same<foo, Unqualified<Args>>...>
>...
>
foo(Args&&... args);
};
Benefit: avoids the constructor from participating in overload resolution in the following scenario:
foo f;
foo g = f; // typical copy constructor taking foo const& is not preferred!
Drawback: participates in every other kind of overload resolution
Since the constructor has the moral effects of constructing a foo_impl
from Args
, it seems natural to express the constraints on those exact terms:
template<
typename... Args
, EnableIf<
std::is_constructible<foo_impl, Args...>
>...
>
foo(Args&&... args);
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?
// function declaration
void fun(foo f);
fun(42);
If, for instance, foo_impl
is std::vector<double>
, then yes, the code is valid. Because std::vector<double> v(42);
is a valid way to construct a vector of such type, then it is valid to convert from int
to foo
. In other words, std::is_convertible<T, foo>::value == std::is_constructible<foo_impl, T>::value
, putting aside the matter of other constructors for foo
(mind the swapped order of parameters -- it is unfortunate).
Naturally, the following comes immediately to mind:
template<
typename... Args
, EnableIf<
std::is_constructible<foo_impl, Args...>
>...
>
explicit foo(Args&&... args);
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
is std::string
, then the following may be inconvenient:
void fun(foo f);
// No:
// fun("hello");
fun(foo { "hello" });
It depends on whether foo
is for instance meant to be a thin wrapper around foo_impl
. Here is what I think is a more annoying drawback, assuming foo_impl
is std::pair<int, double*>
.
foo make_foo()
{
// No:
// return { 42, nullptr };
return foo { 42, nullptr };
}
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 type foo
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 like std::make_tuple
do ease that pain a bit).
Let's separately express construction and conversion constraints:
// New trait that describes e.g.
// []() -> T { return { std::declval<Args>()... }; }
template<typename T, typename... Args>
struct is_perfectly_convertible_from: std::is_constructible<T, Args...> {};
template<typename T, typename U>
struct is_perfectly_convertible_from: std::is_convertible<U, T> {};
// New constructible trait that will take care that as a constraint it
// doesn't overlap with the trait above for the purposes of SFINAE
template<typename T, typename U>
struct is_perfectly_constructible
: And<
std::is_constructible<T, U>
, Not<std::is_convertible<U, T>>
> {};
Usage:
struct foo {
// General constructor
template<
typename... Args
, EnableIf< is_perfectly_convertible_from<foo_impl, Args...> >...
>
foo(Args&&... args);
// Special unary, non-convertible case
template<
typename Arg
, EnableIf< is_perfectly_constructible<foo_impl, Arg> >...
>
explicit foo(Arg&& arg);
};
Benefit: Construction and conversion of foo_impl
are now necessary and sufficient conditions for construction and conversion of foo
. That is to say, std::is_convertible<T, foo>::value == std::is_convertible<T, foo_impl>::value
and std::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 if foo_impl
is e.g. std::vector<int>
, because the constraint is in terms of a construction of the style std::vector<int> v(0, 1, 2, 3, 4);
. It is possible to add a further overload taking std::initializer_list<T>
that is constrained on std::is_convertible<std::initializer_list<T>, foo_impl>
(left as an exercise to the reader), or even an overload taking std::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 modify is_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 like expression(Args)...
. Therefore
!std::is_same<Args, typename std::add_lvalue_reference<foo>::type>::value...
Will 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.
template<bool... Args> struct and_;
template<bool A, bool... Args>
struct and_<A, Args...>{
static constexpr bool value = A && and_<Args...>::value;
};
template<bool A>
struct and_<A>{
static constexpr bool value = A;
};
//...
template <typename... Args,
typename Dummy = typename std::enable_if<
and_<!std::is_same<Args,
typename std::add_lvalue_reference<foo>::type
>::value...>::value
>::type
>
foo(Args&&... args) : impl(std::forward<Args>(args)...)
{
std::cout << "uref" << std::endl;
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With