为什么没有从std::string_view到std::string的隐式转换?

131
有一个从`std::string`到`std::string_view`的隐式转换,尽管这可能会导致很多悬空引用,但它并不被认为是不安全的,只要程序员小心一点就好。
另一方面,没有从`std::string_view`到`std::string`的隐式转换,即使使用相同的参数,也完全相反:因为程序员可能不小心。
很棒的是,C++有一个替代原始`const char*`指针的方法,但同时也让人感到困惑和简化到了极致。
  • 隐式 const char* -> std::string: 可以
  • 隐式 std::string_view -> std::string: 不行
  • 赋值 std::string = const char* : 可以
  • 赋值 std::string = std::string_view: 可以
  • 追加 std::string += const char* : 可以
  • 追加 std::string += std::string_view: 可以
  • 连接 const char* + std::string: 可以
  • 连接 std::string_view + std::string: 不行
  • 连接 std::string + const char*: 可以
  • 连接 std::string + std::string_view: 不行
我是不是漏掉了什么,还是这完全是胡说八道?
最后,没有所有使其类似于const char*的关键部分,这个字符串视图有多有用呢?将其整合到stdlib生态系统中的意义何在,却没有采取最后一步使其完整?毕竟,如果我们需要表示字符串片段的对象,我们可以自己编写。实际上,很多库早在多年前就已经做到了这一点。制定标准的整个目的是使其对最广泛的用例都有用,不是吗?
他们会在C++23中修复这个问题吗?

25
我建议关闭这个问题,因为它不属于讨论性质的问题,而是一篇抱怨。 - Barry
4
这可能适合发布在 https://www.reddit.com/r/cpp (一些 C++ 委员会成员会在那里阅读)。 - Nikos C.
55
这个问题被关闭了,而讨论它会帮助很多程序员,这似乎有些不公平。 - lrleon
30
@Barry,这确实是一个问题。它也是一种抱怨,但这并不意味着它不是一个问题。OP表达了合理的震惊,在我看来这是可以理解的,这并没有削弱问题的技术合法性和有用性。如果这个问题能够重新打开就太好了。 - Don Hatch
3
“Seriously, what's up.”听起来像是一个问题。;) - Super-intelligent Shade
显示剩余3条评论
3个回答

59
问题在于将 std::string_view 转换为 std::string 会复制基础内存,完全包括堆分配,而隐式的 std::string -> std::string_view 则不会。如果您一开始就使用了 std::string_view,那么显然您关心副本,因此您不希望出现隐式的复制。

考虑以下示例:

void foo1(const std::string& x)
{
    foo2(x);
}
void foo2(std::string_view x)
{
    foo3(x);
}
void foo3(const std::string& x)
{
    // Use x...
}

函数foo2可以使用const std::string&参数,但它使用了std::string_view,这样如果传入的字符串不是std::string,它就更加高效了;没有什么奇怪的地方。但如果你只给它一个const std::string&参数,那么它就不如使用std::string_view高效了!

  • foo2std::string参数调用时(例如由foo1调用):当foo2调用foo3时,它创建了字符串的副本。如果它有一个const std::string&参数,它可以使用它已经拥有的对象。
  • foo2const char*参数调用时:早晚都必须制作一份std::string的副本。使用const std::string&参数将使副本更早地制作出来,但总体上无论哪种方式都只有一份副本。

现在想象一下foo2调用多个类似于foo3的函数,或者在循环中调用foo3;它总是在制作完全相同的std::string对象。你希望编译器通知你这个问题。


13
根据我的经验,std::string_view 最常用于优化字符串解析。显然,解析的结果不能存储在视图中,因此需要将其传输到永久存储(std::string)中。为什么要这样冗长呢?毕竟,我有控制权,应该知道在哪里可以将视图可能转换。 - GreenScape
3
@GreenScape,我不明白你第一条评论的意思,也许你可以在你的问题中添加一些示例代码吗?我也不太理解你的第二点; 到底什么是糟糕的设计?foo3 接受 const std::string& 这个事实是不好的设计吗?在 std::string_view 存在之前,这是非常常见的,并且有很多这样的代码存在。还 是 foo2 的实现方式不好吗?因为我同意(尽管它不是糟糕的设计而是糟糕的实现),但它很容易无意中发生,你希望编译器能够捕获它。 - Arthur Tacca
2
@ArthurTacca如果在你的示例中string_viewconst char*替换,你会怎么说?我们已经处理了几十年的const char* -> string隐式转换。 string_view有助于操作临时const char*内存,显然,在某个时刻string_view将通过转换成字符串来触发分配,并且用户有责任确保在何处进行。此外,由于缺少隐式转换,使用到处都是const std::string&的旧代码不能轻松转换为string_view - Juicebox
1
@Juicebox 这是完全不同的情况。首先,字符串字面量是const char*,因此需要进行隐式转换,以便在f接受std::string时使f("foo")起作用(如果它不起作用,那么会引发骚乱)。其次,只有在特别想避免内存分配时才使用string_view - 这就是我的答案的全部意义 - 但这绝对不是const char*的全部意义。 - Arthur Tacca
1
你说传递 std::string_view 比使用 const std::string & 不够高效。这个测试戏剧性地证明了相反的结果。 什么情况下会使你的说法成立? - psimpson
显示剩余15条评论

