简体   繁体   中英

Double move when returned by value C++

Hello for the sake of learning c++ i am building my own string class, and have a question regarding returning by value.

MTX::String MTX::String::operator+(String& sObject)
{

    //Calculate the size of a buffer we will need
    int _cBufferSizeTmp = (_cBufferSize - 1) + sObject._cBufferSize;

    //Now create a buffer which can hold both of the objects
    char* _cBufferTmp = new char[_cBufferSizeTmp];

    //Now copy first string in to it, but without the terminator
    Memory::Copy((void*)_cBuffer, _cBufferTmp, _cBufferSize - 1);

    //Copy second string but with null terminator
    //Now that takes into account the offset of a second string to copy (void *)(_cBufferTmp + (_cBufferSize - 1))
    Memory::Copy((void*)sObject._cBuffer, (void *)(_cBufferTmp + (_cBufferSize - 1)), sObject._cBufferSize);

    //And now we construct our tmp string with it
    String _tmpString;
    _tmpString._cBuffer = _cBufferTmp;
    _tmpString._cBufferSize = _cBufferSizeTmp;

    return _tmpString;
}

The question is, when the value is returned it is moved in to temporary object via move constructor, and then this temporary object is again moved to another object according tot his scheme

int main() {
    
    MTX::String Greetings;

    MTX::String tst1 = "Hello World";
    MTX::String tst2 = "! And good bye\n";
    
    Greetings = tst1 + tst2;
    std::cout << Greetings.sBuffer();  //Didnt implement ostream stuff yet

    return 0;
}

So here is console output

Created an empty string object
Created a string object "Hello World"
Created a string object "! And good bye
"
Created an empty string object
Created (Move) a string object "Hello World! And good bye //Here it creates a new tmp object and moves previous tmp in to it
"
deleting a string object
moved a string object via =
deleting a string object
Hello World! And good bye
deleting a string object
deleting a string object
deleting a string object

Why does it first move it into another tmp object before actually assigning it to the Greetings object.

Here is the full source code of string.cpp

#include "String.h"
#include "Memory.h"

//Work better on utils
int MTX::String::Length(const char *cBuffer) {
    int count = 0;
    while (cBuffer[count] != '\0') {
        count++;
    }
    return count;
}

char* MTX::String::sBuffer()
{
    return _cBuffer;
}

MTX::String& MTX::String::operator=(String& sObject) 
{
    std::cout << "Copied a string bject via =\n";
    return (String&) Buffer<char>::operator=((String&) sObject);
}

MTX::String& MTX::String::operator=(String&& sObject) noexcept 
{
    std::cout << "moved a string object via = \n";
    return (String&) Buffer<char>::operator=((String&&) sObject);
}

MTX::String& MTX::String::operator=(const char* cBuffer)
{
    Clear();

    //Get Length of a buffer with String::Length();
    int cBufferSize = String::Length(cBuffer) + 1;

    //Create a buffer
    _cBuffer = new char[cBufferSize];
    _cBufferSize = cBufferSize;

    //Copy contents of a buffer to local buffer
    Memory::Copy((void*)cBuffer, (void*)_cBuffer, _cBufferSize);

    return *this;
}

MTX::String MTX::String::operator+(String& sObject)
{

    //Calculate the size of a buffer we will need
    int _cBufferSizeTmp = (_cBufferSize - 1) + sObject._cBufferSize;

    //Now create a buffer which can hold both of the objects
    char* _cBufferTmp = new char[_cBufferSizeTmp];

    //Now copy first string in to it, but without the terminator
    Memory::Copy((void*)_cBuffer, _cBufferTmp, _cBufferSize - 1);

    //Copy second string but with null terminator
    //Now that takes into account the offset of a second string to copy (void *)(_cBufferTmp + (_cBufferSize - 1))
    Memory::Copy((void*)sObject._cBuffer, (void *)(_cBufferTmp + (_cBufferSize - 1)), sObject._cBufferSize);

    //And now we construct our tmp string with it
    String _tmpString;
    _tmpString._cBuffer = _cBufferTmp;
    _tmpString._cBufferSize = _cBufferSizeTmp;

    return _tmpString;
}

MTX::String MTX::String::operator+(const char* pBuffer)
{

    //Calculate the size of a buffer we will need
    int _cBufferSizeTmp = (_cBufferSize - 1) + String::Length(pBuffer) + 1;

    //Now create a buffer which can hold both of the objects
    char* _cBufferTmp = new char[_cBufferSizeTmp];

    //Now copy first string in to it, but without the terminator
    Memory::Copy((void*)_cBuffer, _cBufferTmp, _cBufferSize - 1);

    //Copy second string but with null terminator
    //Now that takes into account the offset of a second string to copy (void *)(_cBufferTmp + (_cBufferSize - 1))
    Memory::Copy((void*)pBuffer, (void*)(_cBufferTmp + (_cBufferSize - 1)), String::Length(pBuffer) +1);

    //And now we construct our tmp string with it
    String _tmpString;
    _tmpString._cBuffer = _cBufferTmp;
    _tmpString._cBufferSize = _cBufferSizeTmp;

    //no need to delete the tmp buffer, cause ownership was taken away from it.

    //And return it by value cause it is gona get deleted anyway
    return _tmpString;
}

MTX::String MTX::String::operator<<(String& sObject) noexcept
{
    return *this + sObject;
}

