常量字符串的std::string_view复杂度

4

我正在开发具有大量静态字符串的库。为了稍微优化运行时,这些字符串应该从以空字符结尾的字符串(经典的C风格char数组)更改为预先知道长度的结构,如std::stringstd::string_view或自定义指针+长度对。 std::string是众所周知且多功能的,但缺点是占用空间较大(至少在x64上为32位),并且如果不适用于小字符串优化,则会产生VM开销和运行时成本,而这种情况适用于我的大多数数据。 string_view听起来是最合适的候选项,只要我能确保数据以空字符结尾(虽然string_view本身没有此类保证,但可以通过惯例来减轻风险)。

仍然很重要的问题:现代编译器是否使用内部的strlen而不使用O^(n)的常量复杂度进行预初始化string_view? 如果是,则保留空字符终止符吗?

当然,我可以添加一些宏来初始化string_views,但那样会很麻烦。 类似于:

#define foo(buf) foo, (sizeof(foo)-1)
const string_view svMyVar(foo("original C-string value"));

我希望编译器能为我简化这个过程。


4
你的意思是要写成#define foo(buf) buf, (sizeof(buf)-1)吗? - 463035818_is_not_a_number
2
你也可以将字符串存储为 char const str[] = "foobar";,这样编译器就可以确定数组的大小,并且您可以通过类型系统查询它。理论上,这允许一个构造函数 string_view(char(&)[N]),它保证在编译时计算字符串的“长度”,但是标准库中没有这样的构造函数。 - dyp
3
除此之外,还有使用用户定义字面量的"foobar"sv,在这种情况下,编译器同样知道字符串长度,并将其传递给函数operator""(char const*, size_t)。除此之外,您还可以拥有constexpr string_view x = ...;,合理优化的编译器应该在编译时初始化它。 - dyp
5
您可以在这里随便尝试一下:https://compiler-explorer.com/z/bc6s847Pr。如预期的那样,即使没有进行优化,`constexpr`也会导致编译时字符串长度的“计算”,而可变的块作用域`string_view`在 -O1 时就已经将其字符串长度计算解析为编译时。使用 sv 用户定义字面量也可以在没有优化的情况下在编译时解析出长度。 - dyp
2个回答

4

很遗憾,std::string_view 没有接受 const char (&) [N] 的构造函数,只有(与 const char* 相关的) const char* 或者 const char*, std::size_t size 的构造函数。

前者必须计算字符串长度,后者已经给出了长度。

除了使用宏,你也可以使用函数。

template <std::size_t N>
std::string_view make_string_view(const char* (&s)[N]) { return {s, N - 1}; }

operator ""sv甚至更简单(尺寸在编译时已知/用于构造string_view):

  • "hello world"sv

请注意,如果需要,您可以包括\0

  • "hello world\0"sv

1
不需要附加\0,因为用户定义的字面值指向一个字符串字面值,就像任何其他字符串字面值一样,都是以空字符结尾的。 - underscore_d
1
但是那个(正常的)\0将会超出范围。 - Jarod42
实际上,我现在在这里看到(https://en.cppreference.com/w/cpp/language/user_literal),对于用户定义的字面量,“_`len`是字符串字面量的长度,**不包括终止的空字符**_”,而您的工厂函数也会删除终止的NUL(或其他终止字符,如果它没有指向字符串文字,而只是一些“其他”`char *`)。 - underscore_d

3
尽管std::string_view(const char*)构造函数具有线性复杂度,但它是一个constexpr构造函数,并且字符串字面值是编译时常量,优化器通常能够在实践中在编译时执行线性复杂度,使运行时恒定。在常量评估上下文中,这是有保证的。
请注意,您建议的宏以及另一个答案中建议的模板与std::string_view(const char*)不同,因为字符串字面值可能包含空终止符,而构造函数仅扩展到第一个终止符,而宏则扩展整个字面值。
如果使用包含未初始化元素和垃圾值的非字符串字面值数组,则特别问题严重。
const string_view svMyVar1("test\0test");
// svMyVar1.size() == 4

const string_view svMyVar2(foo("test\0test"));
// svMyVar2.size() == 9

char arr[32];
arr[0] = 'a';
arr[1] = '\0';
// arr now contains a null terminated "a" followed by 30 garbage chars
const string_view svMyVar3(foo(arr));
// svMyVar3.size() == 31, contains garbage

如果有的话,空终止符会被保留吗?
字符串视图不会修改其所引用的数组。如果所引用的数组在所引用的字符串后面包含了一个空终止符,则该空终止符仍然存在。如果没有空终止符,则不会添加空终止符。
即使视图外部存在空终止符,读取svMyVar[svMyVar.size()]仍然具有未定义的行为。
另一方面,如果您知道那里有空终止符(或任何其他字符),则读取*(svMyVar.data() + svMyVar.size())是可以的。您不能依靠通常情况下字符串视图上的这种情况,但是如果视图是从保证为空终止的字符串字面值创建的,则可以依赖它。

不使用内部strlen

编译器足够聪明,甚至可以在编译时计算strlen("literal")

从技术上讲,std::string_view 在内部使用 Traits::length,而不是 std::strlen


我知道当空终止符是字符数组的一部分时,可能会有不同的行为,但这绝对不是我们关心的问题。我已经在文档中看到过字面量的提及,但从未完全确定它们应该如何工作 - 我可以想象编译器会为了优化而处理数据,去掉终止符,因此我有这个问题。 - PasterOfMuppets
1
@PasterOfMuppets,那么我对你最有用的答案是“保证以空终止符结尾的字符串字面值”。 - eerorika

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