如何有效地将底层数据从 std::string 移动到其他类型的变量?

How to efficiently move underlying data from std::string to a variable of another type?

本文关键字:移动 string 其他 类型 变量 std 有效地 数据      更新时间:2023-10-16

编辑:对不起大家,我不认为这个玩具的例子真正反映了我的问题。我应该问的是是否有办法释放 std::string 对象的缓冲区。没有,这是有道理的。谢谢!

假设我有以下(损坏的)代码:

void get_some_data(MyCustomContainer& val)
{
std::string mystr = some_function();
val.m_data = &mystr[0];
}

这是行不通的,因为mystr指向的内存在get_some_data结束时被释放,val.m_data引用的内存将无效。

如何告诉 std::string "不要在析构函数处释放内存缓冲区!我不想复制数据。对象将在其析构函数处处理内存释放。

如果不违反规则,您就无法做到这一点。不允许std::string类显式释放其所有权。事实上,由于 SBO 优化,std::string甚至可能没有分配任何内存:

std::string str1 = "not allocating";
std::string str2 = "allocating on the heap, the string is too large";

此行为完全依赖于平台和实现。如果字符串未在堆上分配其缓冲区,则数据将放置在堆栈上,堆栈不需要取消分配。

{
std::string str1 = "not allocating";
} // no buffer freed

因此,即使有一种方法可以告诉字符串不要取消分配其缓冲区,也无法判断缓冲区是否在堆上管理。

即使有一种方法可以判断字符串是否使用堆栈,您也必须就地分配一个缓冲区作为类成员并复制其内容

传输字符串数据并窃取其对该字符串内存资源的所有权的想法从根本上被打破了,因为如果不复制就无法逃脱,仅仅是因为可能没有所有权可以窃取。


我建议的是,如果您不想更改MyCustomContainer的工作方式,请在所有情况下复制字符串内容:

void get_some_data(MyCustomContainer& val)
{
std::string mystr = some_function();
val.m_data = new char[mystr.size()];
std::memcpy(val.m_data, mystr.data(), mystr.size());
}

相反,如果你允许MyCustomContainer存储一个std::string,你实际上可以在不复制的情况下通过移动字符串来分配缓冲区:

void get_some_data(MyCustomContainer& val)
{
// let m_data be a std::string
val.m_data = some_function();
// The above is equivalent to this:
// std::string mystr = some_function();
// val.m_data = std::move(mystr);
}

移动字符串将调用移动分配。通过移动分配,字符串实现会将mystr缓冲区的所有权转移到m_data。这将阻止任何额外的分配。

如果mystr没有分配,则移动分配将仅复制数据(因此也不会在那里分配)。

解决此问题的正确方法是:

class MyCustomContainer {
public:
std::string m_data;
};
void get_some_data(MyCustomContainer& val) {
val.m_data = some_function();
}

get_some_data甚至可以变成成员函数,这将使调用站点的使用更加容易,并且可能允许m_data私有而不是公开。

如果.m_data是一个 std::string,你可以利用 std::string 的移动赋值运算符:

val.m_data = std::move(mystr);

如果m_data不是 std::string,那么您几乎不走运,内部缓冲区无法访问(因为它应该是)。

不,你不能。std容器只会放弃其托管内存(有时才放弃)给std相同类型的容器。

对于字符串,无论如何这是不可能的,因为大多数实现都会进行短字符串优化并在内部存储短字符串。

您可以将 std 字符串放入某个地方的全局缓冲区并在清理时收获它,但这变得非常复杂。

如果你愿意,你可以使用这个导致未定义行为的代码,所以不应该使用它,但是如果你正在做一些自己的玩具项目,很快就会被放弃,你可以看看它是否适合你。

// REQUIRES: str is long enough so that it is using heap,
// std::string implementation does not use CoW implementation...
// ...
char* steal_memory(string&& str){
alignas(string) char buff[sizeof(string)];
char* stolen_memory = const_cast<char*>(str.data());
new(buff) string(move(str)); 
return stolen_memory;
}

如果要处理短字符串,则应添加malloc并从缓冲区复制。

这里的主要思想是使用从我们的输入字符串中获取所有权的放置 new,而不是在 buff 中调用字符串上的析构函数。没有析构函数意味着没有对 free 的调用,所以我们可以从字符串中窃取内存。

不幸的是,在这种情况下const_cast是UB,所以就像我说的,你永远不应该在严肃的代码中使用此代码。

你可以做mystrstatic

void get_some_data(MyCustomContainer& val)
{
static std::string mystr;
mystr = some_function();
val.m_data = &mystr[0];
}

但是,通过这种方式,您只有一个mystr用于所有get_some_data()调用;所以

get_some_data(mcc1);
get_some_data(mcc2);
// now both `mcc1.m_data` and `mcc2.m_data` point to the same value,
// obtained from the second `some_function()` call

如果可以编译时枚举对get_some_data()的调用,则可以使用模板索引区分mystr

template <std::size_t>
void get_some_data(MyCustomContainer& val)
{
static std::string mystr;
mystr = some_function();
val.m_data = &mystr[0];
}
get_some_data<0U>(mcc1);
get_some_data<1U>(mcc2);
// now `mcc1.m_data` and `mcc2.m_data` point to different values