如果std::string::substr返回std::string_view,会存在哪些缺点?

7

看这个例子(摘自这里):

class foo {
    std::string my_str_;

public:
    std::string_view get_str() const {
        return my_str_.substr(1u);
    }
};

这段代码有问题,因为substr返回一个临时的std::string,所以返回的std::string_view引用了一个已经销毁的对象。但是,如果substr返回std::string_view,这个问题就不存在了。
此外,如果substr返回std::string_view而不是std::string,对我来说似乎是合乎逻辑的,因为返回的字符串是字符串的视图,更加高效,因为没有进行复制操作。
如果substr返回std::string_view,是否存在任何缺点(除了明显的缺点:失去与C++14的一些兼容性 - 我并不低估这一点,只是想知道是否存在其他缺点)?
相关问题:如何有效地获取std::string子串的string_view

string_view 是一个相对较新的东西,标准必须保持向后兼容性。 - The Quantum Physicist
std::string_view sv = my_str_; return sv.substr(1u); 会有帮助吗? - Dev Null
@DevNull:是的,它修复了问题。但我想知道如果substr返回std::string_view会有什么缺点(除了明显提到的缺点)。在某些情况下,“转换”为std::string_view可能会自动发生(例如此案例:std::string::substr可以返回它)。 - geza
2
对于期望获得临时字符串的人来说,可能会遇到相反的问题,例如将其传递给C函数f(my_str.substr(1,5).c_str()); - Bo Persson
2
为什么要坚持更改现有方法的签名而不是引入一个新的方法,例如substringview,让大家都感到满意呢? - W.F.
当有人需要一个可修改的子字符串时会发生什么?这是一个好主意,但只有在与substr()分开实现时才是有效的。 - Justin Time - Reinstate Monica
4个回答

4
string_view被发明时,存在过很多争议,对手派的所有反对论点都源自于您展示的这个例子。
然而,就像我总是告诉每个人一样,针对这种糟糕的例子:C++不是Java,也不是Python。 C++是一门低级语言,在这里您几乎完全掌控着内存,并且我重申了《蜘蛛侠》中的陈词滥调:伴随着强大的力量而来的是巨大的责任。如果您不知道string_view是什么,请不要使用它!
你问题的另一部分有一个简单的答案,而你已经回答了它:
会有什么缺点如果substr返回std :: string_view(除了明显的缺点:失去与C ++ 14的一些兼容性)?
危害是使用substr的字符串的每个程序可能不再有效。向后兼容性在计算机业务中非常重要,这也是为什么英特尔的64位处理器仍然接受x86指令的原因,这也是为什么它们没有出局的原因之一。重新发明轮子成本高昂,而金钱是编程的主要组成部分。因此,除非您计划将所有C ++都扔进垃圾桶并重新开始(就像RUST所做的那样),否则您应该在每个新版本中保持旧规则。
您可以弃用某些内容,但是非常小心和缓慢。但是,弃用与更改API不同,这正是您所建议的。

谢谢回答!我稍微编辑了一下我的问题,以更清楚地表达缺点部分。 - geza
原始的boost string_view禁止从string&&转换为string_view,因为作者正确地预见了允许这样做的危险。自c++11以来,c++已经取得了巨大的进步,通过默认改进代码的正确性。委员会做出的这个决定将撤销所有这些进展,并再次在程序中引入微妙的段错误。伴随着强大的力量也应该有一个安全装置,这样你就不能无意中使用这种力量。 - Richard Hodges
@RichardHodges 这是那些不想要 string_view 的人提出的反对观点。虽然我完全支持安全性,但我没有看到除了 string_view 之外的其他解决方案。要么是令人讨厌的字符数组,要么是复制子字符串,要么就是 string_view - The Quantum Physicist
@RichardHodges:是的,但这样你就不能使用临时对象调用一个带有std::string_view参数的函数,例如std::string get_name(); fn(get_name());,这也不好。 - geza
@T.C.:这个讨论的内容在哪里可以找到?我对设计一个新的字符串类非常感兴趣,所以我想阅读关于字符串的所有高质量讨论。谢谢! - geza
显示剩余3条评论

