When are local variables in a function scope destroyed? [duplicate] - c++

This question already has answers here:
Does returning a local variable return a copy and destroy the original(nrvo)?
(1 answer)
What is meaning of destroying local variable in function
(7 answers)
How is destroying local variables when a block is exited normally called in C++?
(6 answers)
Closed 7 months ago.
After a function is called, when the local (non-static) objects will be destroyed has been vague to me, especially after C++17 where prvalue is redefined. So I decide to ask this question.
(In C++14 and earlier)
Assume there is no optimization, consider the following toy code:
class Y
{
public:
~Y() { cout << "quitting f()\n"; }
};
class X
{
public:
X& operator=(const X& x)
{
cout << "assignment\n";
return *this;
}
};
X f()
{
X x;
Y y;
return x;
}
int main()
{
X x;
x = f();
}
The outputs are as follows:
quitting f()
assignment
Question 1:
In my current understanding, the local variables of f() are destroyed immediately after the returned temporary is created. May I ask if this is true?
(In C++17 and newer)
Consider the following toy code:
class Z { };
class Ya
{
public:
~Ya() { cout << "quitting f1()\n"; }
};
class Yb
{
public:
~Yb() { cout << "quitting f2()\n"; }
};
class X
{
public:
X() {}
X(Z z) { cout << "constructing X\n"; }
X& operator=(const X& x)
{
cout << "assignment\n";
return *this;
}
};
X f1()
{
Z z;
Ya y;
return X(z);
}
X f2()
{
Yb y;
return f1();
}
int main()
{
X x;
x = f2();
}
The outputs are as follows:
constructing X
quitting f1()
quitting f2()
assignment
In my current understanding, X(z) is a prvalue which is used to initialize the prvalue represented by f1(), which is then used to initialize the prvalue represented by f2(). The prvalue represented by f2() is then materialized into a temporary which is then used in the copy assignment.
If my understanding to question 1 is correct, I would guess that the local variables z and y are destroyed immediately after the initialization of the prvalue represented by f1(). Assume this is true, there is a problem: before the prvalue represented by f2() is materialized, there is NO object constructed from X(z) exists, so how could the materialized temporary be created from X(z) at the point when z is already destroyed?
Question 2:
As a result, my guess is that the local variables of f1() are destroyed after the prvalue represented by f2() is materialized (or if the prvalue is used to initialize a variable, had we written X x = f2(); instead). May I ask if this is true?

Your question can be answered with a slightly more detailed example. Here is code that traces when every object is constructed and destroyed:
#include <algorithm>
#include <iostream>
#include <typeinfo>
#include <type_traits>
// Compile-time string suitable as a non-type template argument.
template<size_t N>
struct fixed_string {
char value[N];
consteval fixed_string(const char (&str)[N]) { std::copy_n(str, N, value); }
};
template<fixed_string Name>
class tracer {
std::ostream &out() const { return std::cout << Name.value << " "; }
public:
tracer() { out() << "default constructed\n"; }
tracer(const tracer &) { out() << "copy constructed\n"; }
tracer(tracer &&) { out() << "move constructed\n"; }
template<typename T> tracer(T t) {
out() << "template constructed [" << typeid(T).name() << "]\n";
}
~tracer() { out() << "destroyed\n"; }
tracer &operator=(const tracer &) {
out() << "copy assigned\n"; return *this;
}
tracer &operator=(tracer &&) { out() << "move assigned\n"; return *this; }
};
tracer<"X">
f1()
{
tracer<"Z"> z;
tracer<"==== f1 temp"> y;
return {z};
}
tracer<"X">
f2()
{
tracer<"==== f2 temp"> y;
return f1();
}
int
main()
{
tracer<"X"> x = f2();
}
The output (with C++20) is as follows:
==== f2 temp default constructed
Z default constructed
==== f1 temp default constructed
Z copy constructed
X template constructed [6tracerIXtl12fixed_stringILm2EEtlA2_cLc90EEEEE]
Z destroyed
==== f1 temp destroyed
Z destroyed
==== f2 temp destroyed
X destroyed
So you can see that only one X is ever created, but that X is created before the Z it is constructed from is destroyed. How does that happen? Well, in the implementation you can think of the location in which to construct the X being passed as a kind of implicit argument to the functions that return a prvalue, so that the X is constructed inside f2, but it is located in main's stack.
Your example is slightly more complicated, but the same phenomenon is happening:
int
main()
{
tracer<"X"> x;
x = f2();
}
Produces the following output:
X default constructed
==== f2 temp default constructed
Z default constructed
==== f1 temp default constructed
Z copy constructed
X template constructed [6tracerIXtl12fixed_stringILm2EEtlA2_cLc90EEEEE]
Z destroyed
==== f1 temp destroyed
Z destroyed
==== f2 temp destroyed
X move assigned
X destroyed
X destroyed
So now obviously we have two X's, the one called x, and the temporary one that is materialized to pass into x's move assignment operator. However, the temporary X is created on main's stack, and its lifetime is the full expression, so if there were more code in main, the temporary X would be destroyed at the semicolon after the assignment, rather than at main's closing brace.

