Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

It seems like this code:

#include <string>
#include <vector>

struct bla
{
    std::string a;
    int b;
};

int main()
{
    std::vector<bla> v;
    v.emplace_back("string", 42);
}

could be made to work properly in this case, but it doesn't (and I understand why). Giving bla a constructor solves this, but removes the aggregateness of the type, which can have far-reaching consequences.

Is this an oversight in the Standard? Or am I missing certain cases where this will blow up in my face, or is it just not as useful as I think?

like image 397
rubenvb Avatar asked Apr 14 '17 08:04

rubenvb


2 Answers

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 using the is_aggregate type trait from C++17. But there's a problem with 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.

The language solution is the best of the bunch.

like image 70
Nicol Bolas Avatar answered Oct 08 '22 03:10

Nicol Bolas


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.

like image 31
DAle Avatar answered Oct 08 '22 01:10

DAle