是否允许在std :: string的实现中进行此优化?

时间:2011-01-13 16:41:48

标签: c++ optimization standard-library proxy-object

我正在考虑std::string::substr的实施。它返回一个新的std::string对象,这对我来说似乎有点浪费。为什么不返回引用原始字符串内容的对象,并且可以隐式分配给std::string?一种对实际复制的懒惰评价。这样的课程看起来像这样:

template <class Ch, class Tr, class A>
class string_ref {
public:
    // not important yet, but *looks* like basic_string's for the most part

private:
    const basic_string<Ch, Tr, A> &s_;
    const size_type pos_;
    const size_type len_;    
};

此类的公共接口将模仿真实std::string的所有只读操作,因此使用将是无缝的。然后,std::string可能会有一个新的构造函数,它会占用string_ref,因此用户永远不会更聪明。当您尝试“存储”结果时,您最终会创建一个副本,因此引用没有真正的问题指向数据,然后将其修改为背后。

这个想法是这样的代码:

std::string s1 = "hello world";
std::string s2 = "world";
if(s1.substr(6) == s2) {
    std::cout << "match!" << std::endl;
}

总共构建的std::string个对象不超过2个。对于执行大量字符串操作的代码来说,这似乎是一种有用的优化。当然,这不仅适用于std::string,而是适用于任何可以返回其内容子集的类型。

据我所知,没有实现这样做。

我认为问题的核心是:

给定一个可以根据需要隐式转换为std::string的类,是否符合库编写者的标准,将成员的原型更改为返回类型?或者更一般地说,库编写者是否有余地在这些类型的情况下返回“代理对象”而不是常规对象作为优化?

我的直觉是,这是不允许的,原型必须完全匹配。鉴于您不能单独重载返回类型,这将使​​图书馆编写者无法利用这些类型的情况。就像我说的,我认为答案是否定的,但我想我会问: - )。

6 个答案:

答案 0 :(得分:6)

这个想法是 copy-on-write ,但不是COW整个缓冲区,而是跟踪缓冲区的哪个子集是“真正的”字符串。 (COW,正常形式,在某些库实现中使用(是?)。)

因此您根本不需要代理对象或更改界面,因为这些细节可以完全在内部完成。从概念上讲,您需要跟踪四件事:源缓冲区,缓冲区的引用计数以及此缓冲区中字符串的开头和结尾。

任何时候操作都会修改缓冲区,它会创建自己的副本(来自开始和结束分隔符),将旧缓冲区的引用计数减1,并将新缓冲区的引用计数设置为一。其余的引用计数规则是相同的:复制并将计数增加1,破坏字符串并将计数减1,达到零并删除等。

substr只创建一个新的字符串实例,除非明确指定了开始和结束分隔符。

答案 1 :(得分:3)

这是一个相当广泛使用的众所周知的优化,称为写时复制或COW。基本的事情甚至不是关于子串,而是使用像

这样简单的事情
s1 = s2;

现在,这种优化的问题在于,对于应该在支持多个线程的目标上使用的C ++库,必须使用原子操作来访问字符串的引用计数(或者更糟糕的是,使用互斥锁保护)目标平台不提供原子操作)。这很昂贵,在大多数情况下,简单的非COW字符串实现更快。

见GOTW#43-45:

http://www.gotw.ca/gotw/043.htm

http://www.gotw.ca/gotw/044.htm

http://www.gotw.ca/gotw/045.htm

更糟糕的是,使用COW的库(例如GNU C ++库)不能简单地恢复到简单的实现,因为这会破坏ABI。 (虽然,C ++ 0x来救援,因为这将需要ABI颠簸!:))

答案 2 :(得分:1)

由于substr返回std::string,因此无法返回代理对象,并且它们不能仅仅更改返回类型或重载(由于您提到的原因)。

他们可以通过使string本身能够成为另一个字符串的子句来实现这一点。这将意味着所有用法的内存惩罚(持有额外的字符串和两个size_types)。此外,每个操作都需要检查它是否具有字符或是代理。也许这可以用一个实现指针来完成 - 问题是,现在我们正在为一个可能的边缘情况做一个通用类慢。

如果你需要这个,最好的方法是创建另一个类substring,它从字符串,pos和长度构造,并转换为字符串。您不能将其用作s1.substr(6),但您可以

 substring sub(s1, 6);

您还需要创建带有子字符串和字符串的常用操作以避免转换(因为这是整点)。

答案 3 :(得分:0)

关于您的具体示例,这对我有用:

if (&s1[6] == s2) {
    std::cout << "match!" << std::endl;
}

对于通用解决方案,这可能无法回答您的问题。为此,你需要子字符串CoW,正如@GMan建议的那样。

答案 4 :(得分:0)

您所谈论的是(或曾经)Java的java.lang.String类(http://fishbowl.pastiche.org/2005/04/27/the_string_memory_gotcha/)的核心功能之一。在很多方面,Java的String类和C ++的basic_string模板的设计是相似的,所以我想可以利用这种“子串优化”编写basic_string模板的实现。

您需要考虑的一件事是如何编写c_str() const成员的实现。根据字符串的位置作为另一个字符串的子字符串,它可能必须创建一个新副本。如果请求c_str的字符串不是尾随子字符串,它肯定必须创建内部数组的新副本。我认为这需要在mutable实现的大多数(如果不是全部)数据成员上使用basic_string关键字,这使得其他const方法的实现变得非常复杂,因为编译器不是更长时间能够帮助程序员保持正确性。

编辑:实际上,为了容纳c_str() constdata() const,您可以使用const charT*类型的单个可变字段。最初设置为NULL,它可以是每个实例,在调用charTc_str() const时初始化为指向新data() const数组的指针,并在{{basic_string中删除1}}析构函数,如果非 - NULL

答案 5 :(得分:0)

当且仅当你真的需要比std :: string提供更多的性能时,继续写下你需要它的方式。我之前使用过字符串变体。

我自己的偏好是使用非可变字符串而不是copy-on-write,并使用boost :: shared_ptr或等效字符串,但仅当字符串的长度实际超过16时,所以字符串类也有私有字符串用于短字符串的缓冲区。

这确实意味着字符串类可能带有一点重量。

我的收藏列表中还有一个“切片”类,只要原始对象的生命周期完好无损,它就可以查看居住在其他地方的类的“子集”。所以在你的情况下,我可以切割字符串以查看子字符串。当然它不会被空终止,也没有任何方法可以在没有复制它的情况下实现它。它不是一个字符串类。