Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

std::move calls the destructor unexpectedly

I have been trying to write a class that cannot be copied but can be moved, and that cannot be created except with named constructors. I achieved my goal with namedConstructor3 below. However, I do not understand why namedConstructor2 failed.

struct  A
{
    int a;

    //static A && namedConstructor1( int a_A )
    //{
    //  A d_A( a_A );
    //  return d_A;     // cannot convert from A to A&&
    //}

    static A && namedConstructor2( int a_A )
    {
        wcout << L"Named constructor 2\n";
        A d_A( a_A );
        return move( d_A );
    }

    static A namedConstructor3( int a_A )
    {
        wcout << L"Named constructor 3\n";
        A d_A( a_A );
        return move( d_A );
    }

    A( A && a_RHS ) : a( a_RHS.a )
    {
        a_RHS.a = 0;
        wcout << L"\tMoved: a = " << a << endl;
    }

    ~A()
    {
        wcout << L"\tObliterated: a = " << a << endl;
        a = -a;
    }

    A( const A & ) = delete;
    A & operator =( const A & ) = delete;

protected:
    A( int a_A = 0 ) : a( a_A )
    {
        wcout << L"\tCreated: a = " << a << endl;
    }
};

int main()
{
    A d_A2 = A::namedConstructor2( 2 );
    A d_A3 = A::namedConstructor3( 3 );
    wcout << "Going out of scope\n";
    return 0;
}

The output is

Named constructor 2
        Created: a = 2
        Obliterated: a = 2
        Moved: a = -2
Named constructor 3
        Created: a = 3
        Moved: a = 3
        Obliterated: a = 0
Going out of scope
        Obliterated: a = 3
        Obliterated: a = -2

Questions:

  1. Why is the destructor called before the move constructor in namedConstructor2 as evidenced by the 3rd and 4th lines of the output?

  2. Why is the destroyed data still available to the moved constructor (as evidenced by the 4th line of the output)?

  3. Isn't namedConstructor2 "more natural" than namedConstructor3 in the sense that the signature of std::move leads me to think that the return value of std::move has "two &&"?

​​​​

template< class T >
typename std::remove_reference<T>::type&& move( T&& t )

Compiled with VS2013u4.


Edit

Deduplicator's answer satisfies me. This edit is for completeness sake. The answer and the comments suggested that namedConstructor3 was "sub-optimal". I added

static A namedConstructor4( int a_A )
{
    wcout << L"Named constructor 4\n";
    A d_A( a_A );
    return d_A;
}

to the class and A d_A4 = A::namedConstructor4( 4 ); to the main function. The new output (when compiled in release mode, not in debug mode) shows that the optimal case does not even move the object:

Named constructor 2
        Created: a = 2
        Obliterated: a = 2
        Moved: a = -2
Named constructor 3
        Created: a = 3
        Moved: a = 3
        Obliterated: a = 0
Named constructor 4
        Created: a = 4
Going out of scope
        Obliterated: a = 4
        Obliterated: a = 3
        Obliterated: a = -2
like image 901
Hector Avatar asked Oct 19 '22 21:10

Hector


1 Answers

std::move(lvalue or xvalue) does not call a destructor.

It just changes the passed reference to an rvalue-reference, so move-semantics apply.

So, why is your local destroyed too early?
Simple, returning a reference to a local variable is UB:
Can a local variable's memory be accessed outside its scope?

Going over your named constructors one-by-one:

  1. namedConstructor1 returns an rvalue-reference.
    But you try to return a local (which is an lvalue), and the compiler complains about that error.
  2. namedConstructor2 is in principle the same as namedConstructor1, but you added an explicit cast (in the form of std::move) from lvalue to rvalue-reference, which shuts the compiler up.
    Thus, you get UB for lying to the compiler, specifically the locals lifetime ends at the end of the function, before the returned rvalue-reference is used.
  3. namedConstructor3 is ok, though sub-optimal.
    You are using the rvalue-reference to initialize the return-value (which is not a reference).
    The sub-optimal part is due to that std::move disabling return-value-optimization, which would obviate the need for the implementation to actually move (or copy) in this case.
like image 138
Deduplicator Avatar answered Nov 03 '22 23:11

Deduplicator