copy list initialization vs direct list initialization of temporary
Asked Answered
M

1

5

Given the following struct:

struct ABC
{
    ABC(){cout << "ABC" << endl;}
    ~ABC() noexcept {cout << "~ABC" << endl;}
    ABC(ABC const&) {cout << "copy" << endl;}
    ABC(ABC&&) noexcept {cout << "move" << endl;}
    ABC& operator=(ABC const&){cout << "copy=" << endl;}
    ABC& operator=(ABC&&) noexcept {cout << "move=" << endl;}
};

The output of:

std::pair<std::string, ABC> myPair{{}, {}};

is:

ABC
copy
~ABC
~ABC

While the output of:

std::pair<std::string, ABC> myPair{{}, ABC{}};

is:

ABC
move
~ABC
~ABC

In attempting to understand the difference between the two I think I have identified that the first case is using copy-list-initialization, while the second one uses direct-list-initialization of an unnamed temporary (numbers 7 and 2, respectively, in here: http://en.cppreference.com/w/cpp/language/list_initialization).

Searching for similar questions I've found this: Why does the standard differentiate between direct-list-initialization and copy-list-initialization? and this: Does copy list initialization invoke copy ctor conceptually?.

The answers in those questions discuss the fact that for copy-list-initialization, the use of an explicit constructor would render the code ill-formed. In fact, if I make ABC's default constructor explicit, my first example won't compile but that is (perhaps) a different matter.

So, the question is: Why is the temporary copied in the first case but moved in the second? What prevents it from being moved in the case of copy-list-initialization?

As a note, the following code:

std::pair<std::string, ABC> myPair = std::make_pair<string, ABC>({}, {});

Also results in a call to ABC's move constructor (and no copy constructor call), but different mechanisms may be involved.

You can try the code out (using gcc-4.9.2 in C++14 mode) at: https://ideone.com/Kc8xIn

Maisiemaison answered 5/5, 2015 at 14:46 Comment(0)
F
8

In general, braced-init-lists like {} are not expressions and do not have a type. If you have a function template

template<typename T> void f(T);

and call f( {} ), no type will be deduced for T, and type deduction will fail.

On the other hand, ABC{} is a prvalue expression of type ABC (an "explicit type conversion in functional notation"). For a call like f( ABC{} ), the function template can deduce the type ABC from this expression.


In C++14, as well as in C++11, std::pair has the following constructors [pairs.pair]; T1 and T2 are the names of the template parameter of the std::pair class template:

pair(const pair&) = default;
pair(pair&&) = default;
constexpr pair();
constexpr pair(const T1& x, const T2& y);
template<class U, class V> constexpr pair(U&& x, V&& y);
template<class U, class V> constexpr pair(const pair<U, V>& p);
template<class U, class V> constexpr pair(pair<U, V>&& p);
template <class... Args1, class... Args2>
pair(piecewise_construct_t, tuple<Args1...>, tuple<Args2...>);

Note that there is a constructor

constexpr pair(const T1& x, const T2& y); // (C)

But no

constexpr pair(T1&& x, T2&& y);

instead, there is a perfectly forwarding

template<class U, class V> constexpr pair(U&& x, V&& y); // (P)

If you try to initialize a std::pair with two initializers where at least one of them is a braced-init-list, the constructor (P) is not viable since it cannot deduce its template arguments.

(C) is not a constructor template. Its parameter types T1 const& and T2 const& are fixed by the class template parameters. A reference to a constant type can be initialized from an empty braced-init-list. This creates a temporary object that is bound to the reference. As the type referred to is const, the (C) constructor will copy its arguments into the class' data members.


When you initialize a pair via std::pair<T,U>{ T{}, U{} }, the T{} and U{} are prvalue-expressions. The constructor template (P) can deduce their types and is viable. The instantiation produced after type deduction is a better match than the (C) constructor, because (P) will produce rvalue-reference parameters and bind the prvalue arguments to them. (C) on the other hand binds the prvalue arguments to lvalue-references.


Why then does the live example move the second argument when called via std::pair<T,U>{ {}, U{} }?

libstdc++ defines additional constructors. Below is an extract of its std::pair implementation from 78536ab78e, omitting function definitions, some comments, and SFINAE. _T1 and _T2 are the names of the template parameters of the std::pair class template.

  _GLIBCXX_CONSTEXPR pair();

  _GLIBCXX_CONSTEXPR pair(const _T1& __a, const _T2& __b); // (C)

  template<class _U1, class _U2>
constexpr pair(const pair<_U1, _U2>& __p);

  constexpr pair(const pair&) = default;
  constexpr pair(pair&&) = default;

  // DR 811.
  template<class _U1>
constexpr pair(_U1&& __x, const _T2& __y); // (X)

  template<class _U2>
constexpr pair(const _T1& __x, _U2&& __y); // (E) <=====================

  template<class _U1, class _U2>
constexpr pair(_U1&& __x, _U2&& __y);      // (P)

  template<class _U1, class _U2>
constexpr pair(pair<_U1, _U2>&& __p);

  template<typename... _Args1, typename... _Args2>
    pair(piecewise_construct_t, tuple<_Args1...>, tuple<_Args2...>);

Note the (E) constructor template: It will copy the first argument and perfectly forward the second. For an initialization like std::pair<T,U>{ {}, U{} }, it is viable because it only needs to deduce a type from the second argument. It is also a better match than (C) for the second argument, and hence a better match overall.

The "DR 811" comment is in the libstdc++ sources. It refers to LWG DR 811 which adds some SFINAE, but no new constructors.

The constructors (E) and (X) are a libstdc++ extension. I'm not sure if it's compliant, though.

libc++ on the other hand does not have this additional constructors. For the example std::pair<T,U>{ {}, U{} }, it will copy the second argument.

Live demo with both library implementations

Flanagan answered 5/5, 2015 at 14:58 Comment(6)
how can perfectly-forwarding constructor be selected for myPair{{}, ABC{}}; call, if it can't deduce std::string from {}?Alienable
@MarcAndreson Oops, my bad. I misread this. If you're using libstdc++, this might be an extension. Let me try to look it up.Flanagan
The reason for those overloads is mostly to support stuff like std::pair<std::unique_ptr<int>, int *> p(std::unique_ptr<int>(), 0);. See PR 40925,Phrenic
@Phrenic PR 40925 seems to contain an example similar to LWG 811, but there's a comma at the end of your comment. Is there anything missing?Flanagan
@Flanagan I was linking to comment 8, which is the commit adding those overloads with the comment Add, to deal correctly with move-only types in the presence of "null pointers".Phrenic
@Phrenic Ah, thanks. It did not occur to me to look into the commit/change message of that comment. I've added it and the example to my email to std-discussion.Flanagan

© 2022 - 2024 — McMap. All rights reserved.