Search code examples
c++movec++17move-semantics

C++ Destructor thru reference


I want to share with you a tiny problem that I'm not getting to work out, here is the code (it's for test only):

#include <windows.h>
#include <iostream>
#include <vector>
#include <string>
#include <utility>
#include <type_traits>
#include <sstream>

struct Procedure {
    Procedure(HANDLE)
    { std::cout << "ctor w/connection: " << this << std::endl; }

    ~Procedure()
    { std::cout << "dtor: " << this << std::endl; }

    Procedure(Procedure &&rhs) {
        std::cout << "ctor w/move: " << this << std::endl;
        this->m_Params = std::move(rhs.m_Params);
    }

    Procedure& operator= (Procedure &&rhs) {
        std::cout << "operator= w/move: " << this << std::endl;
        if (this != &rhs) this->m_Params = std::move(rhs.m_Params);
        return *this;
    }

    Procedure& AppendParam(const std::string &str) {
        std::cout << "appendparam: " << this << std::endl;
        m_Params.push_back(str);
        return *this;
    }

    void Execute( const std::string &str) {
        std::stringstream ss;
        ss << str << '(';
        for (int i = 0, mx = m_Params.size(); i < mx; ++i) {
            ss << '\'' << m_Params[i] << '\'';
            if (i < mx - 1) ss << ',';
        }
        ss << ");";
        std::cout << "calling: " << this << " : " << ss.str() << std::endl;
    }


private:
    Procedure(const Procedure &) = delete;
    Procedure& operator=(const Procedure &) = delete;
    std::vector<std::string> m_Params;
};

Procedure ProcedureCaller()
{ return Procedure(nullptr); }


int __cdecl main() {
    std::cout << "test1---------------------" << std::endl; {
        auto &proc = ProcedureCaller().AppendParam("param_1").AppendParam("param_2");
        proc.Execute("sp_test");
    }

    std::cout << "test2--------------------" << std::endl; {
        auto proc = ProcedureCaller();
        proc.AppendParam("param_A").AppendParam("param_B");
        proc.Execute("sp_test_2");
    }

    std::cout << "test3--------------------" << std::endl; {
        ProcedureCaller().AppendParam("param_AA").AppendParam("param_BB").Execute("sp_test_2");
    }
    return 0;
}

And here is the result I'm getting:

test1---------------------
ctor w/connection: 00F8FC98
appendparam: 00F8FC98
appendparam: 00F8FC98
dtor: 00F8FC98
calling: 00F8FC98 : sp_test();
test2--------------------
ctor w/connection: 00F8FD70
appendparam: 00F8FD70
appendparam: 00F8FD70
calling: 00F8FD70 : sp_test_2('param_A','param_B');
dtor: 00F8FD70
test3--------------------
ctor w/connection: 004FFB20
appendparam: 004FFB20
appendparam: 004FFB20
calling: 004FFB20 : sp_test_2('param_AA','param_BB');
dtor: 004FFB20

I have a few questions:
1- Why dtor of "test1" is getting called before the end of its scope? I mean, the code hasn't even called the Execute method.
2- If dtor of "test1" is a temporal object, why I'm not seeing a log from the move ctor, or at least a compiler error because it's trying to use the deleted copy ctor?
3- What's the difference between "test1" and "test2", I want to be able to call the Execute whatever way I want.
4- What am I missing?
Thanks.


Solution

  • Here's a simpler version demonstrating the same problem:

    struct X {
        X() = default;
        ~X() { std::cout << "dtor\n"; }
        X& self() { return *this; }
    };
    
    int main()
    {
        X& x = X().self();
        std::cout << "here?\n";
    }
    

    This program prints dtor before it prints here. Why? The problem is, we have a temporary (X()) that does not get lifetime extended, so it gets destroyed at the end of the expression that contains it (which is X().self()). While you get a reference to it, it's not one of the magic references that does lifetime extension - what you get is just a reference to an object that's immediately going out of scope.

    Lifetime extension only happens under very limited circumstances. The temporary has to be bound immediately to a reference, which can only happen for const references:

    X const& x = X();
    std::cout << "here\n";
    

    Now this prints here before dtor.

    Additionally, there is no transitive lifetime extension. Even if in the original example we did:

    X const& x = X().self();
    

    We'd still get a dangling reference.