std::string class 成员应该是指针吗?

Should a std::string class member be a pointer?

而why/why不是吗?

假设我有一个 class,它在构造函数中获取一个字符串并将其存储。这个 class 成员应该是一个指针,还是只是一个值?

class X {
    X(const std::string& s): s(s) {}
    const std::string s;
};

或者...

class X {
    X(const std::string* s): s(s) {}
    const std::string* s;
};

如果我要存储原始类型,我会复制一份。如果我要存储一个对象,我会使用指针。

我觉得我想要复制那个字符串,但我不知道什么时候决定。我应该复制向量吗?套?地图?整个 JSON 个文件...?

编辑:

听起来我需要阅读移动语义。但无论如何,我想让我的问题更具体一点:

如果我有一个 10 兆字节的文件作为 const 字符串,我真的不想复制它。

如果我要新建 100 个对象,将 5 个字符的 const 字符串传递到每个对象的构造函数中,none 个对象应该拥有所有权。可能只是复制字符串。

所以(假设我没有完全错)在 class 之外做什么是显而易见的,但是当你设计 class GenericTextHaver ,你是如何决定发文方式的?

如果你只需要一个 class ,它在其构造函数中接受一个 const 字符串,并允许你得到一个具有相同值 out 的 const 字符串, 你如何决定如何在内部表示它?

如果构造函数确实"takes a string and stores it",那么您的class当然需要包含一个std::string数据成员。指针只会指向 您实际上并不拥有的其他 字符串,更不用说 "store":

struct X
{
    explicit X(std::string s) : s_(std::move(s)) {}

    std::string s_;
};

请注意,由于我们正在获取字符串的所有权,因此我们也可以按值获取它,然后从构造函数参数中移出。

在大多数情况下,您会希望按值进行复制。如果 std::stringX 之外被销毁,X 将不知道并导致意外行为。但是,如果我们想在不获取任何副本的情况下执行此操作,那么自然的做法可能是使用 std::unique_ptr<std::string> 并在其上使用 std::move 运算符:

class X {
public:
    std::unique_ptr<std::string> m_str;
    X(std::unique_ptr<std::string> str)
      : m_str(std::move(str)) { }
}

通过这样做,请注意原来的 std::unique_ptr 将为空。数据的所有权已转移。这样做的好处是它可以保护数据而无需副本开销。

或者,如果您仍希望从外部世界访问它,则可以使用 std::shared_ptr<std::string>,但在这种情况下必须小心。

Should a std::string class member be a pointer?

没有

And why not?

因为 std::string,就像标准库中的所有其他对象一样,以及 c++ 中所有其他编写良好的对象都被设计为被视为一个值。

它可能会也可能不会在内部使用指针——这与您无关。您需要知道的是,当它被视为一个值时,它的编写非常精美并且表现非常高效(实际上比您现在可能想象的更高效)......特别是如果您使用移动构造。

I feel like I want to copy that string, but I don't know when to decide that. Should I copy vectors? Sets? Maps? Entire JSON files...?

是的。一个写得很好的 class 有 "value semantics"(这意味着它被设计成像一个值一样对待)——因此被复制和移动。

曾几何时,当我第一次编写代码时,指针通常是让计算机快速执行某项操作的最有效方法。如今,有了内存缓存、管道和预取,复制几乎总是更快。 (是的,真的!)

在多处理器环境中,复制在除最极端情况外的所有情况下都快得多

If I have a 10 megabyte file as a const string, I really don't want to copy that.

如果您需要一份,那就复制吧。如果你真的只是想移动它,那么 std::move 它。

If I'm newing up 100 objects, passing a 5 character const string into each one's constructor, none of them ought to have ownership. Probably just take a copy of the string.

一个 5 个字符的字符串复制起来非常便宜,您甚至都不应该考虑它。只是复制它。信不信由你,std::string 是在充分了解大多数字符串都很短并且经常被复制的情况下编写的。 甚至不会涉及任何内存分配

So (assuming I'm not completely wrong) it's obvious what to do from outside the class, but when you're designing class GenericTextHaver, how do you decide the method of text-having?

以最优雅的方式表达代码,简洁地传达您的意图。让编译器决定机器代码的外观——这是工作。成千上万的人付出了他们的时间来确保它比你以往任何时候都做得更好。

If all you need is a class that takes a const string in its constructor, and allows you to get a const string with the same value out of it, how do you decide how to represent it internally?

几乎在所有情况下,都保存一份副本。如果 2 个实例 实际上需要共享同一个字符串 那么请考虑其他的东西,比如 std::shared_ptr。但在那种情况下,他们可能不仅需要共享一个字符串,所以 'shared state' 应该封装在其他一些对象中(最好具有值语义!)

OK, stop talking - show me how the class should look

class X {
public:

    // either like this - take a copy and move into place
    X(std::string s) : s(std::move(s)) {}

   // or like this - which gives a *miniscule* performance improvement in a
   // few corner cases
/*
   X(const std::string& s) : s(s) {}  // from a const ref
   X(std::string&& s) : s(std::move(s)) {}  // from an r-value reference
*/

  // ok - you made _s const, so this whole class is now not assignable
  const std::string s;

  // another way is to have a private member and a const accessor
  // you will then be able to assign an X to another X if you wish

/*    
  const std::string& value() const {
    return s;
  }

private:
  std::string s;
*/
}; 

是的,一般来说,拥有一个指向对象的指针的 class 很好,但您需要实现更复杂的行为才能使 class 安全。首先,正如之前的一位响应者所注意到的那样,保留指向外部字符串的指针是危险的,因为它可能在 class X 不知情的情况下被销毁。这意味着初始化字符串必须在构造 X 的实例。其次,由于成员 X.s 现在指向堆上分配的字符串对象(使用 operator new),class X 需要一个析构函数来进行适当的清理:

class X {
public:
  X(const string& val) {
    cout << "copied " << val << endl;
    s = new string(val);
  }

  X(string&& val) {
    cout << "moved " << val << endl;
    s = new string(std::move(val));
  }

  ~X() {
    delete s;
  }

private:
  const string *s;
};

int main() {
  string s = "hello world";
  X x1(s); // copy
  X x2("hello world"); // move

  return 0;
}

请注意,理论上您也可以拥有一个采用 const string* 的构造函数。它将需要更多检查 (nullptr),将仅支持复制语义,它可能如下所示:

X(const string* val) : s(nullptr) {
    if(val != nullptr) 
      s = new string(*val);
}

这些是技巧。在设计 class 时,手头问题的具体情况将决定是使用值还是指针成员。