21

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?

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
rubenvb
  • 74,642
  • 33
  • 187
  • 332
  • 1
    Long story short: `emplace_back` (and all of the similar forwarding factory functions, such as `std::make_unique`) always use parentheses to construct the object. Why that is, I don't know. – Quentin Apr 14 '17 at 08:26
  • 1
    @Quentin That's related to perfect forwarding of the arguments. I would think there's be a SFINAE trick to do that if possible, else resort to brace init that also works for aggregates. – rubenvb Apr 14 '17 at 08:29
  • On aggregate structs/classes, creating an intermediate copy of the object, before it is inserted into the vector, is usually not too bad, as most operations can be inlined. So `v.push_back(bla { "string", 42 });` works for C++ standards from C++11 to C++17. From C++20 on, `v.emplace_back("string", 42);` works as expected. – Kai Petzke May 08 '22 at 18:40

2 Answers2

13

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.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
5

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.

Community
  • 1
  • 1
DAle
  • 8,990
  • 2
  • 26
  • 45
  • I don't see how you end up with "changing this behaviour could cause backward compatibility loss" from reading the linked answer. Could you elaborate? – rubenvb Apr 14 '17 at 10:18
  • @rubenv I took that from original source (http://cplusplus.github.io/LWG/lwg-active.html#2089) "Altering `std::allocator::construct` to use list-initialization would, among other things, give preference to `std::initializer_list` constructor overloads, breaking valid code in an unintuitive and unfixable way...". – DAle Apr 14 '17 at 11:14
  • that seems like an unqualified statement. It could use list-initialization if and only if the non-list-initialization call is impossible. Hmm this is not the place to discuss that I guess. – rubenvb Apr 14 '17 at 11:59
  • It seems that is what the proposed solution does actually. It's just suboptimal in the grand scheme of things. – rubenvb Apr 14 '17 at 12:09