使用c风格字符串字面值与构造未命名std::string对象的默认建议

Default advice for using C-style string literals vs. constructing unnamed std::string objects?

本文关键字:对象 string 默认 std 未命名 字符串 风格 字面值 使用      更新时间:2023-10-16

所以c++ 14引入了一些用户定义的文字来使用,其中一个是"s"字面值后缀,用于创建std::string对象。根据文档,它的行为与构造std::string对象完全相同,如下所示:

auto str = "Hello World!"s; // RHS is equivalent to: std::string{ "Hello World!" }

当然,构造一个未命名的std::string对象可以在c++ 14之前完成,但是因为c++ 14的方式简单得多,我认为更多的人会考虑在现场构造std::string对象,这就是为什么我认为问这个问题是有意义的。

所以我的问题很简单:在什么情况下,这是一个好(或坏)的主意构造一个未命名的std::string对象,而不是简单地使用c风格的字符串文字?


示例1:

考虑以下内容:

void foo(std::string arg);
foo("bar");  // option 1
foo("bar"s); // option 2

如果我是正确的,第一个方法将调用std::string的适当构造函数重载来创建foo范围内的对象,第二个方法将首先构造一个未命名的字符串对象,然后从中移动-构造foo的参数。虽然我确信编译器非常擅长优化这样的东西,但是,与第一个替代方案相反,第二个版本似乎涉及额外的移动(当然不是像移动是昂贵的)。但是,在使用合理的编译器编译之后,最终结果很可能是高度优化的,并且无论如何都没有冗余的移动/复制。

同样,如果foo被重载以接受右值引用呢?在这种情况下,我认为调用foo("bar"s)是有意义的,但我可能错了。


示例2:

考虑以下内容:

std::cout << "Hello World!" << std::endl;  // option 1
std::cout << "Hello World!"s << std::endl; // option 2

在这种情况下,std::string对象可能通过右值引用传递给cout的操作符,第一个选项可能传递一个指针,所以两者都是非常便宜的操作,但第二个选项有额外的成本,首先构造一个对象。这可能是一种更安全的方式(?)。


当然,在所有情况下,构造std::string对象可能会导致抛出堆分配,因此也应该考虑异常安全性。这在第二个例子中是一个更大的问题,在第一个例子中,std::string对象无论如何都会在这两个例子中被构造。在实践中,通过构造字符串对象获得异常的可能性很小,但在某些情况下仍然可能是一个有效的参数。

如果你能想到更多的例子来考虑,请把它们包括在你的答案中。我感兴趣的是关于未命名std::string对象使用的一般建议,而不仅仅是这两种特殊情况。我附上这些只是为了指出我对这个话题的一些想法。

另外,如果我有什么错,请随时纠正我,因为我不是一个c++专家。我所描述的行为只是我对事物如何运作的猜测,我并没有基于实际的研究或实验。

在什么情况下,它是一个好(或坏)的主意构造一个未命名的std::string对象,而不是简单地使用c风格的字符串文字?

当你特别想要一个std::string类型的变量时,std::string字面量是一个好主意,无论对于

  • 修改后值(auto s = "123"s; s += 'n';)

  • 更丰富、更直观、更少出错的接口(值语义、迭代器、findsize等)

    • 值语义意味着==, <复制等在值上工作,不像c字符串字面量衰减到const char* s后的指针/引用语义
  • 调用some_templated_function("123"s)将简洁地确保<std::string>实例化,参数能够在内部使用值语义处理

    • 如果你知道其他代码的实例化模板的std::string无论如何,它是显著的复杂性相对于你的资源限制,你可能想传递一个std::string也避免不必要的const char*的实例化,但很少需要关心
  • 包含嵌入式NUL s的值

c风格的字符串字面值可能更可取:

  • 指针式语义是需要的(或者至少不是问题)

  • 值只会传递给函数期望const char*无论如何,或std::string临时将得到构造无论如何,你不关心你给你的编译器优化器一个额外的障碍,以实现编译或加载时的建设,如果有可能重用相同的std::string实例(例如,当通过const -引用传递给函数)-再次很少需要关心。

  • (另一个罕见的和令人恶心的黑客)你在某种程度上利用你的编译器的字符串池行为,例如,如果它保证对于任何给定的翻译单元const char*到字符串文字只会(但当然总是)不同,如果文本不同

    • 你不能真正从std::string .data()/.c_str()中得到相同的内容,因为在程序执行期间,相同的地址可能与不同的文本(以及不同的std::string实例)相关联,并且不同地址的std::string缓冲区可能包含相同的文本
  • 您受益于在std::string离开作用域并被销毁后指针仍然有效(例如给定enum My_Enum { Zero, One }; - const char* str(My_Enum e) { return e == Zero ? "0" : "1"; }是安全的,但const char* str(My_Enum e) { return e == Zero ? "0"s.c_str() : "1"s.c_str(); }不是,std::string str(My_Enum e) { return e == Zero ? "0"s : "1"s; }在始终使用动态分配(无SSO,或更长的文本)时过早悲观)

  • 你利用了编译时相邻c字串字面值的连接(例如"abc" "xyz"变成了一个连续的const char[]字面值"abcxyz")——这在宏替换中特别有用

  • 你是内存受限和/或不想在动态内存分配过程中出现异常或崩溃的风险