3
缺点显而易见:这将与 C++ 的每个版本自始至终都会产生重大的 API 不兼容性问题。
C++ 不是一个容易破坏 API 兼容性的语言。

1
重新设计 string_view/string 关系这种危险闹剧,可能不是一个坏的 API 变更。不应该允许从临时字符串创建可复制的数据引用对象,如 string_view,除非有明确的转换。 - Richard Hodges
2
@RichardHodges:破坏string_view的API不会像破坏string::substr()的API那样具有破坏性。 - John Zwinck
我们都认同这一点。委员会需要解决关于std::string_view宽容构造函数集的当前严重缺陷。 - Richard Hodges
这是正确的答案。但是只是对措辞有些挑剔,你的意思是C++标准库不倾向于破坏兼容性 - 而不是语言本身?虽然这也是分别正确的。 - Barry

3

以下是一个具体的(尽管略微不完整)代码示例,目前是安全的,但在更改后将变成未定义行为:

std::string some_fn();
auto my_substr = some_fn().substr(3, 4);
// ... make use of my_substr ...

可以说,在这里使用auto可能有些可疑,但在以下情况下完全合理(我个人认为),因为重复类型名称几乎是多余的:

const char* some_fn();
auto my_substr = std::string(some_fn()).substr(3, 4);
// ... make use of my_substr ...

编辑:即使 substr() 总是返回一个 std::string_view,你可以想象这段代码在开发/调试期间仍可能会造成一些痛苦。


谢谢,那真的是一个缺点! - geza
我不确定,为什么第二个版本会导致未定义的行为?是因为可能返回 nullptr 吗? - bielu000
@bielu000 从技术上讲,如所示的代码片段是没有问题的,只有在之后使用my_substr时才会出现未定义行为,但我认为既然已经为其定义了一个变量,这一点是隐含的。我已经编辑了我的答案以明确这一点。 - Arthur Tacca
@bielu000 但如果你还在疑惑为什么它是未定义行为:我所说的是一个假设的世界,在这个世界中.substr返回一个string_view而不是string,就像问题中所问的那样。因此,表达式std::string(some_fn())创建了一个std::string对象,.substr(3,4)返回一个指向该字符串对象的std::string_view。但是一旦该语句完成,该string对象将被销毁并释放其内存,而string_view仍然指向它。这意味着任何后续使用my_substr都将访问已经被释放的内存。 - Arthur Tacca

1
一方面,C++字符串的底层数据结构与C字符串保持兼容(可以通过c_str()成员访问)。 C字符串以null结尾。因此,您基本上只有一个起始char指针,并且不断增加该指针,直到指针指向0
因此,子字符串可以从原始字符串的任意位置开始。但是,由于您无法在原始字符串中插入null,因此您的子字符串仍然需要在与原始字符串相同的位置结束。
--编辑-- 正如John Zwinck所指出的那样,C++字符串可以包含\0字符,但这仍将意味着子字符串将失去其c_str成员,因为它需要修改原始字符串。这也是string_view的缺点,使用std::string_view与API,期望空终止字符串中也注意到了这一点。

2
C++中的std::string可以轻松地包含空字节,不仅仅是在末尾。它绝对不是一个C字符串,因为它有一个单独的长度字段。 - John Zwinck
@JohnZwinck 确实,类型 std::string 通常可以包含嵌入的空字节,但是由于周围应用程序逻辑的保证,特定的 std::string 变量可能保证不包含空字节。在这种情况下,fn_taking_c_str(my_var.substr(0, 3).c_str()) 是当前可行的代码,并且如果 substr() 被更改为返回 string_view,则该代码将停止工作。(尽管至少它会是编译错误,并且可以通过更改为 fn_taking_c_str(std::string(my_var.substr(0, 3)).c_str()) 轻松解决。) - Arthur Tacca

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