能否在编译期判断 std::string_view 是否引用字符串字面量?
结论
在 C++20 中,仅凭 process(std::string_view input) 这个接口,无法用标准、通用的方法在编译期判断 input 是否引用字符串字面量,也无法据此让同一个函数实例分别返回 std::string_view 或 std::string。
如果需要这样的返回行为,可以调整接口:编译期字符串通过模板参数传入,普通输入使用返回 std::string 的接口。截取位置仍可在运行时提供。
为什么原来的判断无法成立
std::string_view 描述一个字符范围,不拥有字符存储,也不记录字符来自字符串字面量、std::string、局部数组还是静态数组。
例如,下面两种输入都会进入相同的函数:
constexpr auto process(std::string_view input);
process("hello");
std::string text = "hello";
process(text);
转换为 std::string_view 后,传入函数的参数类型相同。即使编译器在某些调用处能够追踪数据来源,标准 C++ 也没有提供接口来查询任意 std::string_view 是否引用字符串字面量。
constexpr 表示函数在满足条件时可以参与常量求值,但普通形参不会因此自动成为可用于 if constexpr 的常量表达式。即使某次调用发生在编译期,也不能直接根据普通形参的值选择 if constexpr 分支。
改用普通 if 同样解决不了返回类型问题:auto 返回类型推导不能把 std::string_view 和 std::string 自动合并成一个类型。
先确定是否真的需要复制
能否安全返回视图,取决于底层字符的生命周期。
字符串字面量具有静态存储期,返回指向它的子视图通常没有生命周期问题。其他存储也可能有足够长的生命周期,例如静态字符数组,或由调用方保证存活的 std::string。
可以将操作拆分为两个接口,由调用方选择是否需要拥有结果:
#include <cstddef>
#include <string>
#include <string_view>
constexpr std::string_view process_view(
std::string_view input,
std::size_t pos = 0)
{
return input.substr(pos);
}
std::string process_owned(
std::string_view input,
std::size_t pos = 0)
{
return std::string{input.substr(pos)};
}
process_view 不复制字符,调用方必须保证底层存储在结果使用期间始终有效。process_owned 返回独立字符串,其有效性不依赖输入存储继续存在;是否实际发生动态分配取决于字符串长度和实现。
C++20:用模板参数区分编译期字符串
如果希望编译期字符串返回视图、普通输入返回字符串,可以使用 C++20 的类类型非类型模板参数:
#include <cstddef>
#include <string>
#include <string_view>
template<std::size_t N>
struct fixed_string {
char value[N];
constexpr fixed_string(const char (&text)[N])
{
for (std::size_t i = 0; i < N; ++i) {
value[i] = text[i];
}
}
};
// 编译期字符串:返回指向模板参数对象的视图。
template<fixed_string Text>
constexpr std::string_view process(std::size_t pos = 0)
{
return std::string_view{
Text.value, sizeof(Text.value) - 1
}.substr(pos);
}
// 普通输入:返回拥有自身存储的字符串。
inline std::string process(
std::string_view input,
std::size_t pos = 0)
{
return std::string{input.substr(pos)};
}
void example(std::size_t pos)
{
auto view = process<"hello world">(pos);
// view 的类型是 std::string_view。
std::string input = "hello world";
auto owned = process(input, pos);
// owned 的类型是 std::string。
}
这段代码没有检查已有 std::string_view 的来源。字符串内容作为模板参数传入,存放在具有静态存储期的模板参数对象中;返回的视图引用该对象的字符数组。
代码通过 sizeof(Text.value) - 1 排除字面量末尾的空字符,字面量内部的 \0 仍包含在视图长度中。
调用语法决定返回类型:
process<"hello world">(pos)返回std::string_view。process(input, pos)返回std::string。即使把这里的input换成字符串字面量,也会调用普通输入接口。
运行时参数有什么影响
在上述模板方案中,返回类型由所选接口确定,不受运行时截取位置的影响。即使 pos 到运行时才知道,也可以在运行时计算指向静态存储的子视图。
字符串内容在编译期确定,整次函数调用仍可在运行时执行;运行时参数也不会改变字符串底层存储的生命周期。
示例使用 substr(pos),当 pos > size() 时会抛出 std::out_of_range。业务代码应明确处理越界位置。
容易误用的替代方案
std::is_constant_evaluated() 判断当前求值是否处于常量求值上下文,不能判断视图是否引用字符串字面量,也不能解决不同分支返回不同类型的问题。
接受 const char (&)[N] 的数组引用重载也不能严格识别字面量,因为局部字符数组同样能匹配该类型。若直接返回视图,仍需检查数组的生命周期。
原示例还混用了 result 和 results。实际代码应统一变量名,但修正拼写无法解决来源判断和返回类型推导的问题。
备注:内容仅供参考。