MTX::String MTX::operator<<(MTX::String sObjectSelf, MTX::String sObject) {
    return sObjectSelf + sObject;
}

Here is the base class

//Constructor Default
        Buffer() : _cBuffer(nullptr), _cBufferSize(0) {};

        Buffer(T* pBuffer, int len) {


            //Check if pBuffer is not nullptr
            if (pBuffer != nullptr) {
                //Allocate the memory needed for a buffer
                _cBuffer = new T[len];
                _cBufferSize = len;
                //Now copy contents of a buffer 
                Memory::Copy((void*)pBuffer, (void*)_cBuffer, sizeof(T) * len);
            }
            else {
                _cBuffer = nullptr;
                _cBufferSize = 0;
            }
        };

        Buffer(Buffer& Object)
        {

            //If attempted to clone empty buffer
            if (Object._cBuffer != nullptr) {
                //Create new buffer with a size of a source buffer
                _cBuffer = new T[Object._cBufferSize];
                _cBufferSize = Object._cBufferSize;

                //Copy contents of that buffer in to local
                Memory::Copy((void*)Object._cBuffer, (void*)_cBuffer, _cBufferSize);
            }
            else {
                _cBuffer = nullptr;
                _cBufferSize = 0;
            }

        };

        Buffer(Buffer&& Object) {

            // If attempting to move empty buffer
            if (Object._cBuffer != nullptr) {

                //Take ownership of buffer
                _cBuffer = Object._cBuffer;
                Object._cBuffer = nullptr;

                //Set buffer size
                _cBufferSize = Object._cBufferSize;
                Object._cBufferSize = 0;

            }
            else {
                _cBuffer = nullptr;
                _cBufferSize = 0;
            }
        };

        Buffer& operator=(Buffer& Object) {

            //Clear buffer, cause it is going to be cleaned anyway
            Clear();

            //If attempted to clone empty buffer
            if (Object._cBuffer != nullptr) {

                //Create new buffer with a size of a source buffer
                _cBuffer = new T[Object._cBufferSize];
                _cBufferSize = Object._cBufferSize;

                //Copy contents of that buffer in to local
                Memory::Copy((void*)Object._cBuffer, (void*)_cBuffer, _cBufferSize);
            }

            return *this;
        };

        Buffer& operator=(Buffer&& Object) {

            //Same as copy assign, buffer is going to be cleared anyway
            Clear();

            // If attempting to move empty string
            if (Object._cBuffer != nullptr) {

                //Take ownership of buffer
                _cBuffer = Object._cBuffer;
                Object._cBuffer = nullptr;

                //Set buffer size
                _cBufferSize = Object._cBufferSize;
                Object._cBufferSize = 0;

            }

            return *this;
        };

And string constructors

//Constructors
        String() 
            :Buffer() {

                        std::cout << "Created an empty string object\n";
    
        };

        String(const char* cBuffer)
            :MTX::Buffer<char>((char*)cBuffer, String::Length(cBuffer) + 1) {
            
                        std::cout << "Created a string object"<<" \""<<cBuffer<<"\"\n";
            
        };

        String(String& sObject)
            :MTX::Buffer<char>((String&)sObject) {

                        std::cout << "Created (Copy) a string object" << " \"" << sObject._cBuffer << "\"\n";
            
        };

        String(String&& sObject) noexcept
            :Buffer<char>((String&&)sObject) {
            
                        std::cout << "Created (Move) a string object" << " \"" << _cBuffer << "\"\n";

        };

        ~String() {
        
        
                        std::cout << "deleting a string object\n";
        
            
        }

Why does it first move it into another tmp object before actually assigning it to the Greetings object.

Because that's what you told it to do.

Your operator+ returns a prvalue. Pre-C++17, that means it returns a temporary, which must be initialized by the return statement. Since you are returning a variable that denotes an automatic object within the function, that means the temporary will be moved into the temporary. This move could undergo elision, but there's no guarantee of that.

When you assign the prvalue temporary returned from the function, you are assigning it to an object . You are not using it to initialize an object; you're assigning it to a live object that has already been constructed. This means that the prvalue temporary must be move-assigned from the temporary into the object you are assigning into. And move-assignment is never elided .

That's two move operations, one of them is required.

Post-C++17, returning a prvalue means returning an initializer for an object. The object it initialized will be moved into under the same reasoning as above.

However, you are still assigning the prvalue to a live object. This means that the prvalue must manifest a temporary, which will then be used as the source for the move-assignment. Manifesting a temporary means using the initializer from the function to create a temporary object. And that means move-construction, per the above.

So again, you have two move operations: a potentially elidable move-construction of a temporary, and a never-elidable move-assignment into the live object.

If you had instead done this:


    MTX::String tst1 = "Hello World";
    MTX::String tst2 = "! And good bye\n";
    
    MTX::String Greetings = tst1 + tst2;
    std::cout << Greetings.sBuffer();  //Didnt implement ostream stuff yet

Then Greetings object would be initialized by a prvalue, not assigned to by a prvalue. Pre-C++17, both the move from the automatic within the function and the move from the temporary into Greetings could be elided. Post-C++17, the move from within the function could still be elided, but there is no move from the prvalue. It never manifests a temporary at all; it will be used to directly initialize the Greetings object. That is, there is only one move to elide; no second move ever even potentially happens.

The take home lesson is this: avoid creating an object, then initializing it whenever possible. Create and initialize the object in one step.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM