Calling Constructor with Braces

Calling constructor with braces

It is neither initializer list, nor uniform initialization. What's the thing is this?

Your premise is wrong. It is uniform initialization and, in Standardese terms, direct-brace-initialization.

Unless a constructor accepting an std::initializer_list is present, using braces for constructing objects is equivalent to using parentheses.

The advantage of using braces is that the syntax is immune to the Most Vexing Parse problem:

struct Y { };

struct X
{
X(Y) { }
};

// ...

X x1(Y()); // MVP: Declares a function called x1 which returns
// a value of type X and accepts a function that
// takes no argument and returns a value of type Y.

X x2{Y()}; // OK, constructs an object of type X called x2 and
// provides a default-constructed temporary object
// of type Y in input to X's constructor.

c++ : calling constructors via curly braces?

In your case, there is simply no difference. But if you modify your code a bit, there will be a visible difference!

First of all, you can construct your type in different ways, all described here: initilization

The difference come in, if your class provides also a constructor which takes a std::initializer_list.

See the following code modified/extended to show the difference:

class A 
{
public:
A(int value):value_(value){ std::cout << "int" << std::endl;}
A(const std::initializer_list<int>& ){ std::cout << "list" << std::endl;}
void print()
{
std::cout << value_ << std::endl;
}
private:
int value_;
};

A get_a1(int value)
{
std::cout << "()" << std::endl;
return A(value);
}

A get_a2(int value)
{
std::cout << "{}" << std::endl;
return {value};
}

int main()
{
A a1 = get_a1(1);
a1.print();
A a2 = get_a2(2);
a2.print();
}

If you run that prog, you will see that using {} will call the constructor with std::initializer_list and using () will use your int constructor.

Why is described here in the standard:

§13.3.1.7 [over.match.list]/p1:

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.

In addition, initializer list constructors do not allow narrowing!

§8.5.4 List-initialization

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

Calling constructor with braces instead parantheses

Yes, a mem-initializer such as Foo{42} can contain either a parenthesized expression-list or a braced-init-list. This is the case regardless of whether the mem-initializer-id denotes the constructor's class, a base class, or a member: that is, both when the constructor delegates and when it does not. See the grammar in [class.base.init].

Furthermore, the standard specifies ([class.base.init]/7 in C++14) that the initialization by the expression-list or braced-init-list occurs according to the usual rules of initialization. Therefore if the initializer is a braced-init-list then std::initializer_list constructors will be favoured in overload resolution.

What is this constructor call with following double braces?

This is known as double brace initialization:

