简体   繁体   English

值返回时双移动 C++

[英]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.您好,为了学习 c++,我正在构建自己的字符串 class,并且有一个关于按值返回的问题。

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问题是,当返回值时,它通过移动构造函数移动到临时 object,然后根据他的方案,这个临时 object 再次移动到另一个 object

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所以这里是控制台 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.为什么它在实际将其分配给 Greetings object 之前先将其移动到另一个 tmp object 中。

Here is the full source code of string.cpp这是 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这是基础 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.为什么它在实际将其分配给 Greetings object 之前先将其移动到另一个 tmp object 中。

Because that's what you told it to do.因为那是你告诉它去做的。

Your operator+ returns a prvalue.您的operator+返回一个纯右值。 Pre-C++17, that means it returns a temporary, which must be initialized by the return statement.在 C++17 之前,这意味着它返回一个临时的,必须由return语句初始化。 Since you are returning a variable that denotes an automatic object within the function, that means the temporary will be moved into the temporary.由于您要返回一个变量,该变量表示 function 中的自动 object,这意味着临时文件将被移动到临时文件中。 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 .当您分配从 function 返回的临时右值时,您将其分配给object You are not using it to initialize an object;您没有使用它来初始化 object; you're assigning it to a live object that has already been constructed.您将其分配给已构建的实时 object。 This means that the prvalue temporary must be move-assigned from the temporary into the object you are assigning into.这意味着prvalue临时必须从临时移动分配到您要分配到的object中。 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.在 C++17 之后,返回纯右值意味着返回 object 的初始化程序。 The object it initialized will be moved into under the same reasoning as above.它初始化的 object 将在与上述相同的推理下移入。

However, you are still assigning the prvalue to a live object.但是,您仍在将纯右值分配给实时 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.显示临时意味着使用 function 中的初始化程序来创建临时 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.同样,您有两个移动操作:临时的潜在可删除移动构造,以及对实时 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.然后Greetings object 将由纯右值初始化,而不是由纯右值分配。 Pre-C++17, both the move from the automatic within the function and the move from the temporary into Greetings could be elided.在 C++17 之前,从 function 中的自动移动和从临时到Greetings的移动都可以省略。 Post-C++17, the move from within the function could still be elided, but there is no move from the prvalue.在 C++17 之后,来自 function 的移动仍然可以被忽略,但没有来自纯右值的移动。 It never manifests a temporary at all;它根本不会表现出暂时的; it will be used to directly initialize the Greetings object.它将用于直接初始化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.带回家的教训是:避免创建 object,然后尽可能初始化它。 Create and initialize the object in one step.一步创建并初始化 object。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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