Does Copy List Initialization Invoke Copy Ctor Conceptually

Does copy list initialization invoke copy ctor conceptually?

The standard describes it pretty well; [dcl.init.list]/3:

List-initialization of an object or reference of type T is defined as follows:

  • [...]
  • Otherwise, if T is a class type, constructors are considered. The
    applicable constructors are enumerated and the best one is chosen
    through overload resolution (13.3, 13.3.1.7). If a narrowing
    conversion (see below) is required to convert any of the arguments,
    the program is ill-formed.

[over.match.list] (emphasis mine):

When objects of non-aggregate class type T are list-initialized
(8.5.4), overload resolution selects the constructor in two phases:

  • Initially, the candidate functions are the initializer-list constructors (8.5.4) of the class T and the argument list consists of
    the initializer list as a single argument.

  • If no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all
    the constructors of the class T and the argument list consists of the
    elements of the initializer list.


If the initializer list has no
elements and T has a default constructor, the first phase is omitted.

In copy-list-initialization, if an explicit constructor is chosen, the
initialization is ill-formed.

Hence, if no initializer-list constructor is found (as in your case), the elements of the initializer list constitute the arguments for the constructor call.
In fact, the only difference of direct-list-initialization and copy-list-initialization is covered by the last, bolded sentence.

This is one of the advantages of list-initialization: It doesn't necessitate the presence of a special member function that is not gonna be used anyway.

copy list initialization vs direct list initialization of temporary

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

What could go wrong if copy-list-initialization allowed explicit constructors?

Conceptually copy-list-initialization is the conversion of a compound value to a destination type. The paper that proposed wording and explained rationale already considered the term "copy" in "copy list initialization" unfortunate, since it doesn't really convey the actual rationale behind it. But it is kept for compatibility with existing wording. A {10, 20} pair/tuple value should not be able to copy initialize a String(int size, int reserve), because a string is not a pair.

Explicit constructors are considered but forbidden to be used. This makes sense in cases as follows

struct String {
explicit String(int size);
String(char const *value);
};

String s = { 0 };

0 does not convey the value of a string. So this results in an error because both constructors are considered, but an explicit constructor is selected, instead of the 0 being treated as a null pointer constant.

Unfortunately this also happens in overload resolution across functions

void print(String s);
void print(std::vector<int> numbers);

int main() { print({10}); }

This is ill-formed too because of an ambiguity. Some people (including me) before C++11 was released thought that this is unfortunate, but didn't came up with a paper proposing a change regarding this (as far as I am aware).

c++ - Direct and Copy Constructors

This isn't copy initialization:

UnusualClass k2=56;   // NOT Copy initialization
// for 56 is not of type UnusualClass

It will call the constructor:

UnusualClass(int a)

I think you meant:

UnusualClass k1(5);    //Direct initialization
UnusualClass k2{k1}; //Copy initialization
UnusualClass k2 = k1; //Copy initialization

Note the type needed in copy initialization.

UnusualClass(const UnusualClass &n) // const reference to type UnusualClass

It should be the object's type which is UnusualClass, not int

UPDATE

I get an error saying use of deleted function

UnusualClass::UnusualClass(const UnusualClass&).

Why would I get this error if it skips this constructor anyways?

UnusualClass::UnusualClass(const UnusualClass&) = delete;

means:

From cppreference

Avoiding implicit generation of the copy constructor.

Thus, you will need to define your own copy constructor.

UPDATE 2

Refer more to @songyuanyao's answer for copy-initialization

Why does copy initialisation with braces elide copy/move construction?

copy initialisation with braces

There is no such thing. If you use a braced-init-list to initialize an object, you are performing some form of list initialization. There are two forms of this: copy-list-initialization, and direct-list-initialization. In C++14, these have no relation to copy-initialization and direct-initialization (technically, direct-list-initialization is a grammatical form of direct-initialization, but since list-initialization bypasses everything that direct-initialization would have done, it's easier to say that direct-list-initialization is its own beast).

List initialization as a concept initializes an object. Using Typename t{} is direct-list-initialization, while Typename t = {} is copy-list-initialization. But regardless of which form is involved, no temporary is created; list initialization initializes the object in question. The only object in your example is e, so that is the object which gets initialized.

In accord with the C++14 rules for list-initialization, e gets initialized by calling a constructor, passing it a value of 1, which is the only value in the braced-init-list.

Strange behavior of copy-initialization, doesn't call the copy-constructor!

Are you asking why the compiler does the access check? 12.8/14 in C++03:

A program is ill-formed if the copy
constructor or the copy assignment
operator for an object is implicitly
used and the special member function
is not accessible

When the implementation "omits the copy construction" (permitted by 12.8/15), I don't believe this means that the copy ctor is no longer "implicitly used", it just isn't executed.

Or are you asking why the standard says that? If copy elision were an exception to this rule about the access check, your program would be well-formed in implementations that successfully perform the elision, but ill-formed in implementations that don't.

I'm pretty sure the authors would consider this a Bad Thing. Certainly it's easier to write portable code this way -- the compiler tells you if you write code that attempts to copy a non-copyable object, even if the copy happens to be elided in your implementation. I suspect that it could also inconvenience implementers to figure out whether the optimization will be successful before checking access (or to defer the access check until after the optimization is attempted), although I have no idea whether that warranted consideration.

Could this behavior be dangerous? I
mean, I might do some other useful
thing in the copy-ctor, but if it
doesn't call it, then does it not
alter the behavior of the program?

Of course it could be dangerous - side-effects in copy constructors occur if and only if the object is actually copied, and you should design them accordingly: the standard says copies can be elided, so don't put code in a copy constructor unless you're happy for it to be elided under the conditions defined in 12.8/15:

MyObject(const MyObject &other) {
std::cout << "copy " << (void*)(&other) << " to " << (void*)this << "\n"; // OK
std::cout << "object returned from function\n"; // dangerous: if the copy is
// elided then an object will be returned but you won't see the message.
}


Related Topics



Leave a reply



Submit