Why can an aggreggate struct be brace-initialized, but not emplaced using the same list of arguments as in the brace initialization?

Is this an oversight in the Standard?

It is considered a defect in the standard, tracked as LWG #2089, which was resolved by C++20. There, constructor syntax can perform aggregate initialization on an aggregate type, so long as the expressions provided wouldn't have called the copy/move/default constructors. Since all forms of indirect initialization (push_back, in_place, make_*, etc) uses constructor syntax explicitly, they can now initialize aggregates.

Pre-C++20, a good solution to it was elusive.

The fundamental problem comes from the fact that you cannot just use braced-init-lists willy-nilly. List initialization of types with constructors can actually hide constructors, such that certain constructors can be impossible to call through list initialization. This is the vector<int> v{1, 2}; problem. That creates a 2-element vector, not a 1-element vector whose only element is 2.

Because of this, you cannot use list initialization in generic contexts like allocator::construct.

Which brings us to:

I would think there's be a SFINAE trick to do that if possible, else resort to brace init that also works for aggregates.

That would require an is_aggregate type trait. Which doesn't exist at present, and nobody has proposed its existence. Oh sure, you could make do with is_constructible, as the proposed resolution to the issue states. But there's a problem with that: it effectively creates an alternative to list-initilaization.

Consider that vector<int> example from before. {1, 2} is interpreted as a two-element initializer_list. But through emplace, it would be interpreted as calling the two-integer constructor, since is_constructible from those two elements would be true. And that causes this problem:

vector<vector<float>> fvec;
fvec.emplace(1.0f, 2.0f);
vector<vector<int>> ivec;
ivec.emplace(1, 2);

These do two completely different things. In the fvec case, it performs list initialization, because vector<float> is not constructible from two floats. In the ivec case, it calls a constructor, because vector<int> is constructible from two integers.

So you need to limit list initialization in allocator::construct to only work if T is an aggregate.

And even if you did that, you would then have to propagate this SFINAE trick into all of the places where indirect initialization is used. This includes any/variant/optional's in_place constructors and emplacements, make_shared/unique calls, and so forth, none of which use allocator::construct.

And that doesn't count user code where such indirect initialization is needed. If users don't do the same initialization that the C++ standard library does, people will be upset.

This is a sticky problem to solve in a way that doesn't bifurcate indirect initialization APIs into groups that allow aggregates and groups that don't. There are many possible solutions, and none of them are ideal.


23.2.1/15.5

T is EmplaceConstructible into X from args, for zero or more arguments args, means that the following expression is well-formed:

allocator_traits<A>::construct(m, p, args)

23.2.1/15

[Note: A container calls allocator_traits<A>::construct(m, p, args) to construct an element at p using args. The default construct in std::allocator will call ::new((void*)p) T(args), but specialized allocators may choose a different definition. —end note ]

So, default allocator uses a constuctor, changing this behavior could cause backward compatibility loss. You could read more in this answer https://stackoverflow.com/a/8783004/4759200.

Also there is an issue "Towards more perfect forwarding" and some random discussion about it's future.