尝试编写一个字符串类,可以实现从std::string进行移动语义。

3

我正在编写自己的字符串类,只是为了学习和巩固一些知识。除了我想要使用带有std::string的移动语义的构造函数之外,我已经把所有东西都搞定了。

在我的构造函数中,我需要复制并将std::string数据指针和其他内容清空,它需要保留在一个空但有效的状态下,而不删除字符串指向的数据,我该怎么做?

到目前为止,我有这个:

class String
{
private:
char* mpData;
unsigned int mLength;
public:
String( std::string&& str)
    :mpData(nullptr), mLength(0)
    {
    // need to copy the memory pointer from std::string to this->mpData

    // need to null out the std::string memory pointer
    //str.clear();  // can't use clear because it deletes the memory
    }
~String()
{
delete[] mpData;
mLength = 0;
}

相关问题:https://dev59.com/omLVa4cB1Zd3GeqPty6O - Brent Bradburn
3个回答

7

无法做到这一点。 std::string 的实现是实现定义的。每个实现都不同。

此外,不能保证字符串将包含在动态分配的数组中。一些std::string实现执行小字符串优化,其中小字符串存储在std::string对象本身内部。


那么这是不可能的吗?在不同类型的类之间实现移动语义通常是一个不好的想法吗?STL 如何处理像 vector 类中的字符串这样的情况? - EddieV223
5
一个vector<string>只需要能够从另一个string移动构造或移动赋值一个string。通过它的移动构造函数和移动赋值运算符,string类已经处理了这个问题。虽然跨类型移动有点奇怪,但如果您控制源类型和目标类型的实现,或者源类型提供某种移动方式(例如,unique_ptr提供一个release成员函数),那么这是完全可能的。 - James McNellis
我真的很讨厌总结性的结论“不可能”。C++给你很多控制权,有状态的分配器至少可以为一般的std::basic_string情况进行管理。它只需要记住分配的块(合理假设一个basic_string不拥有堆上的多个块),并且自定义的move-from只需要在clear之前通知它所有权的变更即可。 - Potatoswatter
2
@Potatoswatter:现在你不能再从任意字符串中移动了。 - Xeo
@Xeo 这不是一个实际的解决方案(嗯,它和任何自定义分配器一样糟糕),但提到你可以做什么比只说“不行”更有建设性。 - Potatoswatter

1
下面的实现方式可以完成所需的功能,但存在一定的风险。
关于这种方法的注意事项:
  • 它使用std::string来管理分配的内存。在我看来,像这样分层分配是一个好主意,因为它减少了单个类试图完成的事情的数量(但由于使用指针,该类仍然具有与编译器生成的复制操作相关的潜在错误)。

  • 我取消了delete操作,因为现在由allocation对象自动执行。

  • 如果使用mpData修改底层数据,则会调用所谓的未定义行为。如此处所示,它是未定义的,因为标准规定它是未定义的。不过,我想知道是否有真实世界的实现,其中const char * std::string::data()的行为与T * std::vector::data()不同--通过这些修改将是完全合法的。可能会发生这样的情况,即通过data()进行的修改不会反映在对allocation的后续访问中,但根据这个问题中的讨论,假设没有通过allocation对象进行进一步更改,似乎很不可能出现这样的修改会导致不可预测的行为。

  • 它是否真正针对移动语义进行了优化?这可能是实现定义的。它也可能取决于传入字符串的实际值。正如我在另一个答案中指出的那样,移动构造函数提供了一种优化机制--但它并不保证会发生优化。


class String
{
private:
char* mpData;
unsigned int mLength;
std::string allocation;
public:
String( std::string&& str)
    : mpData(const_cast<char*>(str.data())) // cast used to invoke UB
    , mLength(str.length())
    , allocation(std::move(str)) // this is where the magic happens
    {}
};

-1

我理解这个问题是关于“如何使移动构造函数行为正确”,而不是“如何使移动构造函数尽可能快”。

如果问题严格来说是“有没有一种可移植的方法可以窃取std::string中的内部内存”,那么答案是“否,因为公共API中没有提供‘转移内存所有权’的操作。”


以下来自移动语义的解释的引用提供了“移动构造函数”的一个很好的总结...
引用如下: C++0x引入了一种新机制,称为“右值引用”,它允许我们通过函数重载等方式检测右值参数。我们所要做的就是编写一个带有右值引用参数的构造函数。在该构造函数内部,我们可以对源代码进行任何操作,只要将其保留在某个有效状态即可。
根据这个描述,我认为您可以实现“移动构造函数”(或“移动构造器”),而不必实际窃取内部数据缓冲区。 一个示例实现:
String( std::string&& str)
    :mpData(new char[str.length()]), mLength(str.length())
    {
    for ( int i=0; i<mLength; i++ ) mpData[i] = str[i];
    }

据我所知,移动语义的重点在于,如果您想要更高效,那么您可以更高效。由于传入的对象是短暂的,因此其内容不需要被保留--因此可以窃取它们,但这并非强制性的。也许,如果您没有转移某个基于堆的对象的所有权,则实现此操作没有意义,但似乎应该是合法的。也许它作为一个阶段性的过渡是有用的--即使那不是整个内容,您也可以窃取尽可能多的内容。
顺便说一下,这里有一个密切相关的问题here,其中正在构建同一种非标准字符串,并包括std::string的移动构造函数。然而,类的内部结构是不同的,建议std::string在内部具有对移动语义的内置支持(std::string->std::string)。

如果您不需要移动动态内存,那么只需使用引用。此外,根据类的实现,您的代码可能需要一个空终止符。String(std :: string&str) - EddieV223
他试图做的不仅仅是移动语义允许的,而是更多。 - Benjamin Lindley
我找到了一篇博客文章,它支持我的答案:http://akrzemi1.wordpress.com/2011/08/30/move-constructor-qa - Brent Bradburn
1
从您的博客文章中:如果您只是在移动构造函数中实现常规复制,那么您确实满足了这两个约束条件,因此这样做是正确的;但是,这样做真的没有任何意义。如果您不能或不想提供比复制构造函数更有效的移动构造函数,那就根本不要定义它。在所有情况下都可以使用复制构造函数。 - EddieV223
@EddieV223:同意——我在我的答案中指出了这一点。但既然这只是“学习”,我想我会在我的答案中注明一些细微差别。 - Brent Bradburn

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接