按值 C++ 返回时双移动

Double move when returned by value C++

你好,为了学习 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;
}

问题是,当返回值时,它通过移动构造函数移入临时对象,然后根据他的方案再次将该临时对象移动到另一个对象

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;
}

这是控制台输出

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

为什么在实际将其分配给 Greetings 对象之前先将其移动到另一个 tmp 对象中。

这里是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] != '[=13=]') {
        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;
}

这里是基地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;
        };

和字符串构造函数

//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.

因为那是你告诉它要做的。

你的 operator+ returns 一个纯右值。 Pre-C++17,这意味着它 returns 是临时的,必须由 return 语句初始化。由于您要返回一个表示函数内自动对象的变量,这意味着临时对象将移入临时对象。此移动可能会被省略,但不能保证。

当您分配从函数返回的临时纯右值时,您是在将其分配给一个对象。您没有使用它来初始化对象;您正在将其分配给已经构建的活动对象。这意味着纯右值临时必须从临时移动分配到您要分配到的对象中。移动赋值 从未省略 .

这是两个移动操作,其中一个是必需的。

Post-C++17,返回一个纯右值意味着返回一个对象的初始值设定项。它初始化的对象将被移动到上面相同的推理下。

但是,您仍在将纯右值分配给活动对象。这意味着 prvalue 必须显示一个临时值,然后将其用作移动分配的源。显示一个临时对象意味着使用函数中的初始值设定项来创建一个临时对象。根据上述,这意味着移动构造。

同样,您有两个移动操作:临时对象的可能可省略的移动构造,以及对活动对象的不可省略的移动分配。

如果你这样做了:


    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

那么 Greetings 对象将由纯右值 初始化,而不是由纯右值赋值。在 C++17 之前,从函数内的自动移动和从临时移动到 Greetings 的移动都可以省略。 Post-C++17,仍然可以省略函数内部的移动,但是 没有来自纯右值的移动。它根本不会表现出暂时性;它将用于直接初始化 Greetings 对象。也就是说,只有一个动作可以省略;甚至可能不会发生第二步。

教训是:避免创建一个对象,然后尽可能地初始化它。一步创建和初始化对象。