为什么/为什么不呢?
假设我有一个类,它在构造函数中包含一个字符串并存储它。这个类成员应该是一个指针,还是一个值?
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兆字节的文件作为常量字符串,我真的不想复制它。
如果我正在新建100个对象,将5个字符的const字符串传递给每个构造函数,那么它们都不应该拥有所有权。可能只是拿一个字符串副本。
所以(假设我没有完全错误)很明显该怎么做 outside 这个类,但是当你设计class GenericTextHaver
时,你如何决定文本的方法-having?
如果您只需要一个在其构造函数中包含const字符串的类,并允许您获取具有相同值 out 的const字符串,那么您如何决定如何表示它内部?
答案 0 :(得分:6)
如果构造函数真正“使用字符串并存储它”,那么您的类当然需要包含std::string
数据成员。指针只指向你实际上并不拥有的其他字符串,更不用说“存储”了:
struct X
{
explicit X(std::string s) : s_(std::move(s)) {}
std::string s_;
};
请注意,由于我们取得了字符串的所有权,因此我们也可以按值获取它,然后从构造函数参数中移出。
答案 1 :(得分:6)
std :: string类成员应该是指针吗?
没有
为什么不呢?
因为std :: string与标准库中的每个其他对象一样,并且c ++中所有其他编写良好的对象都被设计为被视为值。
它可能会也可能不会在内部使用指针 - 这不是你关注的问题。所有你需要知道的是它写得很漂亮并且表现得非常有效(实际上比你现在想象的更有效)当被视为一个值时...特别是如果你使用移动构造。
我觉得我想复制那个字符串,但我不知道何时决定。我应该复制载体吗?集?地图?整个JSON文件......?
是。一个编写良好的类具有"值语义" (这意味着它的设计被视为一个值) - 因此被复制和移动。
曾几何时,当我第一次编写代码时,指针通常是使计算机快速执行某些操作的最有效方法。现在,通过内存缓存,管道和预取,复制几乎总是更快。 (是的,真的!)
在多处理器环境中,除了最极端的情况外,复制的速度要快得多。
如果我有一个10兆字节的文件作为常量字符串,我真的不想复制它。
如果您需要它的副本,请复制它。如果你真的只想移动它,那么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;
*/
};
答案 2 :(得分:0)
在大多数情况下,您需要按值复制。如果std::string
在X
之外被销毁,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>
代替,但在这种情况下必须小心。
答案 3 :(得分:0)
是的,一般来说,拥有一个包含指向对象的指针的类是可以的,但是为了使您的类安全,您需要实现更复杂的行为。首先,正如之前的响应者之一注意到保持指向外部字符串的指针是危险的,因为它可以在不知道类X的情况下被销毁。这意味着当X的实例是X时,必须复制或移动初始化字符串。建造。其次,由于成员X.s现在指向堆上分配的字符串对象(使用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字符串*的构造函数。它需要更多的检查(nullptr),只支持复制语义,它可能如下所示:
X(const string* val) : s(nullptr) {
if(val != nullptr)
s = new string(*val);
}
这些是技巧。在设计类时,手头问题的具体细节将决定是否有值或指针成员。