讨论

[basic.string。[字面量]21.7列表:

string operator "" s(const char* str, size_t len);

返回: string{str,len}

基本上,使用""s调用一个按值返回std::string的函数——关键是,你可以绑定const引用或右值引用,但不能绑定左值引用。

当用来调用void foo(std::string arg);时,arg将确实被移动构造。

同样,如果foo被重载以接受右值引用呢?在这种情况下,我认为调用foo("bar"s)是有意义的,但我可能错了。

你选择哪一个并不重要。维护方面——如果foo(const std::string&)被更改为foo(const char*),只有foo("xyz");调用将无缝地继续工作,但是有非常一些模糊合理的原因可能是(所以C代码也可以调用它?但是如果不继续为现有的客户端代码提供foo(const std::string&)过载,那就有点疯狂了所以它可以在C语言中实现?——也许;移除对<string>头的依赖?-与现代计算资源无关)。

std:: cout & lt; & lt;"Hello World !"& lt; & lt;std:: endl;//选项1

std:: cout & lt; & lt;你好,世界!"s <<std:: endl;//选项2

前者将调用operator<<(std::ostream&, const char*),直接访问常量字符串文字数据,唯一的缺点是流可能必须扫描终止NUL。选项2";将匹配const引用重载,并暗示构建临时对象,尽管编译器可能能够优化它,因此它们不会经常不必要地这样做,甚至可以在编译时有效地创建字符串对象(这可能只适用于足够短的字符串,可以使用对象内短字符串优化(SSO)方法)。如果他们还没有做这样的优化,那么潜在的好处和这样做的压力/愿望可能会增加。

首先,我认为答案是基于意见的!

对于示例1,您已经提到了使用新的s字面量的所有重要参数。是的,我希望结果是相同的,所以我认为没有必要在定义中说我想要一个std::string。

一个参数可以是,构造函数定义为explicit,并且不会发生自动类型转换。在这种情况下,s字面值是有用的。

但我认为这是一个品味问题!

对于你的例子2,我倾向于使用"旧的"c-string版本,因为生成std::string对象有开销。为cout提供指向字符串的指针是定义良好的,我认为没有任何用例可以从中受益。

所以我的个人建议实际上是(每天都有新的信息可用:-))使用c-string,如果这完全符合我的需要。这意味着:字符串是常量,永远不会被复制或修改,只能按原样使用。所以std::string没有任何好处。

在需要定义std::string时使用's'-literal

简而言之:如果我不需要std::string比旧的c-string提供的额外功能,我就不会使用std::string。对我来说,关键不在于使用s字面值,而在于使用std::string与c-string的区别。

仅作为评论:我必须在非常小的嵌入式设备上编程,特别是在8位avr上。使用std::string会导致大量开销。如果我必须使用动态容器,因为我需要这个容器的特性,那么最好有一个实现和测试得很好的容器。但如果我不需要它,使用它就太贵了。

在像x86机器这样的大目标上,用std::string代替c-string似乎可以忽略不计。但是考虑到一个小设备,你就会知道在大机器上到底发生了什么。

只有我的两美分!

在什么情况下,构造一个未命名的std::string对象而不是简单地使用c风格的字符串字面量是一个好(或坏)主意?

什么是好主意,什么不是好主意,往往随情况而变化。

我的选择是在足够的时候使用原始文字(当我不需要任何其他文字时)。如果我需要访问指向字符串第一个元素的指针以外的任何东西(字符串长度、返回值、迭代器或其他任何东西),那么我使用std::string literal。

当然,在所有情况下,构造std::string对象都可能导致堆分配,这可能引发异常,因此也应该考虑异常安全性。

喔…虽然代码确实可能抛出,但这无关紧要,除非在非常特殊的情况下(例如,运行在或接近硬件内存限制的嵌入式代码,或高可用性应用程序/环境)。

在实践中,我从来没有遇到过内存不足的情况,从编写auto a = "abdce"s;或其他类似的代码。

总之,不需要为实例化std::string literal所产生的内存不足情况的异常安全性而烦恼。如果在执行此操作时遇到内存不足的情况,请在发现错误时更改代码。