String Literal In Cpp11

在 C++ 中优雅地写多行字符串

背景

在 UT 中常常碰到以下需求:

1
2
3
4
TEST_F(TestSuite, deserialize) {
auto jsonStr = "{\"name\":\"James\",\"nickname\":\"goodboy\"}";
auto object = deserialze(jsonStr);
}

jsonStr 不直观,我们想要 json 原本的样子

String Literal

C++11 提供了 R"delimiter(raw string)delimiter" 的语法,其中 delimiter 可以自行定义

有了 String Literal ,以上代码可以写成:

1
2
3
4
5
6
R"delimiter(
{
"name": "James",
"nickname": "good boy"
}
)delimiter"

但我们往往需要同时兼顾代码的对齐以及字符串的格式(比如将字符串打印出来),比如:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>

int main() {
auto s = R"delimiter(
{
"name": "James",
"nickname": "good boy"
}
)delimiter";
std::cout << s << std::endl;
}
1
2
3
4
5
6

{
"name": "James",
"nickname": "good boy"
}

空行和行首的空格是为了对齐代码而引入的,我们并不希望它们也打印出来,符合期望的输出是:

1
2
3
4
{
"name": "James",
"nickname": "good boy"
}

Trim

为减少性能消耗,Trim 需要在编译期完成

有两种方法做到这一点:

  1. 模板:将字符串作为模板参数传入
  2. constexpr

模板被证明是行不通的,将在函数内声明的字符串字面量作为模板参数传给模板类会报错:non-type template argument refers to object that does not have linkage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
template <int N>
class StringLiteral {
private:
char mArray[N];

public:
template <int S>
constexpr StringLiteral(const char (&s)[S],
bool omitFirstEmptyLine,
bool omitLastEmptyLine) {
static_assert(S >= 1, "");

int begin = 0;
// Omit the first empty line.
if (omitFirstEmptyLine && s[0] == '\n') {
begin = 1;
}
// Omit the last empty line.
// N-2 N-1 N
// \n \0
int end = N;
if (omitLastEmptyLine && s[N - 2] == '\n') {
end = N - 1;
}

int minSpaceNum = N;
bool newLine = true;
int spaceNum = 0;
for (int i = begin; i < end; i++) {
if (s[i] == '\n' || i == end - 1) {
if (minSpaceNum > spaceNum) {
minSpaceNum = spaceNum;
}
newLine = true;
spaceNum = 0;
continue;
}
if (s[i] == ' ' && newLine) {
spaceNum++;
continue;
}
newLine = false;
}

int k = 0;
spaceNum = 0;
for (int i = begin; i < end - 1; i++) {
if (s[i] == '\n') {
spaceNum = 0;
mArray[k] = s[i];
k++;
continue;
}
if (spaceNum < minSpaceNum) {
spaceNum++;
continue;
}
mArray[k] = s[i];
k++;
}
mArray[k] = '\0';

// Omit the last empty line.
if (omitLastEmptyLine && mArray[k - 1] == '\n') {
mArray[k - 1] = '\0';
}
}

constexpr const char* c_str() const {
return mArray;
}
};

template <int N>
constexpr auto literal(const char (&lit)[N]) -> StringLiteral<N> {
return StringLiteral<N>(lit, true, true);
}

编译

由于使用了 constexpr 特性,需要在 c++14 标准下编译

1
clang++ -std=c++14 -O0 -ggdb test.cpp -o test

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int main() {
static constexpr auto a = literal(R"delimiter(
test
test
)delimiter");
static constexpr auto b = literal(R"delimiter(
test
test
)delimiter");
static constexpr auto c = literal(R"delimiter(
test
test
)delimiter");
static constexpr auto d = literal(R"delimiter(
test
test
)delimiter");
static constexpr auto s = d.c_str();
std::cout << a.c_str() << std::endl;
std::cout << b.c_str() << std::endl;
std::cout << c.c_str() << std::endl;
std::cout << s << std::endl;
}
1
2
3
4
5
6
7
8
9
int main() {
static constexpr auto s = literal(R"delimiter(
{
"name": "James",
"nickname": "good boy"
}
)delimiter");
std::cout << s.c_str() << std::endl;
}

输出:

1
2
3
4
{
"name": "James",
"nickname": "good boy"
}

Reference

  1. string literal
  2. Compile-time string concatenation
  3. C-Style Strings as template arguments?

String Literal In Cpp11
https://clcanny.github.io/2020/02/16/computer-science/programming-language/c++/string-literal-in-cpp11/
作者
JunBin
发布于
2020年2月16日
许可协议