20

因为昂贵的隐式转换是不可取的...

你在整个示例中只列出了一个隐式转换:const char* -> std::string;而你正在请求另一个隐式转换。在所有其他“OK”标记的函数中,内存分配都是明显/显式的:

  • 赋值:当你将任何东西分配给具有可变存储大小的拥有对象时,就理解可能需要分配。 (除非是移动分配,但是这无关紧要。)
  • 附加I:当你向具有可变存储大小的拥有对象附加内容时,更明显它将需要分配内存以容纳附加数据。 (它可能有足够的保留空间,但是没有人保证字符串如此。)
  • 连接:在所有三种情况下,仅为结果分配内存,而不为任何中间对象分配内存。由于连接的操作数保持不变(无法假定其保存结果),因此显然需要为结果分配内存。

总体而言,隐式转换既有益处也有劣势。C ++实际上大多数情况下都很吝啬,大多数隐式转换都来自C。但是,const char*std::string 是一个例外。正如@ArthurTacca指出的那样,它分配内存。现在,C ++ 核心指南say

C.164: 避免使用隐式转换运算符

原因:
隐式转换可能是必要的(例如,从double到int),但通常会带来惊喜(例如,从String到C风格字符串)。

当意外的转换执行昂贵操作(如分配内存)并且具有副作用时,这种情况变得更加明显,可能会进行系统调用。


PS - std::string_view有很多需要注意的地方;请参考Victor Ciura在CppCon 2018上的演讲:

足够的string_view让我们自取灭亡

因此,请记住它不是某种万能药; 它是另一个需要小心使用而不是粗心的类。

...一个显式构造函数就足够了。

std::string实际上存在一个从std::string_view的显式构造函数。通过将std::string{my_string_view}传递给接受字符串的函数来使用它。


3
为什么?这是一个灾难吗?最糟糕的情况是什么?额外的内存分配吗?如果必要的话,它可以很容易地进行优化。另一方面,我们有一个有缺陷的API。有缺陷的API不那么容易解决。使用这样的API很让人恶心:大量的样板代码只是为了避免这种“神秘的灾难性隐式转换”。请记住,我们谈论的是非常规类型。它有更严重的问题:悬挂引用。我们对此没有问题。但是不接受隐式内存分配! - GreenScape
5
这句话的意思是,如果你要在每个地方都使用明确的std::string{foo},那么你可能不应该首先使用string_view。直接使用字符串即可。使用string_view的主要目的之一是避免这些分配。 - einpoklum
3
@GreenScape,我完全同意你的观点。我决定用“string_view”替换我库中所有可能的函数参数中的“std::string const&”,结果花费了我几乎数天的时间来修复所有突然出现的隐式转换错误(这些错误在我的程序环境下都是可取的转换)。这让我思考委员会在这个问题上是如此的错误,因为他们没有权利指导我编程的方式。 - Jango
4
这个不一致的设计决定存在很多问题。在 C++17 最终确定之前,我曾与标准委员会成员提起过此事,但他们想要防止静默分配的愿望占了上风。我告诉他们,在尝试过早优化用户代码的同时向标准中添加一个烦人的不一致性甚至行不通,因为人们会绕过它,这种不一致性可能导致其他子优化代码,并且静态分析将是一个“更好”的解决方案。可惜,我的担忧被忽略了,所以现在我们就在这里。 - Joseph Thomson
3
@JosephThomson:嗯,我对统一调用语法提案被因为我认为是“地方主义”而拒绝已经有一段时间感到不满,所以...我想我可以理解。 - einpoklum
显示剩余4条评论

5

字符串视图在嵌入式环境中有用的原因之一是它们不执行动态分配,由于长度作为视图的一部分传递,我们获得了安全性。因此,对我而言,缺少对std::string的隐式转换并不是一个破坏性问题,因为那将需要动态分配。在这种环境下,沉默的转换将导致错误,并且必须被发现和移除。


1
请问,嵌入式代码中有多少C++代码?说实话,您的领域无论如何都会放弃C++的一半功能(禁止使用它们)。因此,考虑到您必须编写多少代码来替换stdlib中被放弃的功能,您可以轻松地编写自己的string_view。我的看法是:stdlib必须针对最常见和广泛的用例。并尽可能舒适地使用其功能。所有其他边角案例都可以为特定领域编写。也就是说,这就是为什么stdlib没有BTree或Trie实现的原因。 - GreenScape
@GreenScape,我并不是说我喜欢使用嵌入式C++。但是很多嵌入式代码都是用c++编写的,比如mbed-os、pigweed、arduino、etl。string_view对于在嵌入式C++中开发来说是一个受欢迎的增强功能。我认为标准并没有针对嵌入式开发编写,但是string_view恰好适用于嵌入式开发。 - silvergasp
2
在野外有许多嵌入式 C++ 代码,这是令人惊讶的,因为它通常会编译成比 C 更有效的代码。此外,C++ 还具有许多非常有用的特性,例如模板、constexpr、函数重载、更好的类型安全性、RAII 等等。std::string_view 增加了安全性,而开发人员无需额外付出努力,因此在嵌入式领域非常有用。 - Andrew Goedhart

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