string += s1和string = string + s1之间的区别

27

当我使用fans = fans + s[i]时,我的一个程序超出了时间限制,而当我使用fans += s[i]时,则被接受...为什么会这样呢?

更详细地解释一下,fans是一个字符串,s也是一个字符串,所以在遍历字符串s时,我只想要s的一些字符,因此我创建了一个新字符串fans。现在有两种方式可以将字符添加到我的新字符串fans中。下面提到了问题。

fans = fans + s[i]; // gives Time limit exceeded 
fans += s[i];       // runs successfully

1
一个创建临时对象,另一个则不会。移动赋值可以使前者更高效,但它无法比后者更好。 - WhozCraig
8
请提供一个 [mcve],否则您的问题将被视为不相关。作为新用户,请先参观 [tour] 并阅读 [ask]。 - Ulrich Eckhardt
2
那么fans是一个std::string,而s[i]是一个char吗?还是另一个只包含单个字符的字符串? - Fabio says Reinstate Monica
1
@Naman,你读了Ulrich Eckhardt和我发表的评论吗?至少你应该澄清fanss[i]是什么类型,但实际上,如果你[编辑]你的问题并添加我们可以复制和编译的完整程序([mcve])会更好。只需要查看你已经收到的链接即可。这不会花费太多时间,而且它会显著改善你的问题。谢谢! - Fabio says Reinstate Monica
2
你不应该粘贴你正在工作的程序代码。你需要从那段代码中提取一个 [mcve] 并发布它。它应该可以直接编译而无需更改。它不应该包含任何不必要的内容来演示问题。你的问题仍然缺少这个,所以它仍然是离题的。 - Ulrich Eckhardt
显示剩余6条评论
4个回答

29
对于内置类型来说,a += ba = a + b 是完全相同的(除了a只被评估一次),但对于类而言,这些运算符被重载并调用不同的函数。
在你的例子中,fans = fans + s[i] 创建了一个临时字符串,并将其分配(移动)给fans,但fans += s[i]没有创建这个临时字符串,因此它可能会更快。

3
如果字符串大小已经达到预先分配的缓冲区大小,"+"操作符也可能导致重新定位。如果以后证明这是个问题(当然需要进行测量),并且大小可以提前估计,那么可以使用string::reserve()来显式增加缓冲区大小。 - IMil
@IMil实际上,这取决于实现如何处理分配,两种方法之间可能没有区别,不是吗?对于一个天真的实现,我希望两种方法几乎需要相同的时间(即1次分配的时间)。 - MC ΔT
1
@MCΔT 没错。因此,建议提供者应该理解标准容器的工作原理以及它们在速度和内存方面保证和不保证什么,如果性能是一个问题的话。在紧密循环内修改std::string绝对是浪费的,但是没有上下文很难建议替代方法。 - IMil

12

std::string有成员函数operator +operator +=。前者通常通过中间临时变量实现后者。效果类似于这样(如果想确切了解你的实现源代码,请检查):

/// note reference return type
std::string& operator +=(char c) 
{
    this->append(c);
    return *this;
}

// note value return type
std::string operator +(char c) const
{
    std::string tmp = *this;
    tmp += c; // or just tmp.append(c) directly
    return tmp;
}

tmp的设置很昂贵。通过在调用端使用move-assignment语义将结果移动到最终目标,通常可以使整个函数更好,但临时变量的开销仍然存在。执行几次,您可能不会注意到差别。但执行数千次,或数百万次等等,则可能产生天壤之别。


在实现string+anythingstring+=anything时,使用另一种方式似乎总是效率低下(多了一次分配和释放内存)。你确定有这样的实现吗? - Deduplicator
@Deduplicator +=通常是两者中更有效的(尽管可能存在异常情况)。通常通过调用上述方式来实现 +,这不会产生任何显着的开销。两个运算符通常通过const引用接受它们的RHS参数(如果它是某些非平凡类型),因此没有额外的复制该参数,只有普通函数调用开销--甚至编译器内联该代码时通常也会被消除。类似地,NRVO或其友元意味着 operator+ 仅创建一个临时对象,而不是两个。 - Miral
1
@Miral,我们一起看看这个,好吗?使用+时,字符串将被重新分配,无法使用任何额外的容量。使用+=时,字符串将为lhs的副本分配空间,然后必须重新分配以容纳rhs。对于不是连接的类型,通常不会出现这个问题,但这是关于具有容量的字符串,特别是std::string - Deduplicator
正如我所说,通常你不会通过调用+来实现+=,因为那样完全低效。对于+调用+=,是的,你正确地指出了上面的确切实现可能不太高效,因为它可能会分配两次内存(但如果附加的rhs适合lhs的容量,则不会发生这种情况)。为了提高效率,在实践中通常会使用不同的实现,在调用+=之前(或道德等价物)保留附加的大小。虽然这超出了本答案的范围。 - Miral