The first brace creates a new
AnonymousInnerClass, the second
declares an instance initializer block
that is run when the anonymous inner
class is instantiated. This type of
initializer block is formally called
an "instance initializer", because it
is declared within the instance scope
of the class -- "static initializers"
are a related concept where the
keyword static is placed before the
brace that starts the block, and which
is executed at the class level as soon
as the classloader completes loading
the class (specified at
http://java.sun.com/docs/books/jls/third_edition/html/classes.html#8.6)
The initializer block can use any
methods, fields and final variables
available in the containing scope, but
one has to be wary of the fact that
initializers are run before
constructors.

This only works only for non-final
classes because it creates an
anonymous subclass.

What constructor should be called in return statement with curly braces?

There is quite some implementation confusion regarding return value copy elision and temporary materialization. To begin with, let's have a look at the OP's example slightly modified to not use the braced-init-list in the return statement:

// Program (A1)
struct NonMovable {
NonMovable() = default;
NonMovable(const NonMovable&) = default;
NonMovable(NonMovable&&) = delete; // #1
};

NonMovable f() {
NonMovable nonMovable;
return nonMovable; // #2
// GCC, Clang: OK
// MSVC: Error (use of deleted function)
}

int main() {}

#1 formally provides an explicitly-deleted definition for the move constructor, meaning the move constructor will participate in overload resolution, and its lack of a non-deleted definition will have no effect on the result of overload resolution.

Now, as per [class.copy.elision]/3, overload resolution for the copy-init context of #2 is (possibly) two-phase, starting to look for an ctor overload as if the object were designated by an rvalue:

In the following copy-initialization contexts, a move operation might be used instead of a copy operation:

(3.1) If the expression in a return statement is a (possibly parenthesized) id-expression that names an object with automatic storage duration declared in the body or parameter-declaration-clause of the innermost enclosing function or lambda-expression, or [...]

overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue. If the first overload resolution fails or was not performed, or if the type of the first parameter of the selected constructor is not an rvalue reference to the object's type (possibly cv-qualified), overload resolution is performed again, considering the object as an lvalue.

The key here being that the second phase is only performed if the first phase overload resolution fails (or was not performed). In the example above, the first phase overload resolution will find the deleted move ctor, and the second phase will not be performed. Thus, solely based on [class.copy.elision]/3, one would argue that program (A) is ill-formed.

On the other hand, all non-deleted constructors in NonMovable are trivial, meaning we may turn to [class.temporary]/3,

When an object of class type X is passed to or returned from a function, if each copy constructor, move constructor, and destructor of X is either trivial or deleted, and X has at least one non-deleted copy or move constructor, implementations are permitted to create a temporary object to hold the function parameter or result object. The temporary object is constructed from the function argument or return value, respectively, and the function's parameter or return object is initialized as if by using the non-deleted trivial constructor to copy the temporary (even if that constructor is inaccessible or would not be selected by overload resolution to perform a copy or move of the object).

Which would arguably overrule the fact that [class.copy.elision]/3 would otherwise reject the program.

Before going into what the compilers actually do here, consider a slightly modified version of program (A1) above:

// Program (A2)
struct NonMovable {
NonMovable() {};
NonMovable(const NonMovable&) {};
NonMovable(NonMovable&&) = delete; // #1
};

// ... as above

where the default and copy ctors have now been made non-trivial as they are user-provided.

Then:

  • Clang 11: accepts both (A1) and (A2) for C++14 through C++20
  • Clang 13: rejects both (A1) and (A2) for C++14 through C++20
  • GCC 10: accepts both (A1) and (A2) for C++14 through C++20
  • GCC 11: accepts both (A1) and (A2) for C++14 through C++17; rejects both for C++20
  • MSVC: rejects both (A1) and (A2) for C++14 through C++20

If we tweak program (A1) and (A2) but wrapping the return object in braces,

// ... as above
return {nonMovable};

denoting the corresponding programs as (B1) and (B2), then:

  • Clang 11: accepts both (B1) and (B2) for C++14 through C++20
  • Clang 13: accepts both (B1) and (B2) for C++14 through C++20
  • GCC 10: accepts both (B1) and (B2) for C++14 through C++20
  • GCC 11: accepts both (B1) and (B2) for C++14 through C++20
  • MSVC: rejects both (B1) and (B2) for C++14 through C++20


Who's correct? What type of constructor should be called in return {var}; statement there?

To wrap up and answer the OP's original question, MSVC is wrong to reject (B1) and (B2), as [class.copy.elision]/3, particularly /3.1, does not apply when the expression in a return statement is a braced-init-list, even if it wraps a named object with automatic storage duration. This case is simply braced-copy-init, and the copy constructor shall be the best viable function resulting from overload resolution.

The implementation variance for (A1) and (A2) likely relates to P1825R0: Merged wording for P0527R1 and P1155R3 (more implicit moves), which would explain why both GCC 11 and Clang 13 now rejects both (A1) and (A2) for C++20, and we may moreover note that GCC and Clang particularly marks P1825R0 as implemented in release 11 and 13, respectively.

I do not understand, however, why Clang 13 also seems to have backported this, particularly rejecting (A1) and (A2) also for C++14 and C++17. We may note that Clang (even in earlier version) also rejects the example of [diff.cpp17.class]/3 (added to the C++20 standard as part of P1825R0, highlighting a compatibility change from C++17) in C++14 and C++17 which is arguably a Clang bug. I'm speculating that the backporting rejection of (A1) and (A2) is unintentional.

Returning constructor arguments in braces?

return {1, 2};, the return value is list-initialized from {1, 2}, as the effect, the returned std::vector<double> contains 2 elements with value 1 and 2.

return std::vector<double>{1, 2};, the return value is copy-initialized from std::vector<double>{1, 2}, as the effect, the returned std::vector<double> contains 2 elements with value 1 and 2. In concept it'll construct a temporary std::vector<double> and the return value is copy-initialized from the temporary; because of mandatory copy elision (since C++17) the copy/move operation is ommitted and the effect is exactly the same as the 1st case.

return std::vector<double>(1, 2), the return value is copy-initialized from std::vector<double>(1, 2), as the effect, the returned std::vector<double> contains 1 elements with value 2. Mandatory copy elision takes effect in this case too.

Is assign with braces the same as call the constructor?

This is direct list initialization.

shared_ptr<int> myIntSmartPtr { my_alloc(42), my_free };

This is an example of the first syntax:

T object { arg1, arg2, ... };   (1)

The exact effect it has is therefore

List initialization is performed in the following situations:

  • direct-list-initialization (both explicit and non-explicit constructors are considered)
    1. initialization of a named variable with a braced-init-list (that is, a possibly empty brace-enclosed list of expressions or nested braced-init-lists)

And for more detail about what that actually means:

The effects of list-initialization of an object of type T are:

... [A bunch of cases that don't apply]

Otherwise, the constructors of T are considered, in two phases:

  • All constructors that take std::initializer_list as the only argument, or as the first argument if the remaining arguments have default values, are examined, and matched by overload resolution against a single argument of type std::initializer_list
  • If the previous stage does not produce a match, all constructors of T participate in overload resolution against the set of arguments that consists of the elements of the braced-init-list, with the restriction that only non-narrowing conversions are allowed. If this stage produces an explicit constructor as the best match for a copy-list-initialization, compilation fails (note, in simple copy-initialization, explicit constructors are not considered at all).

std::shared_ptr does not have a constructor that takes an std::initializer_list, so the second bullet point applies and it's constructed from the arguments therein.



Related Topics



Leave a reply



Submit