How unused default member initializer can change program behavior in C++?
Asked Answered
B

1

7

Please consider this short code example:

#include <iostream>

struct A
{
    A() { std::cout << "A() "; }
    ~A() { std::cout << "~A() "; }
};

struct B { const A &a; };

struct C { const A &a = {}; };

int main()
{
    B b({});
    std::cout << ". ";

    C c({});
    std::cout << ". ";
}

GCC prints here ( https://gcc.godbolt.org/z/czWrq8G5j )

A() ~A() . A() . ~A() 

meaning that the lifetime of A-object initializing reference in b is short, but in c the lifetime is prolonged till the end of the scope.

The only difference between structs B and C is in default member initializer, which is unused in main(), still the behavior is distinct. Could you please explain why?

Blanco answered 13/7, 2021 at 9:51 Comment(6)
If it actually prints it then it is a bug in compiler.Spot
Another vote for a GCC bug, the code doesn't even compile in MSVC or clangAntipode
@AlanBirtles: Compiles with MSVC Demo. and it is a C++20 features (aggregate initialization with ()), that clang doesn't support yet, see compiler_support (Parenthesized initialization of aggregates).Sochor
@Jarod42, should the C++20 tag be added to the question?Dogwatch
Yes, good pointBlanco
But seems a bug anyway as aggregate-initialization with {} gives different result.Sochor
B
1

C c(...); is syntax for direct initialisation. Overload resolution would find a match from the constructors of C: The move constructor can be called by temporary materialisation of a C from {}. {} is value initialisation which will use the default member initialiser. Thus, the default member initialiser isn't unused. Since C++17, the move constructor isn't necessary and {} initialises variable c directly; In this case c.a is bound directly to the temporary A and the lifetime is extended until destruction of C.

B isn't default constructible, so the overload resolution won't find a match. Instead, aggregate initialisation is used since C++20 - prior to that it would be ill-formed. The design of the C++20 feature is to not change behaviour of previously valid programs, so aggregate initialisation has lower priority than the move constructor.

Unlike in the case of C, the lifetime of the temporary A isn't extended because parenthesised initialisation list is an exceptional case. It would be extended if you used curly braces:

B b{{}};
Bodycheck answered 13/7, 2021 at 10:20 Comment(4)
but B{{}} doesn't behave as B({}) DemoSochor
Looks like correct explanation. Based on it, I eliminated C-object default initialization and the result is consistent now: gcc.godbolt.org/z/3GxE3qqh5 ThanksBlanco
The () form does not lifetime-extend temporaries. See eel.is/c++draft/class.temporary#6.10Intramural
@Intramural Thanks. Then the behaviour of the compiler is correct.Bodycheck

© 2022 - 2024 — McMap. All rights reserved.