11

如果您使用fans=fans+s[i],则在每个循环过程中都会复制字符串。新元素将被添加到字符串的副本中,并将结果重新分配给变量fans。之后,旧字符串将需要被删除,因为它不再被引用。这需要大量时间。

如果您使用增强赋值fans += s[i],则不需要在每个循环过程中复制字符串,也不需要删除引用变量,因为这里没有引用变量。这样可以节省大量时间。

希望现在您能理解了!


在现代C++中,不需要“复制”字符串 - 可以应用移动语义。 - Alnitak
@Alnitak,我认为移动语义可以使string = string + s1优于没有移动语义,但不如string += s1好。我认为前者总是至少有一个分配和一个完整的复制strings1的所有字节,而后者(string += s1)仅复制s1的字节并且不进行任何额外的分配(只要结果可以适合string已分配的容量)。请参阅我的相关帖子[https://dev59.com/NbXna4cB1Zd3GeqPHj-a],如果我错了,请纠正我! - phonetagger
@phonetagger,我指的是在没有可用的移动语义的情况下由=运算符引起的额外复制。有了移动语义,字符串+ s1的结果可以重新分配给字符串而不会产生复制开销。但是,是的,在首次创建string + s1时会创建一个临时副本,这也涉及到复制操作。 - Alnitak
fans = fans + s[i] 中,operator+ 的结果必须实现一个临时对象;这个对象可以被移动到 fans 中,但这仍然需要完整复制 fans 的原始内容与原始内容同时存在,直到移动发生,然后原始内容被丢弃。 - M.M

3
对于基本类型,a = a + ba += b意思相同。
对于任意类类型,a = a + ba += b是不相关的;它们查找不同的运算符,并且这些运算符可以执行任意操作。它们实际上是不相关的,这是一种设计问题的迹象。 a = a + b大致变为operator=(a, operator+(a, b));实际的查找规则有点更复杂(涉及成员运算符和非成员运算符,以及=没有非成员运算符等),但这是它的核心。 a += b以类似的方式变为operator+=(a, b)
现在,通常的模式是通过+=实现+;如果您这样做,您将得到:
a = a + b

变成

a = ((auto)(a) += b);

其中 (auto) 是新的 / "创建参数的临时副本" 特性。

从根本上讲,a+=b 可以直接重用 a 的内容,而 a = a + b 不能;在计算 a+b 时,它不知道 a 很快就会被覆盖。

一些库使用称为“表达式模板”的技术处理这个问题;a+b 不是一个值,而是一个编译时描述表达式 a+b 的描述,当它赋给 a 时,实际上用于填充 a 的数据。 使用表达式模板,消除了 a+=ba=a+b 知道更多的根本问题。

现在,对于特定的std::string来说,a+b会创建一个临时字符串对象,然后a=(a+b)将其移动到a中(它可以重复使用临时字符串对象的缓冲区或a的缓冲区,标准对此问题不予解释)。 a+=b必须重复使用a缓冲区中多余的容量。因此,如果你调用了a.reserve(1<<30)(10亿),a+=b就不能再分配更多的空间了。

你能否提供关于这个新的(auto)特性的进一步讨论链接? - M.M
@m.m 不是讨论,而是一篇论文:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0849r1.html - Yakk - Adam Nevraumont
谢谢。为了保持一致性,可能应该允许使用 static_cast<auto>(x),即 T(x) 表示 static_cast<T>(x),反之亦然。 - M.M
1
@M.M T(x) 通常不意味着 static_cast<T>(x),反之亦然。虽然有时候确实如此。我认为在这里啰嗦是一种罪过;C++标准在事物的初始版本中往往过于啰嗦,我们应该抵制这种倾向。 - Yakk - Adam Nevraumont

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