LLVM中的String相关


说明:本文为译文,点击 此处查看原文。

1. 传递字符串(StringRef 类和 Twine 类)

虽然LLVM通常不做太多字符串操作,但是我们有几个重要的APIs接受字符串。两个重要的例子是 Value 类(它有指令、函数等的名称)和 StringMap 类(在 LLVM 和 Clang 中广泛使用)。
这些是泛型类,它们需要能够接受可能包含空字符的字符串。因此,它们不能简单地接受const char *,而接受const std::string&要求客户机执行堆分配,这通常是不必要的。代替的是,许多LLVM APIs使用StringRefconst 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.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 自定义格式化

有两种方法可以定制一个类型的格式化行为。

  1. 使用适当的静态格式化方法为您的类型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);
      }
    }
    
    这是一个有用的扩展机制,用于添加对使用自定义样式选项格式化自定义类型的支持。但是,当您想要扩展格式化库已经知道如何格式化的类型的机制时,它没有帮助。为此,我们需要别的东西。
  2. 提供从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有几个主要的限制,使得更强大的字符串容器更有用:

  1. 您不能直接将StringRef转换为’ const char* ',因为没有办法添加尾随nul(不像在各种更强的类上添加.c_str()方法)。
  2. StringRef不拥有或保留底层字符串字节。因此,它很容易导致悬空指针,并且在大多数情况下不适合嵌入数据结构(相反,使用std::string或类似的东西)。
  3. 出于同样的原因,如果方法“计算”结果字符串,则StringRef不能用作方法的返回值。相反,使用std:: string。
    StringRef不允许您更改指向字符串的字节,也不允许您从范围中插入或删除字节。对于这样的编辑操作,它与Twine类互操作。
  4. 由于其优点和局限性,函数接受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

SmallStringSmallVector的子类,它添加了一些方便的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保存结果。

猜你喜欢

转载自blog.csdn.net/qq_23599965/article/details/88416959