C++中的constexpr std::array字符串字面量

5

我一直很愉快地在我的代码中使用以下风格的常量字符串字面值,但并没有真正理解它是如何工作的:

constexpr std::array myStrings = { "one", "two", "three" };

这可能看起来很琐碎,但我对底层发生的细节不够清楚。据我理解,类模板参数推导(CTAD)用于构造具有适当大小和元素类型的数组。我的问题是:

  1. 在这种情况下,std::array的元素类型是什么,还是这是特定于实现的?查看调试器(我正在使用Microsoft C ++),元素只是指向非连续位置的指针。
  2. 以这种方式声明字符串字面量的constexpr数组是否安全?

我可以选择做如下操作,但它不太整洁:

const std::array<std::string, 3> myOtherStrings = { "one", "two", "three" };
2个回答

2
是的,这里使用了CTAD来为您推导模板参数(自C++17起)。 std::array有一个推导向导,使得在这种形式的初始化器中启用了CTAD。
它将推导出myStrings的类型为:
const std::array<const char*, 3>
const char*是应用于初始化列表元素(这些元素是const char数组)的常规数组到指针衰减的结果。
前面的constconstexpr的结果。
数组的每个元素将指向相应的字符串字面值。 constexpr是安全的,您可以通过const char*指针像使用单个字符串字面值一样使用数组元素。尝试通过const_cast修改这些字面量或数组会导致未定义的行为。 const std::array<std::string, 3>也可以工作,但无法在常量表达式中使用。这是因为std:string不允许使用constexpr
CTAD也可以用于推断此类型,但需要使用字符串字面值运算符的帮助:
#include<string>
using namespace std::string_literals;

//...

const std::array myOtherStrings = { "one"s, "two"s, "three"s };

或自 C++20 版本开始:

const auto myOtherStrings = std::to_array<std::string>({ "one", "two", "three" });

2
operator""s用于std::string自C++20起已成为constexpr,因此假定可以在新的编译器中使用constexpr。或者自C++17起,他们可以使用sv后缀使所有它们成为std::string_view字面值(从一开始就是constexpr)。 - ShadowRanger
@ShadowRanger 自C++20开始,它可以在常量表达式中使用,但仍然无法拥有一个常量初始化的std::string,因此constexpr仍然无法工作。 std::string_view / sv将作为一种替代方案,类似于添加大小信息的const char *,并且可以是constexpr - user17732522
我会买单(还没有使用/理解现代的constexpr,而且std::string似乎不太适合一般情况下工作,考虑到数据的内在运行时特性,忽略SSO)。但是,在实际代码中,只要用户不降到NUL终止的C风格字符串级别,我就给所有东西加上sv前缀;为什么要让使用它的每个东西都计算长度(或者在遍历字符串时重复检查NUL),当您可以在编译时将长度烘焙并避免每个运行时的strlen检查呢? - ShadowRanger

1
user17732522已经指出,您原始代码的类型推导会生成const std::array<const char*,3>。这可以工作,但它不是C++std::string,因此每次使用都需要扫描NUL终止符,并且它们不能包含嵌入式NUL。我想强调一下我的评论建议使用std::string_view
由于std::string在本质上依赖于运行时内存分配,除非相关代码的全部也是constexpr(因此根本不存在实际的string在运行时,编译器在编译时计算最终结果),否则无法使用它,并且如果目标是避免针对部分编译时已知的内容进行不必要的运行时工作(特别是如果array在每个函数调用上都被重新创建; 它不是全局或static,所以它被执行多次,而不仅是在使用前初始化一次)。
话虽如此,如果您可以依赖于C++17,则可以使用std::string_view。它有一个非常简洁的文字形式(在任何字符串文字前添加sv作为前缀),并且它完全是constexpr,因此通过执行以下操作:
// Top of file
#include <string_view>
// Use one of your choice:
using namespace std::literals; // Enables all literals
using namespace std::string_view_literals; // Enables sv suffix only
using namespace std::literals::string_view_literals; // Enables sv suffix only

// Point of use
constexpr std::array myStrings = { "one"sv, "two"sv, "three"sv };

您可以使用std::string_view来代替C风格字符串,这样可以避免运行时操作,同时具有大部分std::string的优点(知道自己的长度,可以包含嵌入的NUL,被大多数字符串相关的API接受),因此在函数接受字符串数据的三种常见方式中比C风格字符串更高效:
  1. 对于需要读取类似字符串的现代API,它们通过值接受std::string_view,开销仅为将指针和长度复制到函数中。
  2. 对于接受const std::string&的旧API,当您调用它时,它会构造一个临时的std::string,但它可以使用从std::string_view提取长度的构造函数,因此不需要预先使用strlen遍历C风格字符串以找出要分配的空间大小。
  3. 对于任何需要std::string的API(因为它将修改/存储自己的副本),它们通过值接收std::string,您将获得与#2相同的好处(必须构建,但构建更有效)。
唯一使用std::string_view比使用std::string更劣的情况是第二种情况(如果std::array包含std::string,则不会发生复制),并且只有在进行多个此类调用时才会出现问题;在这种情况下,您可以使用const std::array myStrings = { "one"s, "two"s, "three"s };,支付小的运行时成本来构建真正的string,以避免传递给接受const std::string&的旧式API时的复制。

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