Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Type trait for aggregate initializability in the standard library?

The C++ standard library has std::is_constructible<Class, T...> to check if a class can be constructed from the given types as arguments.

For example, if I have a class MyClass which has a constructor MyClass(int, char), then std::is_constructible<MyClass, int, char>::value will be true.

Is there a similar standard library type trait that will check that aggregate initialization works, i.e. MyClass{int, char} is well-formed and returns a MyClass?

My use case:

I want to write a function template that converts a std::tuple to a (usually POD) class using aggregate initialization, something with the following signature:

template <typename Class, typename... T>
inline Class to_struct(std::tuple<T...>&& tp);

In order to prevent users from using this function with an invalid Class, I could write a static_assert inside this function to check if the given tp parameter has types that are convertible to the members of Class. It seems a type trait like is_aggregate_initializable<Class, T...> would come in handy.

I could roll my own implementation of this trait, but just for information, is there such a trait in the standard library that I've overlooked, or one that is soon to become part of the standard library?

like image 266
Bernard Avatar asked Dec 19 '17 08:12

Bernard


1 Answers

From the discussion in the comments and browsing the C++ reference, it seems like there is a standard library type trait for neither aggregate initializability nor list initializability, at least up to C++17.

It was highlighted in the comments that there was a distinction between list initializability in general (Class{arg1, arg2, ...}) and aggregate initializability.

List initializability (specifically direct list initializability) is easier to write a type trait for because this trait is solely dependent on the validity of a certain syntax. For my use case of testing if a struct can be constructed from the elements of a tuple, direct list initializability seems to be more appropriate.

A possible way to implement this trait (with appropriate SFINAE) is as follows:

namespace detail {
    template <typename Struct, typename = void, typename... T>
    struct is_direct_list_initializable_impl : std::false_type {};

    template <typename Struct, typename... T>
    struct is_direct_list_initializable_impl<Struct, std::void_t<decltype(Struct{ std::declval<T>()... })>, T...> : std::true_type {};
}

template <typename Struct, typename... T>
using is_direct_list_initializable = detail::is_direct_list_initializable_impl<Struct, void, T...>;

template<typename Struct, typename... T>
constexpr bool is_direct_list_initializable_v = is_direct_list_initializable<Struct, T...>::value;

Then we can test direct list initializability by doing is_direct_list_initializable_v<Class, T...>.

This also works with move semantics and perfect forwarding, because std::declval honors perfect forwarding rules.

Aggregate initializability is less straightforward, but there is a solution that covers most cases. Aggregate initialization requires the type being initialized to be an aggregate (see explanation on the C++ reference on aggregate initialization), and we have a C++17 trait std::is_aggregate that checks if a type is an aggregate.

However, it doesn't mean that just because the type is an aggregate the usual direct list initialization would be invalid. Normal list initialization that matches constructors is still allowed. For example, the following compiles:

struct point {
    int x,y;
};

int main() {
    point e1{8}; // aggregate initialization :)
    point e2{e1}; // this is not aggregate initialization!
}

To disallow this kind of list initialization, we can utilize the fact that aggregates cannot have custom (i.e. user-provided) constructors, so non-aggregate initialization must have only one parameter and Class{arg} will satisfy std::is_same_v<Class, std::decay_t<decltype(arg)>>.

Luckily, we can't have a member variable of the same type as its enclosing class, so the following is invalid:

struct point {
    point x;
};

There is a caveat to this: reference types to the same object are allowed because member references can be incomplete types (GCC, Clang, and MSVC all accepts this without any warnings):

struct point {
    point& x;
};

While unusual, this code is valid by the standard. I have no solution to detect this case and determine that point is aggregate initializable with an object of type point&.

Ignoring the caveat above (it is rare to need to use such a type), we can devise a solution that will work:

template <typename Struct, typename... T>
using is_aggregate_initializable = std::conjunction<std::is_aggregate<Struct>, is_direct_list_initializable<Struct, T...>, std::negation<std::conjunction<std::bool_constant<sizeof...(T) == 1>, std::is_same<std::decay_t<std::tuple_element_t<0, std::tuple<T...>>>, Struct>>>>;

template<typename Struct, typename... T>
constexpr bool is_aggregate_initializable_v = is_aggregate_initializable<Struct, T...>::value;

It doesn't look very nice, but does function as expected.

like image 181
Bernard Avatar answered Oct 24 '22 19:10

Bernard