按值 C++ 返回时进行双倍移动

Double move when returned by value C++

本文关键字:移动 C++ 返回 按值      更新时间:2023-10-16

你好,为了学习 C++,我正在构建自己的字符串类,并且有一个关于按值返回的问题。

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

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

int main() {

MTX::String Greetings;
MTX::String tst1 = "Hello World";
MTX::String tst2 = "! And good byen";

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

为什么它先将其移动到另一个 tmp 对象中,然后再实际将其分配给问候对象。

这是字符串的完整源代码.cpp

#include "String.h"
#include "Memory.h"
//Work better on utils
int MTX::String::Length(const char *cBuffer) {
int count = 0;
while (cBuffer[count] != '') {
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;
}

这是基类

//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 objectn";

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


}

为什么它先将其移动到另一个 tmp 对象中,然后再实际将其分配给 Greetings 对象。

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

您的operator+将返回一个 prvalue。在 C++17 之前,这意味着它返回一个临时的,必须由return语句初始化。由于您返回的变量表示函数中的自动对象,这意味着临时对象将被移动到临时对象中。这一举动可能会被忽略,但不能保证这一点。

当您分配从函数返回的临时 prvalue 时,您正在将其分配给一个对象。您不使用它来初始化对象;您正在将其分配给已构造的活动对象。这意味着必须将临时值从临时移动到要分配到的对象中。移动分配永远不会被省略

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

在 C++17 之后,返回 prvalue 意味着返回对象的初始值设定项。它初始化的对象将以与上述相同的推理移动到其中。

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

同样,您有两个移动操作:临时对象的潜在可限定移动构造,以及活动对象中永远无法限定的移动分配。

如果你这样做了:


MTX::String tst1 = "Hello World";
MTX::String tst2 = "! And good byen";

MTX::String Greetings = tst1 + tst2;
std::cout << Greetings.sBuffer();  //Didnt implement ostream stuff yet

然后Greetings对象将由 prvalue初始化,而不是由 prvalue 分配给。在C++17之前,都可以省略从功能内的自动和从临时到Greetings的移动。在 C++17 之后,仍然可以省略函数内部的移动,但不会从 prvalue 移动。它从不表现出暂时的;它将用于直接初始化Greetings对象。也就是说,只有一步可以省略;甚至没有可能发生的第二步。

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