1

I have some class A, which I can either construct using nothing, or an std::function. On destruction that given function should be called (in case there is one). My problem is that the object gets destroyed right after it is created and returned by my getSomeA() function which calls the std::function before it is supposed to be called. The function passed to the constructor should only be called once. Some example code:

#include <iostream>
#include <functional>

static void testFunction(const std::string& msg)
{
    std::cout << "TestFunction: " << msg << "\n";
}

class A {
public:
    A(void) = default;

    A(const std::function<void()>& onDestroy) :
        onDestroy(onDestroy)
    {  }

    ~A(void)
    {
        if (onDestroy) onDestroy();
        else std::cout << "in dtor but no onDestroy was set\n";
    }

private:
    std::function<void()> onDestroy;
};

A getSomeA(void)
{
    return A(std::bind(testFunction, "the A that was created inside getSomeA"));
}

int main(void)
{
    A b1;
    std::cout << "After creating b1\n";
    b1 = getSomeA();
    std::cout << "After reassigning b1\n";

    std::cout << "Here the program works with b1...\n";
}

The program outputs

After creating b1
TestFunction: the A that was created inside getSomeA
After reassigning b1
Here the program works with b1...
TestFunction: the A that was created inside getSomeA

So the function is called before it is supposed to be called (at the end of int main()).

After adding a move constructor and assignment operator everything works as expected:

A(A&& other) :
        onDestroy(std::exchange(other.onDestroy, nullptr))
    {  }

    A& operator=(A&& other)
    {
        onDestroy = std::exchange(other.onDestroy, nullptr);
        return *this;
    }

And the program outputs

After creating b1
in dtor but no onDestroy was set
After reassigning b1
Here the program works with b1...
TestFunction: the A that was created inside getSomeA

The actual question: Where is the object destroyed for the first time so that testFunction is called? In getSomeA() or in the main function before the assignment but after the object was created in getSomeA()?

All the edits: I tried to bring down my question for one hour but since I have no idea about move/copy semantics in C++ that was pretty hard for me.

kessler bebe
  • 106
  • 5
  • Can you simplify this question a bit please? Make it easy for us to help. There's a lot to read at the moment. – Asteroids With Wings Oct 05 '20 at 22:31
  • Sure, I'll try. – kessler bebe Oct 05 '20 at 22:34
  • `A& operator=(A other)` is a copy-assignment operator, not a move-assignment operator. – Asteroids With Wings Oct 05 '20 at 22:35
  • Move semantics doesn't change an object's lifetime, only management of the object's inner resources. Regardless of its content, if an object is in dynamic memory then it is destroyed when `delete` is called on it, if an object is in automatic memory then it is destroyed when it goes out of scope. If an object is a temporary, it goes out of scope at the end of the statement that creates it. So, if you don't implement the [Rule of 3/5/0](https://en.cppreference.com/w/cpp/language/rule_of_three) correctly, then you will have bad things happen when copied-from/moved-from objects are destroyed. – Remy Lebeau Oct 05 '20 at 22:40
  • @AsteroidsWithWings actually, that operator can act as both a copy-assignment and a move-assignment operator, if `A` implements both a copy constructor and a move constructor – Remy Lebeau Oct 05 '20 at 22:41
  • @RemyLebeau http://eel.is/c++draft/class.copy.assign#1 http://eel.is/c++draft/class.copy.assign#3 ‍♂️ It may be able to act like a move-assignment operator (and it can't, really; all you can control is whether the argument is moved-into at the callsite), but it isn't one. – Asteroids With Wings Oct 05 '20 at 22:43
  • @RemyLebeau Oh. So I have to use the rule of 3 basically as soon as I have something to manage - which in my case are callbacks. The destructor which caused my new "A" object to be deleted was it going out of scope in the `getSomeA()`, right? Can I take it as guaranteed that the move semantics take care of temporary objects? Thank you for tha fast comment. – kessler bebe Oct 05 '20 at 22:46
  • @AsteroidsWithWings `A& operator=(A other)` may not be *defined* as a move operator, but it does act as a viable move operator, if a move constructor is implemented. `A a; a = other;` (copy) and `A a; a = std::move(other);` (move) both work as expected. – Remy Lebeau Oct 05 '20 at 22:56
  • @RemyLebeau Not sure what else to tell you. The standard clearly defines what is a copy assignment operator, and what is a move assignment operator. What the OP has shown meets the definition of the former, not the latter. ‍♂️ There's really no scope for disagreement here. – Asteroids With Wings Oct 05 '20 at 22:56
  • @kesslerbebe yes, exactly. `getSomeA()` returns a temporary, which you can copy/move from into `b1` if you have proper `operator=` implementation(s) in place. – Remy Lebeau Oct 05 '20 at 22:57
  • Of course you can make it so that it is invoked without a copy: that's always been the case. Do we now call every `A& operator=(const A& other)` a "move constructor"? No... – Asteroids With Wings Oct 05 '20 at 22:57
  • @AsteroidsWithWings whatever. Defined or not, it is not uncommon practice to implement both copy-assignment and move-assignment using a single `operator=`, and it works just fine, and is well-defined behavior using copy/move constructors. That is all I'm going to say. – Remy Lebeau Oct 05 '20 at 22:59
  • @RemyLebeau I never disputed any of that (and I would do it that way myself). I am merely correcting the terminology in the question. Not sure why you took issue with that... – Asteroids With Wings Oct 05 '20 at 22:59
  • @AsteroidsWithWings terminology refers to special function signatures *generated by compiler* If user defines overriding ones with equivalent signatures and behaviour, it doesn't change functions's purpose. I.e. user-defined copy constructor may take non-const parameter, it only limits or extends class's copyability (requires mutable argument, can mutate argument - transfer semantics) – Swift - Friday Pie Oct 05 '20 at 23:38
  • `A& operator=(A other)` would be ambiguous with `A& operator=(A&& other)` in context of move operation attempted, so it _is_ equivalent of move operator. – Swift - Friday Pie Oct 05 '20 at 23:55
  • @Swift-FridayPie Irrelevant. This is not a move assignment operator. That is just a plain, simple, provable fact. That you can `std::move` into the argument for a copy assignment operator is entirely besides the point. – Asteroids With Wings Oct 06 '20 at 12:36
  • @AsteroidWithWings then what it is if it overloads compiler-defined move assignment and is not a copy assignment by definition (because argument is not T&, const T& or volatile T&)? This appears to be a defect of standard overlooking this possibility – Swift - Friday Pie Oct 06 '20 at 12:52

1 Answers1

1

In the first version the object gets destroyed for the first time as part of returning the object constructed in getSomeA. Its return statement effectively constructs a temporary object, which then gets assigned to main's b1. If you ignore the process of returning from a function, the sequence of events is:

A <temporary object>(std::bind( ... )

b1=<temporary object>

<temporary object gets destroyed>

At this point the bound function gets called, as a result of the temporary object gets destroyed. The temporary object is a fully tricked out object, with all the rights and privileged granted thereof. Including a destructor.

But wait, there's more! b1 is a perfect bit-by-bit copy of the original object, with the bound function, before it got destroyed. So when b1 gets destroyed, the function gets called again.

This is the indirect consequence of the Rule Of Three. When an object owns a resource, and must maintain an exclusive ownership of the resource you will need to provide a copy and/or move constructor and and assignment operator to spell out exactly what should happen in that situation.

P.S. these rules change slightly with C++17's guaranteed copy elision, but the underlying concept is still the same.

Sam Varshavchik
  • 114,536
  • 5
  • 94
  • 148