Related

Overwriting object with new object of same type and using closure using this

In the following code an object is overwritten with a new object of same type, where a lambda-expression creates a closure that uses this of the old object. The old address (this) remains the same, the new object has the same layout, so this should be ok and not UB. But what about non trivial objects or other cases?
struct A {
void g(A& o, int v) {
o = A{.x = v, .f = [this]{
std::cout << "f" << this->x << '\n';
}};
}
int x{0};
std::function<void()> f;
~A() {
std::cout << "dtor" << x << '\n';
}
};
void test() {
A a;
a.g(a, 2);
a.f();
}
You are not actually replacing any object. You are just assigning from another object to the current one. o = simply calls the implicit copy assignment operator which will copy-assign the individual members from the temporary A constructed in the assignment expression with A{...}.
The lambda is going to capture this from this in g, not from the temporary object.
std::function will always keep a copy of the lambda referring to the original object on which g was called and since that is its parent object, it cannot outlive it.
So there is no problem here. The only exception would be that you call f during the destruction of the A object, in which case using the captured pointer may be forbidden.
Here is a slightly modified code with a corner case. I create a temporary in a function and call g on it passing it a more permanent object. The temporary vanishes and the long life object now has a closure refering to an object after its end of life. Invoking f is UB:
#include <iostream>
#include <functional>
struct A {
void g(A& o, int v) {
o = A{ .x = v, .f = [this] {
std::cout << "f" << this->x << ' ' << this << '\n';
} };
}
int x{ 0 };
std::function<void()> f;
~A() {
std::cout << "dtor" << x << ' ' << this << '\n';
}
};
void test(A& a) {
A b{ 2 };
b.g(a, 3);
}
int main() {
A a{ 1 };
std::cout << a.x << '\n';
test(a);
std::cout << a.x << '\n';
a.f(); // UB because a.f uses an object after its end of life
}
The output is:
1
dtor3 0135F9C0
dtor2 0135FA30
3
f341072 0135FA30
dtor3 0135FAA8
proving that the invocation of a.f() tried to use the object at address 0135FA30 (in that specific run) after it has been destroyed.

Copy elision with tuples

I always had the wrong impression that if I create temporaries in a function that returns a tuple, and use std::forward_as_tuple in the return statement, then there is no copy, just like automatic copy elision for non-tuple return types.
How then can we avoid a copy? For example, in the following code, func1 “copies” the values 1 and 2 into temp as it is being constructed, and there is no additional copy upon returning. func2 on the other hand does the same construction, but additionally copies when a tuple is created in the return statement.
I tried to be witty about it, and declare the temporary to be returned a tuple and construct a My_struct in place, like this: std::tuple<My_struct> temp {{1,2}}. But the copy constructor is still called. Adding a move constructor won't help as far as I can see since the data members are simple types.
#include <iostream>
#include <tuple>
struct My_struct {
int x, y;
My_struct(int x, int y) :
x(x), y(y)
{
std::cout << "Constructor called\n";
}
My_struct(const My_struct &a) :
x(a.x), y(a.y)
{
std::cout << "Copy constructor called!\n";
}
};
My_struct func1()
{
My_struct temp {1,2};
return temp;
}
std::tuple<My_struct> func2()
{
My_struct temp {1,2};
return std::forward_as_tuple(temp);
}
int main()
{
std::cout << "# Calling func1\n";
auto result1 = func1();
std::cout << "# Calling func2\n";
auto result2 = func2();
}
result:
# Calling func1
Constructor called
# Calling func2
Constructor called
Copy constructor called!

