I'm trying to understand how the following construction behaves differently across C++ standards, and what the guarantees are on elements a and b.
struct X {
int a;
int b;
};
void func() {
X x(1); // (1) Works in c++2a only
X y{1}; // (2) Works in c++1z
}
This has nothing to do with the default constructor being trivial, or really with the default constructor at all.
X y{1}; does aggregate-initialization which doesn't use any constructor, but instead initialized members of an aggregate (which your class is since it doesn't explicitly declare any constructors) directly one-by-one from the braced initializer list. The remaining members which are not explicitly given a value are value-initialized, which here means that b will be zero-initialized.
X x(1); can also initialize aggregates in the same way (with minor differences not relevant here) since C++20. Before that, non-empty parenthesized initializers would only try constructors. But there is no constructor that takes a single argument of type int in your class. There is only an implicitly-declared default constructor which takes no arguments and implicitly-declared copy and move constructors, which take references to a X as argument.
Related
In C++17, consider a case where S is a struct with a deleted default constructor and a float member, when S is initialized with empty braces, is the float member guaranteed by the standard to be zero-initialized?
struct A {
int x{};
};
struct S
{
S() = delete;
A a;
float b;
};
int main()
{
auto s = S{}; // Is s.b guaranteed to be zero?
}
In my opinion, cppreference.com is not clear, saying both that:
If the number of initializer clauses is less than the number of members and basesor initializer list is completely empty, the remaining members and bases (since C++17) are initialized by their default member initializers, if provided in the class definition, and otherwise (since C++14) copy-initialized from empty lists, in accordance with the usual list-initialization rules (which performs value-initialization for non-class types and non-aggregate classes with default constructors, and aggregate initialization for aggregates). If a member of a reference type is one of these remaining members, the program is ill-formed.
(from here), which implies that b is guaranteed to be zero
In all cases, if the empty pair of braces {} is used and T is an aggregate type, aggregate-initialization is performed instead of value-initialization.
(from here)
which implies that b is not guaranteed to be zero.
There is also a discussion that seems to imply that while not guaranteed, all known compiler zero-initialize anyway:
The standard specifies that zero-initialization is not performed when the class has a user-provided or deleted default constructor, even if that default constructor is not selected by overload resolution. All known compilers performs additional zero-initialization if a non-deleted defaulted default constructor is selected.
Related to Why does aggregate initialization not work anymore since C++20 if a constructor is explicitly defaulted or deleted?
This is a quirk of C++ that is fixed in C++20. In the meantime you can add explicit to the deleted default constructor to force the struct to become non-aggregate, and make your code a guaranteed compile error:
struct A {
int x{};
};
struct S
{
explicit S() = delete;
const A a;
const float b;
};
int main()
{
auto s = S{}; // error: call to deleted constructor of 'S'
}
Because S is an aggregate, S{} will perform aggregate initialization. The rule in the standard about how members are initialized when there are no initializers in the list is basically what you cited:
If the element has a default member initializer ([class.mem]), the element is initialized from that initializer.
Otherwise, if the element is not a reference, the element is copy-initialized from an empty initializer list ([dcl.init.list]).
So for b, that's the equivalent of float b = {};. Per the rules of list initialization, we have to get all the way down to 3.10:
Otherwise, if the initializer list has no elements, the object is value-initialized.
And value initialization will initialize a float to 0.
struct A {
A() {}
A(const A&) = delete;
};
A foo() {
return {}; // Calls A()
}
struct B {
B(const B&) = delete;
};
B bar() {
return {}; // Aggregate initialization.
}
foo and bar both compile fine in C++11, because they use copy-list-initialization. There is no copy elision needed.
Where is it mentioned in the C++ standard that in such cases copy/move constructors are not needed?
I can see in [stmt.return] A return statement with a braced-init-list initializes the object or reference to be returned from the function by copy-list-initialization (8.5.4) from the specified initializer list.
I cannot find the section that mentions that in such cases copy/move constructors are not needed.
This is a C++11 (and 14, and 17) issue where aggregate-initialization is allowed to bypass the copy-constructor check.
B is an aggregate class (in C++11/14/17)
You can verify that in C++17 with the std::is_aggregate type trait
You are using list-initialization
This performs aggregate initialization, which is treated slightly differently than a regular constructor
Per #NicolBolas' nice answer regarding [stmt.return], return {} is like performing copy-list-initialization (aggregate initialzation) of the returned object directly (as opposed to constructing and then returning)
If you had written return B() instead, the compiler would have rejected this code.
The bypass is fixed in C++20 (The compiler will reject your code) per P1008 since B is no longer an aggregate type (C++20 says that a class with any user-declared constructors is not an aggregate).
Where is it mentioned in the C++ standard that in such cases copy/move constructors are not needed?
It isn't. There is no need to mention such a thing because copy-list-initialization is not specified to do any copying or moving.
The behavior of return {...}; is defined as follows:
A return statement with a braced-init-list initializes the object or reference to be returned from the function by copy-list-initialization
So this syntax invokes copy-list-initialization, with the braced-init-list being the initializer and the function's return value object being the object to be initialized. So it is functionally equivalent to T return_value_object = {...};.
[dcl.init.list]/3 explains the entire process of list-initialization, and nowhere does it say that the object to be initialized is copied or moved from some T (where T is the object type being initialized). No temporary object of type T is created or anything of the kind. The braced-init-list simply initializes the object.
So there is no need for a copy or move constructor on T unless the members of the braced-init-list would themselves require one (if you provided an object of type T as a member of the list, for example).
Note that this is not elision. Elision implies that a copy/move would have happened but was optimized out. List initialization doesn't have any copying or moving to optimize away to begin with.
here i am using two syntax to pass values to a constructor :-
class A
{
public:
int x,y;
A(int a,int b) : x(a),y(b){}
void show()
{
cout<<x<<" "<<y<<endl;
}
};
int main()
{
A obj1={5,6};//first method
A obj2(9,10);//second method
obj1.show();
obj2.show();
}
And both are working fine
but when i remove the constructor function itself then also the first method is working fine .
please explain this.
When you remove this constructor, class A becomes eligible for aggregate initialization, which is caused here by this curly braces syntax: A obj1={5,6}; (that also has this equivalent form: A obj1{5,6}; since C++11)
As you can read here, this applies in your case as class A then has none of the following:
private or protected non-static data members (applies until C++11)
user-declared constructors (applies since C++11 until C++17)
user-provided constructors (explicitly defaulted or deleted constructors are allowed) (applies since C++17 until C++20)
user-provided, inherited, or explicit constructors (explicitly defaulted or deleted constructors are allowed) (applies since C++20)
user-declared or inherited constructors
virtual, private, or protected (applies since C++17) base classes
virtual member functions
default member initializers (applies since C++11 until C++14)
In contrast, this syntax: A obj2(9,10); is performing direct-initialization, and it fails to compile once you remove the constructor due to [dcl.init¶17.6.2]:
[...] if the initialization is direct-initialization [...]
constructors are considered. [...] If no constructor applies [...] the initialization is
ill-formed.
Prior to removing the constructor, the same syntax A obj1={5,6}; was calling it, while performing copy-list-initialization.
What is aggregate initialization? It is the initialization of an instance of an aggregate type (either an array or a struct/class adhering to the above list) by the braced-init-list syntax. The arguments inside the braces must match the struct/class non-static data members in declaration order. It got significant boost in later versions of C++, as more syntactic forms for it were added to the language. Your usage of ={...} is the only one in existence from before C++11.
The general pros and cons of using parentheses forms of initialization vs. brace initialization is discussed by many. A good place to get a comprehensive readout is at Item 7 of Scott Meyers' Effective Modern C++. That said, the foremost advantage of all brace initialization forms is that they don't allow narrowing.
A obj1={5,6};
This is using list initialization for the object, which is perfectly valid even without your 2 argument constructor being defined.
This may help you understand: Why is list initialization (using curly braces) better than the alternatives?
So let's say I'm working with this toy example:
struct Foo {
int member;
};
I know that the default constructor won't default initialize member. So if I do this, member remains uninitialized: const Foo implicit_construction. As an aside this seems to work fine: const Foo value_initialization = Foo() though my understanding is that this isn't actually using the default constructor.
If I change Foo like this:
struct Foo {
Foo() = default;
int member;
};
And I try to do const Foo defaulted_construction, unsurprisingly it behaves exactly as implicit_construction did, with member being uninitialized.
Finally, if I change Foo to this:
struct Foo {
Foo(){};
int member;
};
And I do: const Foo defined_construction member is zero-initialized. I'm just trying to make sense of what the implicitly defined constructor looks like. I would have though it would have been Foo(){}. Is this not the case? Is there some other black magic at work that makes my defined constructor behave differently than the defaulted one?
Edit:
Perhaps I'm being mislead here. defaulted_construction is definitely uninitialized.
While defined_construction is definitely initialized.
I took this to be standardized behavior, is that incorrect?
What you're experiencing is called default initialization and the rules for it are (emphasis mine):
if T is a non-POD (until C++11) class type, the constructors are considered and subjected to overload resolution against the empty argument list. The constructor selected (which is one of the default constructors) is called to provide the initial value for the new object;
if T is an array type, every element of the array is default-initialized;
otherwise, nothing is done: the objects with automatic storage duration (and their subobjects) are initialized to indeterminate values.
Edit, in response to OP's request below:
Note that declaring a constructor = default does not change the situation (again, emphasis mine):
Implicitly-defined default constructor
If the implicitly-declared default constructor is not defined as deleted, it is defined (that is, a function body is generated and compiled) by the compiler if odr-used, and it has exactly the same effect as a user-defined constructor with empty body and empty initializer list. That is, it calls the default constructors of the bases and of the non-static members of this class.
Since the default constructor has an empty initializer list, its members satisfy the conditions for default initialization:
Default initialization is performed in three situations:
...
3) when a base class or a non-static data member is not mentioned in a constructor initializer list and that constructor is called.
Also note that you have to be careful when experimentally confirming this, because it's entirely possible that the default-initialized value of an int may be zero. In particular, you mentioned that this:
struct Foo {
Foo(){};
int member;
} foo;
Results in value-initialization, but it does not; here, member is default-initialized.
Edit 2:
Note the following distinction:
struct Foo {
int member;
};
Foo a; // is not value-initialized; value of `member` is undefined
Foo b = Foo(); // IS value-initialized; value of `member` is 0
This behavior can be understood by following the rules for value-initialization:
Value initialization is performed in these situations:
1,5) when a nameless temporary object is created with the initializer consisting of an empty pair of parentheses;
Form 1 (T();) is the form used on the right-hand side of the = above to initialize b.
The effects of value initialization are:
1) if T is a class type with no default constructor or with a user-provided or deleted default constructor, the object is default-initialized;
2) if T is a class type with a default constructor that is neither user-provided nor deleted (that is, it may be a class with an implicitly-defined or defaulted default constructor), the object is zero-initialized and then it is default-initialized if it has a non-trivial default constructor;
3) if T is an array type, each element of the array is value-initialized;
4) otherwise, the object is zero-initialized.
Finally though, note that in our earlier example:
struct Foo {
Foo(){}; // this satisfies condition (1) above
int member;
};
Foo f = Foo();
Now, condition (1) applies, and our (empty) user-declared constructor is called instead. Since this constructor does not initialize member, member is default-initialized (and its initial value is thus undefined).
I have a simple struct with an array:
struct A
{
uint32_t arr[size];
};
I have two functions, which create it using default initialization and value initialization:
template<class T>
void testDefault()
{
T* pa = new T; // Default
use(*pa);
delete pa;
}
template<class T>
void testValue()
{
T* pa = new T(); // Value
use(*pa);
delete pa;
}
I'm facing different performance for those functions. The funny thing is that performance differences vary depending on how I declare default constructor of the struct. I have three ways:
struct A
{
uint32_t arr[size];
// Implicit constructor
};
struct B
{
uint32_t arr[size];
B() {}; // Empty constructor
};
struct C
{
uint32_t arr[size];
C() = default; // Defaulted constructor
};
I thought they are all the same from compiler's point of view. Never have been I so wrong. I did run both testDefault() and testValue() several times with structs A, B and C and measured performance. Here is what I have:
Default initialization (implict constructor) done in 880ms
Value initialization (implict constructor) done in 1145ms
Default initialization (empty constructor) done in 867ms
Value initialization (empty constructor) done in 865ms
Default initialization (defaulted constructor) done in 872ms
Value initialization (defaulted constructor) done in 1148ms
Note how performance is clearly worse for both implicit and defaulted constructors. Only empty constructor correctly shows the same performance for both different initialization forms.
I tested this with VC++, gcc and clang. See online demo for gcc. Timings are quite persistent.
What I assume is:
Default and value initializations for UDT are the same thing
All demonstrated ways of defining default constructor are doing the same thing
Default constructor of these structs should leave content of the array in indeterminate state
Since all the compilers exhibit the same timings, it seems like I'm missing something. Can anyone please explain me these timings?
(See also my question Why compilers put zeros into arrays while they do not have to? on the same topic. I give some links to cppreference there.)
Let's look at the definition of value-initialize:
To value-initialize an object of type T means:
if T is a (possibly cv-qualified) class type with either no default constructor (12.1) or a default constructor that is user-provided or deleted, then the object is default-initialized;
if T is a (possibly cv-qualified) class type without a user-provided or deleted default constructor, then the object is zero-initialized [...];
if T is an array type, then each element is value-initialized;
otherwise, the object is zero-initialized.
Also let's review the adjectives in use here for constructors:
user-declared - you declared the constructor
user-provided - you declared the constructor and didn't set it to = default or = delete
default - can be called with no arguments
declared as defaulted - marked = default; or implicitly generated as such
Looking at your classes:
A has a default constructor which is implicitly declared as defaulted, and not user-provided.
C has a default constructor which is user-declared as defaulted, and not user-provided.
So the second bullet point in the definition of value-initialize applies. The object is zero-initialized, meaning the arr is zeroed out.
B has a default constructor which is user-provided.
So the first bullet point of value-initialize applies to B; value-initialization is the same as default-initialization here, and arr is not zeroed out.
Your timings correctly seem to correspond to what is expected: value-initialization of A or C zeroes out arr and the other cases don't.
One of the constructors behaves differently under value initialization. In this version,
B() {};
the array B::arr is not value-initialized when the B is. With the others, it is. Whether this explains the performance difference is another matter.
So, B::arr doesn't get zero-initialized with value initialization, whereas A::arr and C::arr do. All three cases have the same behaviour under default initialization, that is to say, arr gets default-initialized, i.e. no initialization is performed.