Why Doesn't Emplace_Back() Use Uniform Initialization

Why doesn't emplace_back() use uniform initialization?

Great minds think alike ;v) . I submitted a defect report and suggested a change to the standard on this very topic.

http://cplusplus.github.com/LWG/lwg-active.html#2089

Also, Luc Danton helped me understand the difficulty: Direct vs uniform initialization in std::allocator.

When the EmplaceConstructible (23.2.1
[container.requirements.general]/13) requirement is used to initialize
an object, direct-initialization occurs. Initializing an aggregate or
using a std::initializer_list constructor with emplace requires naming
the initialized type and moving a temporary. This is a result of
std::allocator::construct using direct-initialization, not
list-initialization (sometimes called "uniform initialization")
syntax.

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 — there would be no way for emplace_back to access a
constructor preempted by std::initializer_list without essentially
reimplementing push_back.

std::vector<std::vector<int>> v;
v.emplace_back(3, 4); // v[0] == {4, 4, 4}, not {3, 4} as in list-initialization

The proposed compromise is to use SFINAE with std::is_constructible,
which tests whether direct-initialization is well formed. If
is_constructible is false, then an alternative
std::allocator::construct overload is chosen which uses
list-initialization. Since list-initialization always falls back on
direct-initialization, the user will see diagnostic messages as if
list-initialization (uniform-initialization) were always being used,
because the direct-initialization overload cannot fail.

I can see two corner cases that expose gaps in this scheme. One occurs
when arguments intended for std::initializer_list satisfy a
constructor, such as trying to emplace-insert a value of {3, 4} in the
above example. The workaround is to explicitly specify the
std::initializer_list type, as in
v.emplace_back(std::initializer_list(3, 4)). Since this matches
the semantics as if std::initializer_list were deduced, there seems to
be no real problem here.

The other case is when arguments intended for aggregate initialization
satisfy a constructor. Since aggregates cannot have user-defined
constructors, this requires that the first nonstatic data member of
the aggregate be implicitly convertible from the aggregate type, and
that the initializer list have one element. The workaround is to
supply an initializer for the second member. It remains impossible to
in-place construct an aggregate with only one nonstatic data member by
conversion from a type convertible to the aggregate's own type. This
seems like an acceptably small hole.

C++11 emplace_back on vectorstruct?

For anyone from the future, this behavior will be changed in C++20.

In other words, even though implementation internally will still call T(arg0, arg1, ...) it will be considered as regular T{arg0, arg1, ...} that you would expect.

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

emplacing a POD

There is no constructor X::X(int,int), which is what your call to emplace_back would use to construct the X object. Containers use allocator_traits<A>::construct(allocator, p, args) to construct objects, where p is a pointer to some allocated space and args are the arguments passed to the constructor. This is used by emplace_back. This construct function calls ::new((void*)p) T(std::forward<Args>(args)...), so it doesn't use uniform initialization.

xs.emplace_back({1, 2}) will also be an error, despite the fact that an aggregate can be constructed with list-initialization. That's because a brace-enclosed initializer list cannot be forwarded.

Why default constructor doesn't work for `vector::emplace_back`

std::emplace_back() takes the arguments you provide to it and perfect-forwards them to the constructor of the value_type object it is supposed to create (in your case, PersonC).

Table 101 of the C++11 Standard specifies the semantics of emplace_back():

Expression: a.emplace_back(args)

Return type: void

Operational semantics: Appends an object of type T constructed with
std::forward<Args>(args)...
.

There is no constructor of PersonC that accepts an int and a const char* (or anything which could be constructed from an int and a const char*, respectively), hence the error.

In case you're wondering, the only constructors that a compiler can implicitly define are the default constructor, the copy constructor, and the move constructor.

Why doesn't QTCreator warn me if an emplace_back is completely wrong?

If I do:

uniquePairs.emplace_back("literal garbage data");

It doesn't yell at me.

Because it doesn't check for correctness of (execution of) such statement.


The real question, then, is - why it doesn't do so?

The answer is: because that's usually a lot of work. Let's examine this very case:

uniquePairs.emplace_back("literal garbage data");

attempts to call template< class... Args > reference emplace_back( Args&&... args ), where Args is a parameter pack consisting of one type - const char[21]. Keep in mind that "c-string literals" have distinct types for every length of the literal. This is problem #1.

Problem #1:

Instantiating emplace_backs for all the types you're calling with it. It's not that much work, but it may add up afterwards.

Successful instantiation of said function template results in (by definition of the function), perfect forwarding, i.e., problem #2.

Problem #2:

Calling (instantiating) std::forward. This is just repeating problem #1, but in another scope. Not great, not terrible.

Problem #3:

After we already instantiated (in the background, without full intent of compilation) some function templates, we now need to check if perfectly forwarded arguments for emplace_back can construct a value_type of your std::vector.

Your value_type is std::pair<ulong, ulong>. Obviously, const char[21] cannot do that. Your IDE can now scream at you.


This is a pretty trivial case, but notice how much work had to be done at the first place. Now imagine that you are perfect forwarding something else. More arguments. More calls. More background instantiations.

Background code sanitisers should not interrupt your work. They also should (hopefully) give you reliable hints in a reasonable time fashion. This very feature may be quite trivial (your case) or very complicated. The simples answer is just to not attempt to accomplish such tasks.

Bonus:

Why, then, if you push_back("literal garbage data"), the IDE usually yells at you?

Because push_back's signature takes either const T& or T&& for std::vector<T>. It's pretty trivial to see that const char[21] is not a std::pair<ulong, ulong> (neither is convertible to one). No instantiations needed (provided that the instantiation of your vector was already done at the background. If not, add another problem to both cases). Not a lof of work. Easy to compute, easy to warn about.

Why uniform initialization works without explicit constructor

The purpose of uniform initialization is to be a single means of initializing any object which can be initialized. It therefore can pick and choose the appropriate initialization mechanism to use internally for a particular type.

Point is an aggregate. In C++98/03, it therefore could be initialized with aggregate initialization, much like an array.

Point p = {3, 4};

That was legal in C++98/03.

In C++11, aggregate initialization is one of the possible ways for uniform initialization to initialize a variable. If the type being initialized is an aggregate, then the members of the braced-init-list are used to initialize it via aggregate initialization. But aggregate initialization in C++98/03 only worked in Typename var = braced-init-list; form. Uniform initialization allows all of these forms to use it as well:

Point p{3, 4};

void Func(const Point &p);
Func({3, 4});

Point Func2()
{
return {3, 4};
}

All of these use uniform initialization to initialize a Point aggregate. However, since it's using uniform initialization syntax, you can later change Point to add a constructor. And as long as you add a constructor that takes two integers (and don't add an initializer_list constructor that takes integers), all your code will work just fine.



Related Topics



Leave a reply



Submit