11

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?

Bernard
  • 5,209
  • 1
  • 34
  • 64
  • 1
    I don't see why it would be all that useful, to be honest. `std::is_constructible` pretty much exists so that generic code, for a type like `std::vector`, could avoid doing list initialization by accident. Why can't you just do `MyClass { Args... }` and need to care about this being an aggregate or not? – StoryTeller - Unslander Monica Dec 19 '17 at 08:39
  • @StoryTeller In more general code, `to_struct()` should work as long as `std::tuple_size` and `std::tuple_element` are defined, so it should look like `template inline Class to_struct(Tuple&& tp);`, with proper internal implementation functions that do not rely on `std::tuple`. After that, for example, I might want to have another overload of `to_struct()` that wraps `Class` around the given object, *without* unpacking (only if it can't be unpacked). In this case, I will need to restrict the first overload (probably using SFINAE stuff) using the type trait. – Bernard Dec 19 '17 at 08:50
  • @Bernard: "*I could roll my own implementation of this trait*" No, you can't. Without an `is_aggregate` trait, there is no way you can tell the difference between `aggregate{1, 2}` working and `non_aggregate{1, 2}` working. – Nicol Bolas Dec 19 '17 at 13:22
  • 4
    @NicolBolas [`is_aggregate`](http://en.cppreference.com/w/cpp/types/is_aggregate) – Barry Dec 19 '17 at 13:25
  • Really, you want to know if `T foo{a,b,c}` is legal? Do you care if that uses a normal constructor or aggregate initialization? – Yakk - Adam Nevraumont Dec 19 '17 at 14:56
  • @Yakk I didn't give much thought as to whether normal constructors are allowed. But as per the comment above, I would be able to use [`std::is_aggregate`](http://en.cppreference.com/w/cpp/types/is_aggregate) to constrain the template if I only want to allow aggregate initialization. For this use case, it seems useful to allow the creation of objects that are non-aggregate types. – Bernard Dec 19 '17 at 16:19
  • @Bernard So really what you want is `is_list_initializable` that checks whether `Class{arg1, arg2, arg3}` is valid? – Daniel H Dec 19 '17 at 20:00
  • @Daniel Yes, for this use case `is_list_initializable` is probably best. But some information about `is_aggregate_initializable` would be educational too. – Bernard Dec 20 '17 at 01:15

1 Answers1

6

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.

Bernard
  • 5,209
  • 1
  • 34
  • 64
  • In independently stumbling down this same rabbit hole, I've encountered a stumbling block with exception specifications. Before I invest too much more, I'm hoping there's a quick yes or no: Is a `is_nothrow_aggregate_initializable` trait possible, even in theory? – ildjarn May 17 '22 at 22:16
  • @ildjarn You should be able to replace `is_direct_list_initializable` with an `is_nothrow_direct_list_initializable` type trait, and it will work with the same caveat. Since there's a [noexcept operator](https://en.cppreference.com/w/cpp/language/noexcept) that tests if an arbitrary expression is noexcept, you should be able to use `std::enable_if` to SFINAE on that to build your own `is_nothrow_direct_list_initializable`. – Bernard May 18 '22 at 14:02
  • Unfortunately the noexcept operator is not so all-encompassing; I wish it were. Consider: https://godbolt.org/z/175vzz43E I hope I'm missing something obvious here :-] – ildjarn May 18 '22 at 20:34