LLVM中的String相关
说明:本文为译文,点击 此处查看原文。
1. 传递字符串(StringRef
类和 Twine
类)
虽然LLVM通常不做太多字符串操作,但是我们有几个重要的APIs接受字符串。两个重要的例子是 Value
类(它有指令、函数等的名称)和 StringMap
类(在 LLVM 和 Clang 中广泛使用)。
这些是泛型类,它们需要能够接受可能包含空字符的字符串。因此,它们不能简单地接受const char *
,而接受const std::string&
要求客户机执行堆分配,这通常是不必要的。代替的是,许多LLVM APIs使用StringRef
或const twine&
来有效地传递字符串。
1.1 StringRef
类
StringRef
数据类型表示对常量字符串(一个字符数组和一个长度)的引用,并支持std::string
上可用的公共操作,但不需要堆分配。
它可以使用一个C风格的以null结尾的字符串、一个std::string
隐式地被造,也可以使用一个字符指针和长度显式地构造。例如,StringRef find
函数声明为:
iterator find(StringRef Key);
client可以用以下任意一种方式调用这个函数:
Map.find("foo"); // Lookup "foo"
Map.find(std::string("bar")); // Lookup "bar"
Map.find(StringRef("\0baz", 4)); // Lookup "\0baz"
类似地,需要返回string
的APIs可能会返回一个StringRef
实例,该实例可以直接使用,也可以使用str
成员函数将其转换为std::string
。有关更多信息,请查看 llvm/ADT/StringRef.h (doxygen)。
您应该很少直接使用StringRef
类,因为它包含指向外部内存的指针,所以存储该类的实例通常是不安全的(除非您知道不会释放外部存储)。StringRef
在 LLVM 中足够小和普遍,因此它应该总是通过值传递。
1.1.1 StringRef.getAsInteger()成员函数
函数声明为:bool getAsInteger(unsigned Radix, APInt &Result) const;
函数说明:将StringRef
解析为指定基数(Radix)
的整数(Result)
,如果字符串不只是由适当基数中的有效非空数组成,则返回true
;即如果 Radix=10,String 如果不只是由数字 0-9 组成,getAsInteger 就会返回 true。简单的理解就是返回true代表解析StringRef为Integer失败。
1.2 Twine
类
Twine
(doxygen)类是 APIs 接受连接字符串的有效方法。例如,一个常见的LLVM范型是根据带有后缀的另一条指令的名称来命名一条指令,例如:
New = CmpInst::Create(..., SO->getName() + ".cmp");
Twine
类实际上是一个轻量级的rope,它指向临时(分配给栈的)对象。Twine
可以隐式地构造为加运算符应用于字符串的结果(即,一个C字符串,一个std::string
,或者一个StringRef
)。Twine
会延迟字符串的实际连接,直到实际需要它时,才会有效地将其直接呈现到字符数组中。这避免了在构造字符串连接的临时结果时涉及的不必要的堆分配。有关更多信息,请查看 llvm/ADT/Twine.h(doxygen)和这里。
与StringRef
一样,Twine
对象指向外部内存,并且几乎不应该直接存储或提及。它们仅用于在定义一个应该能够有效接受连接字符串的函数时使用。
2. 格式化字符串(formatv
函数)
虽然LLVM不一定要做很多字符串操作和解析,但它确实做了很多字符串格式化。从诊断消息,到llvm工具输出(如llvm-readobj
),再到打印详细的分解清单和LLDB运行时日志,字符串格式化的需求无处不在。
formatv
在本质上类似于printf
,但是使用了另一种语法,这种语法大量借鉴了Python和c#。与printf不同,它推断要在编译时格式化的类型,因此不需要%d之类的格式说明符。这减少了构造可移植格式字符串的脑力开销,特别是对于size_t或指针类型等特定于平台的类型。与printf和Python不同的是,如果LLVM不知道如何格式化类型,它还不能编译。这两个属性确保函数比传统的格式化方法(如printf函数族)更安全,使用起来也更简单。
2.1 简单的格式化
formatv
调用涉及一个由0个或多个替换序列
组成的格式字符串,然后是替换值
的一个可变长度列表。一个替换序列是一个形式为{N[[,align]:style]}
的字符串。
N表示替换值列表中参数的基于0的索引。注意,这意味着可以以任何顺序多次引用相同的参数,可能使用不同的样式和/或对齐选项。
align是一个可选字符串,指定要将值格式化为的字段的宽度,以及字段内值的对齐方式。它被指定为一个可选的对齐样式
,后跟一个正整数字段宽度
。对齐样式可以是字符-(左对齐)、=(中对齐)或+(右对齐)
中的一个。默认值是右对齐的。
style是一个可选字符串,由控制值格式的特定类型组成。例如,要将浮点值格式化为百分比,可以使用样式选项P。
2.2 自定义格式化
有两种方法可以定制一个类型的格式化行为。
- 使用适当的静态格式化方法为您的类型T提供
llvm::format_provider<T>
的模板专门化。
这是一个有用的扩展机制,用于添加对使用自定义样式选项格式化自定义类型的支持。但是,当您想要扩展格式化库已经知道如何格式化的类型的机制时,它没有帮助。为此,我们需要别的东西。namespace llvm { template<> struct format_provider<MyFooBar> { static void format(const MyFooBar &V, raw_ostream &Stream, StringRef Style) { // Do whatever is necessary to format `V` into `Stream` } }; void foo() { MyFooBar X; std::string S = formatv("{0}", X); } }
- 提供从llvm::FormatAdapter继承的格式适配器。
如果检测到该类型派生自namespace anything { struct format_int_custom : public llvm::FormatAdapter<int> { explicit format_int_custom(int N) : llvm::FormatAdapter<int>(N) {} void format(llvm::raw_ostream &Stream, StringRef Style) override { // Do whatever is necessary to format ``this->Item`` into ``Stream`` } }; } namespace llvm { void foo() { std::string S = formatv("{0}", anything::format_int_custom(42)); } }
FormatAdapter<T>
,formatv
将对以指定样式传递的参数调用format
方法。这允许提供任何类型的自定义格式,包括已经有内置格式提供程序的格式。
2.3 formatv
例子
下面将提供一组不完整的示例,演示formatv
的用法。通过阅读doxygen文档或查看单元测试套件可以找到更多信息。
std::string S;
// 基本类型的简单格式化和隐式字符串转换。
S = formatv("{0} ({1:P})", 7, 0.35); // S == "7 (35.00%)"
// 无序引用和多引用
outs() << formatv("{0} {2} {1} {0}", 1, "test", 3); // prints "1 3 test 1"
// 左、右、中对齐
S = formatv("{0,7}", 'a'); // S == " a";
S = formatv("{0,-7}", 'a'); // S == "a ";
S = formatv("{0,=7}", 'a'); // S == " a ";
S = formatv("{0,+7}", 'a'); // S == " a";
// 自定义样式
S = formatv("{0:N} - {0:x} - {1:E}", 12345, 123908342); // S == "12,345 - 0x3039 - 1.24E8"
// Adapters
S = formatv("{0}", fmt_align(42, AlignStyle::Center, 7)); // S == " 42 "
S = formatv("{0}", fmt_repeat("hi", 3)); // S == "hihihi"
S = formatv("{0}", fmt_pad("hi", 2, 6)); // S == " hi "
// Ranges
std::vector<int> V = {8, 9, 10};
S = formatv("{0}", make_range(V.begin(), V.end())); // S == "8, 9, 10"
S = formatv("{0:$[+]}", make_range(V.begin(), V.end())); // S == "8+9+10"
S = formatv("{0:$[ + ]@[x]}", make_range(V.begin(), V.end())); // S == "0x8 + 0x9 + 0xA"
3. String-like容器
在C和C++中有多种传递和使用字符串的方法,LLVM添加了一些可供选择的新选项。在这个列表中选择第一个选项来做你需要做的,它们是根据它们的相对成本排序的。
注意,通常不希望将字符串作为const char*’s
传递。它们有很多问题,包括它们不能表示嵌入的nul (“0”)
字符,而且没有有效的长度可用。‘const char*’
的一般替换是StringRef
。
有关为API选择字符串容器的更多信息,请参见第1节。
3.1 llvm/ADT/StringRef.h
StringRef
类是一个简单的值类,它包含一个指向字符和长度的指针,并且与ArrayRef类非常相关(但是专门用于字符数组)。因为StringRef
携带一个长度,所以它可以安全地处理包含nul字符的字符串,获得长度不需要strlen调用,而且它甚至有非常方便的API来切片和分割它所表示的字符范围。
StringRef非常适合传递已知为活动的简单字符串,因为它们是C字符串文本、std::string、C数组或SmallVector。每一种情况都有一个到StringRef的有效隐式转换,这不会导致执行动态strlen。
StringRef有几个主要的限制,使得更强大的字符串容器更有用:
- 您不能直接将StringRef转换为’ const char* ',因为没有办法添加尾随nul(不像在各种更强的类上添加.c_str()方法)。
- StringRef不拥有或保留底层字符串字节。因此,它很容易导致悬空指针,并且在大多数情况下不适合嵌入数据结构(相反,使用std::string或类似的东西)。
- 出于同样的原因,如果方法“计算”结果字符串,则StringRef不能用作方法的返回值。相反,使用std:: string。
StringRef不允许您更改指向字符串的字节,也不允许您从范围中插入或删除字节。对于这样的编辑操作,它与Twine类互操作。 - 由于其优点和局限性,函数接受StringRef和对象上的方法返回指向其拥有的某个字符串的StringRef是非常常见的。
3.2 llvm/ADT/Twine.h
Twine
类用作API的中间数据类型,这些API希望获取一个可以通过一系列连接内联构建的字符串。Twine通过在堆栈上形成Twine数据类型的递归实例(一个简单的值对象)作为临时对象,将它们链接到一个树中,然后在使用Twine时将其线性化。Twine只能作为函数的参数使用,并且应该始终作为常量引用,例如:
void foo(const Twine &T);
...
StringRef X = ...
unsigned i = ...
foo(X + "." + Twine(i));
这个例子形成了一个类似“blarg.42”的字符串。“通过将值连接在一起,并且不构成包含“blarg”或“blarg.”的中间字符串。
因为Twine是用栈上的临时对象构造的,而且这些实例在当前语句的末尾被销毁,所以它本质上是一个危险的API。例如,这个简单的变量包含未定义的行为,可能会崩溃:
void foo(const Twine &T);
...
StringRef X = ...
unsigned i = ...
const Twine &Tmp = X + "." + Twine(i);
foo(Tmp);
因为临时任务在调用之前就被销毁了。也就是说,Twine的效率比中间的std::string临时函数要高得多,而且它们在StringRef中工作得非常好。只是要意识到它们的局限性。
3.3 llvm/ADT/SmallString.h
SmallString
是SmallVector的子类,它添加了一些方便的API,比如+=,它接受StringRef的API。SmallString避免在预分配的空间足够容纳其数据时分配内存,并且在需要时回调一般堆分配。因为它拥有自己的数据,所以使用它非常安全,并且支持字符串的完全变异。
和SmallVector一样,SmallString的最大缺点是它们的sizeof。虽然它们针对小字符串进行了优化,但它们本身并不特别小。这意味着它们对于堆栈上的临时刮擦缓冲区非常有效,但通常不应该放到堆中:很少看到SmallString作为频繁分配的堆数据结构的成员或按值返回。
3.4 std::string
标准的c++ std::string
类是一个非常通用的类,它(像SmallString)拥有它的底层数据。sizeof(std::string)非常合理,因此它可以嵌入到堆数据结构中并按值返回。另一方面,std:: string是非常低效的内联编辑(如连接一堆东西在一起),因为它是标准库提供的主机的性能特征取决于很多标准库(如libc + +和MSVC提供一个高度优化的字符串类,GCC包含一个很慢实现)。
std::string的主要缺点是,几乎所有使它们变大的操作都可以分配内存,这是很慢的。因此,最好使用SmallVector或Twine作为划痕缓冲区,然后使用std::string保存结果。