2

Consider the following code:

class c
{
    large_type_t i;
public:
    // the most simple variant
    c( large_type_t i ): i( i ) { }
    // move the parameter copied
    c( large_type_t i ): i( std::move( i ) ) { }
    // copy is done directly in interaction with our class member from original
    c( const large_type_t &i ): i( i ) { }
    // parameter was constructed in the call, just treat as rvalue
    c( large_type_t &&i ): i( std::move( i ) ) { }
    // this is here just to show all possible combinations in case someone in the future sees this question and someone answering it wants to explain everything
    c( large_type_t &&i ): i( i ) { }
};

What is the best way to do this? Are these all going to boil down to the same code anyway and it doesn't matter? I feel that I am fundamentally not understanding the purpose of move.

Undo
  • 25,519
  • 37
  • 106
  • 129
nowi
  • 437
  • 4
  • 13
  • 1
    Move doesn't matter for a primitive type like `int`. Instead imagine that you had something like a potentially large `vector`. `std::move` allows you to move it to a different variable/location by just transferring ownership of the heap allocation, without copying the underlying data. – jtbandes Feb 22 '20 at 04:11
  • Your #1 and #3 would both incur a copy. See also https://stackoverflow.com/questions/37935393/pass-by-value-vs-pass-by-rvalue-reference – jtbandes Feb 22 '20 at 04:15
  • Well, 2 (and 1 for that matter) could be called with `c(std::move(x))` which would prevent copying the data inside x. – jtbandes Feb 22 '20 at 04:31

3 Answers3

2

What is the best way to do this?

The simplest variant. The reason: Because it is the simplest variant.

Trivial types do not have copy constructors nor move constructors, and "moving" them is same as copying.

Are these all going to boil down to the same code anyway and it doesn't matter?

If optimisation and in particular inline expansion is involved, then probably yes. You can verify whether that is the case for your program by comparing the resulting assembly. Assuming no (or poor) optimisation, the simplest variant is potentially fastest.

Regarding your edit: For non-trivial types, the second variant is best because it moves from rvalue arguments instead of copying.

eerorika
  • 232,697
  • 12
  • 197
  • 326
2

Move constructors (or move assignments) provide the means for your program to avoid spending excessive time copying the contents of one object to another, if the copied-from object will no longer be used afterwards. For example, when an object is to be the returned value of a method or function.

This is more obvious when you have an object with, say, dynamically allocated content.

Example:

class MyClass {
private:
    size_t _itemCount ;
    double * _bigBufferOfDoubles ;
public:

    // ... Initial contructor, creating a big list of doubles
    explicit MyClass( size_t itemCount ) {
        _itemCount = itemCount ;
        _bigBufferOfDoubles = new double[ itemCount ]; 
    }

    // ... Copy constructor, to be used when the 'other' object must persist 
    //     beyond this call
    MyClass( const MyClass & other ) {

        //. ... This is a complete copy, and it takes a lot of time
        _itemCount = other._itemCount ;
        _bigBufferOfDoubles = new double[ _itemCount ]; 
        for ( int i = 0; i < itemCount; ++i ) {
            _bigBufferOfDoubles[ i ] = other. _bigBufferOfDoubles[ i ] ;
        }

    }

    // ... Move constructor, when the 'other' can be discarded (i.e. when it's 
    //     a temp instance, like a return value in some other method call)
    MyClass( MyClass && other ) {

        // ... Blazingly fast, as we're just copying over the pointer
        _itemCount = other._itemCount ;
        _bigBufferOfDoubles = other._bigBufferOfDoubles ;

        // ... Good practice to clear the 'other' as it won't be needed 
        //     anymore
        other._itemCount = 0 ;
        other._bigBufferOfDoubles = null ; 
    }
    ~MyClass() {
        delete [] _bigBufferOfDoubles ;
    }
};

// ... Move semantics are useful to return an object 'by value'
//     Since the returned object is temporary in the function,
//     the compiler will invoke the move constructor of MyClass
MyClass someFunctionThatReturnsByValue() {
    MyClass myClass( 1000000 ) ; // a really big buffer...
    return myClass ; // this will call the move contructor, NOT the copy constructor
}

// ... So far everything is explicitly handled without an explicit 
//     call to std::move
void someOtherFunction() {
    // ... You can explicitly force the use of move semantics
    MyClass myTempClass( 1000000 ) ;

    // ... Say I want to make a copy, but I don't want to invoke the
    //     copy contructor:
    MyClass myOtherClass( 1 ) ;
    myOtherClass = std::move( myTempClass ) ;

   // ... At this point, I should abstain from using myTempClass
   //     as its contents have been 'transferred' over to myOtherClass.

}
El Stepherino
  • 600
  • 6
  • 18
2

All of those are different and it very much depends on what large_type_t i; is.

Does it manage dynamically allocated data? If yes and it supports both moving and copy construction then following two constuctors should be used:

 c( const large_type_t &i ): mi( i ) { };
 c( large_type_t &&i ): mi( std::move( i ) ) {};

Although, it is potentially a mildly less efficient way, but you can use a single constructor for both purposes:

 c( large_type_t i): mi( std::move( i ) ) { };

For the above, without std::move it will likely result in unnecessary copying and memory allocations.

If it doesn't manage any dynamically allocated data - then usually moving/copying is the same thing (for exotic types figure it yourself since it's too dependent on circumstances). Thus you only need copy constructor:

 c( const large_type_t &i ): mi( i ) { };

For small trivial types, you'd better use the most basic form:

c( small_type_t i ): mi( i ) { };
ALX23z
  • 4,456
  • 1
  • 11
  • 18