The life range of a temporary object created at callsite arguments

I have the following code, was wondering when Foo's destructor is called.
#include <iostream>
class Foo {
public:
Foo() {
}
~Foo() {
std::cout << "destruct" << std::endl;
}
};
void go(Foo f) {
std::cout << "go" << std::endl;
}
int main() {
go(Foo());
std::cout << "main" << std::endl;
return 0;
}
If I run the code, I got the following output
go
destruct
main
It shows Foo's destructor is called after go is done. My gcc is 4.8.3.
I had thought the temporary Foo's object should be deleted after it is copied to go's argument. But this is not the case, and only one object of Foo exists. Is this expected or undefined in terms of compiler's implementation?
It's an optimization permitted by the C++ Standard.
The C++ standard draft, [class.temp/2] says and I quote (relevant parts only; emphasis are mine):
The materialization of a temporary object is generally delayed as long
as possible in order to avoid creating unnecessary temporary objects.
.....
Example:
class X {
public:
X(int);
X(const X&);
X& operator=(const X&);
~X();
};
class Y {
public:
Y(int);
Y(Y&&);
~Y();
};
X f(X);
Y g(Y);
void h() {
X a(1);
X b = f(X(2));
Y c = g(Y(3));
a = f(a);
}
X(2) is constructed in the space used to hold f()'s argument and
Y(3) is constructed in the space used to hold g()'s argument.
Formerly, in n3690, it said:
An implementation might use a temporary in which to construct X(2)
before passing it to f() using X’s copy constructor;
alternatively, X(2) might be constructed in the space used to hold
the argument
That means, this:
void go(Foo) {
std::cout << "go" << std::endl;
}
int main() {
go(Foo());
}
is sometimes as "performant" as you want!, See, C++ is gradually getting there ;-).
But you see, using std::move will inhibit that behavior, because std::move produces an xvalue expression from a materialized object:
void go(Foo) {
std::cout << "go" << std::endl;
}
int main() {
go(std::move(Foo()));
}
In conclusion,
When not using std::move in this your case, the object is created once as seen Live on Coliru
But when you use std::move, it is created twice as seen Live on Coliru, this is because of materialization of the object. Read the complete paragraph of class.temp/2 to understand what materialization means.

'this' pointer behaviour in c++

