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 instd::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.