Is-Braces-Constructible Type Trait

Is-braces-constructible type trait

template<class T, typename... Args>
decltype(void(T{std::declval<Args>()...}), std::true_type())
test(int);

template<class T, typename... Args>
std::false_type
test(...);

template<class T, typename... Args>
struct is_braces_constructible : decltype(test<T, Args...>(0))
{
};

Conditionally enable a constructor whenever a member variable can be constructed by variadic arguments

As Rostislav mentioned, if T is not a function type, std::is_constructible_v<T, Args> is true iff the variable definition T obj(std::declval<Args>()...); is well-formed. That's not the case in bar<1> obj(0.);, cause bar<1> has no corresponding constructor.

In contrast, bar1<1> obj{ 0. }; is well-formed. Using the proposed Detection Toolkit, we could use

template<class T, typename... Arguments>
using initializable_t = decltype(T{ std::declval<Arguments>()... });

template<class T, typename... Arguments>
constexpr bool is_initializable_v = is_detected_v<initializable_t, T, Arguments...>;

and change the check to

template<class... Elements, class U = Tuple,
class = std::enable_if_t<is_initializable_v<U, decltype(static_cast<value_type>(std::declval<Elements>()))...>>>

I think that's more readable than the plain decltype approach.

Type traits in VS2013

This seems to be a known bug.

Link1: http://connect.microsoft.com/VisualStudio/feedback/details/800328/std-is-copy-constructible-is-broken

Link2: http://connect.microsoft.com/VisualStudio/feedback/details/802032/std-is-copy-constructible-doesnt-work-correctly

Arity of aggregate in logarithmic time

Discussion

(The discussion is based on another answer of mine which I will delete now.)

As in the original question, the following answer checks whether the invocation of the constructor of the aggregate is possible with a given number of arguments. For aggregates, one can base a binary search on this pattern by using the following properties from the standard:

8.5.1 (6):

An initializer-list is ill-formed if the number of initializer-clauses
exceeds the number of members or elements to initialize
. [ Example:
char cv[4] = { ’a’, ’s’, ’d’, ’f’, 0 }; // error is ill-formed. — end
example ]

and

8.5.1 (7):

If there are fewer initializer-clauses in the list than there are
members in the aggregate, then each member not explicitly initialized
shall be initialized from its default member initializer
(9.2) or, if
there is no default member initializer, from an empty initializer list
(8.5.4). [ Example: struct S { int a; const char* b; int c; int d =
b[a]; }; S ss = { 1, "asdf" }; initializes ss.a with 1, ss.b with
"asdf", ss.c with the value of an expression of the form int{} (that
is, 0), and ss.d with the value of ss.b[ss.a] (that is, ’s’), and in
struct X { int i, j, k = 42; }; X a[] = { 1, 2, 3, 4, 5, 6 }; X b[2] =
{ { 1, 2, 3 }, { 4, 5, 6 } }; a and b have the same value — end
example ]

However, as you already implied by the question title, a binary search will in general not work with non-aggregates, first due to the fact that those are usually not callable with less parameters than necessary, and next due to the fact that non-aggregates can have explicit constructors so that the "conversion-to-anything" trick via the struct filler won't work.

Implementation

First ingredient is an is_callable check from here:

template<typename V, typename ... Args>
struct is_constructible_impl
{
template<typename C> static constexpr auto test(int) -> decltype(C{std::declval<Args>() ...}, bool{}) { return true; }
template<typename> static constexpr auto test(...) { return false; }
static constexpr bool value = test<V>(int{});
using type = std::integral_constant<bool, value>;
};

template<typename ... Args>
using is_constructible = typename is_callable_impl<Args...>::type;

Note that this one is usable also with a fewer number of parameters than necessary (unlike your check).

Next a helper function which takes an integer argument and returns whether the aggregate is callable with the corresponding number of constructor arguments:

template<typename A, size_t ... I>
constexpr auto check_impl(std::index_sequence<I ...>)
{
return is_constructible<A, decltype(I, filler{}) ...>::value;
}

template<typename A, size_t N>
constexpr auto check()
{
return check_impl<A>(std::make_index_sequence<N>{});
}

And finally the binary search:

template<typename A, size_t Low, size_t Up, size_t i = Low + (Up - Low)/2>
struct binary_search
: public std::conditional_t<check<A, i>() && !check<A,i+1>()
, std::integral_constant<size_t, i>
, std::conditional_t<check<A, i>()
, binary_search<A, i, Up>
, binary_search<A, Low, i> >
>
{};

Use it as

int main()
{
static_assert(binary_search<A2,0,10>::value==2);
}

Live on Coliru

(Im)perfect forwarding with variadic templates

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.

Constrain on self

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

Constrain on construction expression

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).

Constrain on construction expression, explicitly

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).

Separately constrain conversion from construction

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.

Constraint on std::optional's forwarding reference constructor

Lying traits are ungood.

Lying traits for a fundamental vocabulary type are plusungood.

Lying traits for a fundamental vocabulary type that can also easily interfere with overload resolution are doubleplusungood.

void f(std::optional<int>);
void f(std::optional<const char*>);
f({""}); // ambiguous without the constraint


Related Topics



Leave a reply



Submit