I have a basic understanding of this pointer in C++.While studying have come across the following code:
#include<iostream>
using namespace std;
class Test
{
private:
int x;
int y;
public:
//Test (int x = 0, int y = 0) { this->x = x; this->y = y; }
Test setX(int a) { x = a; return *this; }
Test setY(int b) { y = b; return *this; }
void print() { cout << "x = " << x << " y = " << y << endl; }
};
int main()
{
Test obj1;
obj1.setX(10).setY(20);
obj1.print();
return 0;
}
My issue is why the compiler deosn't report an error where I am returning a this pointer in SetX and SetY functions but haven't stated the return type as a pointer?
This comes because you're returning *this not this.
this is the pointer to a object of type Test. This means the this-variable basically holds the address where the object is stored. To access the object on which this points you use the *.
So you're returning the actual object on which you this Pointer points at.
EDIT
The problem why your code does not work in the way you want it to do is caused by the fact, that you're working on the stack.
Let's take a look at the addresses:
#include<iostream>
using namespace std;
class Test
{
private:
int x;
int y;
public:
//Test (int x = 0, int y = 0) { this->x = x; this->y = y; }
Test setX(int a) { x = a; return *this; }
Test setY(int b) {
y = b;
cout << this << endl; // >> 0x29ff18
return *this;
}
void print() { cout << "x = " << x << " y = " << y << endl; }
};
int main()
{
Test obj1;
cout << &obj1 << endl; // >> 0x29ff10
obj1 = obj1.setX(10).setY(20);
cout << &obj1 << endl; // >> 0x29ff10
//obj1.setY(20);
obj1.print();
return 0;
}
As you can see, the object where this points at is at a different address within you setY method compared to the main. This is because the object is copied to the stackframe of the setY method - so within setX and setY you're working with a copy of obj1
If you're doing obj1.setX(10).setY(20); you basically copy the object obj1 and use it within setX and the return object of setX is then used in setY. If you want to save the last copy, you have to reassign it to obj1.
Your solution to the problem works, but is grossly inefficient. The last paragraph describing what is happening is incorrect. setx is called with and uses obj1. sety is called with and uses copy of obj1. obj1 is then assigned copy of copy of obj1 returned by sety. The address doesn't change because obj1's storage is being overwritten, not replaced. Add a copy constructor and an assignment operator and you can watch what's really happening. The recommended solution is to use references to the same object throughout and chaining as per #πάνταῥεῖ 's answer below. – user4581301
My issue is why the compiler deosn't report an error where I am returning a this pointer in SetX and SetY functions but haven't stated the return type as a pointer?
It's completely valid syntax, so the compiler isn't supposed to return an error message. The problem is that you're using copies of this* with your return type.
To chain operations properly to operate on the original instance return a reference to this:
Test& setX(int a) { x = a; return *this; }
// ^
Test& setY(int b) { y = b; return *this; }
// ^
Otherwise you're returning an unrelated copy of your class.

What goes on in the background when we assign Object1 = Object2 of the same class in C++?

Suppose that both the objects (of the same class) have been initialized already.
Now, you do:
Object2 = Object1;
In Java, what happens is that both Object1 and Object2 now point to the same memory location.
What happens in C++?
#include <iostream>
using namespace std;
class X {
public:
X() {
cout << "Default Constructor called\n";
i = 0;
}
X(int i) {
cout << "Parameterized Constructor called\n";
this->i = i;
}
X (const X& x) {
cout << "Copy Constructor called\n";
i = x.getI();
}
~X() {
cout << "Destructor called\n";
}
int getI() const {
return i;
}
private:
int i;
};
void main() {
cout << "\nLine-1\n\n";
X x1(1); // On Stack
cout << "\nLine-2\n\n";
X* x2 = new X(2); // On Heap
cout << "\nLine-3\n\n";
X x3(x1);
cout << "\nLine-4\n\n";
X* x4 = new X(x1);
cout << "\nLine-5\n\n";
X x5 = x1;
cout << "\nLine-6\n\n";
x5 = x3;
cout << "\nLine-7\n\n";
X x6 = *x2;
cout << "\nLine-8\n\n";
*x2 = *x4;
cout << "\nLine-9\n\n";
*x4 = x3;
cout << "\nLine-10\n\n";
}
As you can see, whenever I do createdObj1 = createdObj2, none of the constructors is invoked.
The function operator=() defines what happens. It can be defined as a member function, typically:
Object & Object::operator=(const Object &other);
If you do not provide one of these functions, a default implementation will be provided, which uses the operator=() function for each member variable.
Two major cases exist:
When performing an declaration with assignment, as in
X x = y;
it is usually (see the comment #T.C. made about explicit copy constructors) equivalent to
X x(y);
and thus will simply construct your new object in place. No assignment is done at all. This obviously creates a new object x which has automatic storage duration. (If I had written (static X x = y; it would have had static storage duration instead.)
The other case is
x = y;
where x is modified to equal y. This is done by calling X::operator=(X), whose default implementation will simply assign each member in turn. (A custom version can do whatever it wants.)