9

为什么/为什么不?

假设我有一个类,它在构造函数中接受一个字符串并存储它。这个类成员应该是一个指针,还是一个值?

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 字符串传递给每个对象的构造函数,那么它们都不应该拥有所有权。可能只是复制字符串。

所以(假设我没有完全错)很明显在课外做什么但是当你设计时class GenericTextHaver,你如何决定文本拥有的方法?

如果您只需要一个在其构造函数中采用 const 字符串的类,并允许您从中获取具有相同值的 const 字符串那么您如何决定如何在内部表示它?

4

4 回答 4

11

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

那么为何不?

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

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

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

是的。一个编写良好的类具有“值语义”(这意味着它被设计为被视为一个值)——因此被复制和移动。

曾几何时,当我第一次编写代码时,指针通常是让计算机快速完成某事的最有效方式。如今,借助内存缓存、管道和预取,复制几乎总是更快。(对真的!)

在多处理器环境中,除了最极端的情况外,复制速度要快得多

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

如果您需要它的副本,请复制它。如果你真的只是想移动它,那么std::move它。

如果我要更新 100 个对象,将 5 个字符的 const 字符串传递给每个对象的构造函数,那么它们都不应该拥有所有权。可能只是复制字符串。

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

所以(假设我没有完全错)很明显在课堂之外做什么,但是当你设计类 GenericTextHaver 时,你如何决定文本拥有的方法?

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

如果您只需要一个在其构造函数中采用 const 字符串的类,并允许您从中获取具有相同值的 const 字符串,那么您如何决定如何在内部表示它?

几乎在所有情况下,都存储一份副本。如果 2 个实例实际上需要共享相同的字符串,那么请考虑其他内容,例如std::shared_ptr. 但在这种情况下,他们可能不仅需要共享一个字符串,因此“共享状态”应该封装在其他对象中(理想情况下具有值语义!)

好的,停止说话 - 让我看看这门课应该是什么样子

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;
*/
}; 
于 2015-12-15T22:53:16.460 回答
7

如果构造函数真的“接受一个字符串并存储它”,那么你的类当然需要包含一个std::string数据成员。指针只会指向您实际上并不拥有的其他字符串,更不用说“存储”了:

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

    std::string s_;
};

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

于 2015-12-15T21:12:32.190 回答
1

在大多数情况下,您会希望按值复制。如果在std::string之外被破坏XX将不知道它并导致不良行为。但是,如果我们想在不获取任何副本的情况下执行此操作,那么很自然的做法可能是在其上使用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 将为空。数据的所有权已转移。这样做的好处是它可以保护数据而无需复制开销。

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

于 2015-12-15T21:16:37.197 回答
0

是的,一般来说,拥有一个包含指向对象的指针的类很好,但您需要实现更复杂的行为以使您的类安全。首先,正如之前的响应者之一注意到的那样,保留指向外部字符串的指针是很危险的,因为它可以在不知道类 X 的情况下被销毁。这意味着当 X 的实例被复制或移动时,必须复制或移动初始化字符串建。其次,由于成员 Xs 现在指向在堆上分配的字符串对象(使用 operator new),所以类 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);
}

这些是技术。当你设计你的类时,手头的问题的细节将决定是有一个值还是一个指针成员。

于 2015-12-16T00:00:10.067 回答