我们将从LLVM代码中的RTTI替换开始,并讨论其实现方式。然后,继续讨论基本容器和智能指针。最后,将探讨一些用于表示token位置的重要类,以及如何在Clang中实现诊断。稍后,在第4.6节"Clang插件项目"中,将使用这些类中的某些类在测试项目中。 LLVM由于性能问题而避免使用RTTI。LLVM引入了一些辅助函数,这些函数替换了RTTI对应物,允许从一个类型转换到另一个类型。基本函数如下: llvm::isa\ 类似于Java的instanceof运算符,根据测试对象是否属于测试类而返回true或false。 llvm::cast\ :当确定对象是指定的派生类型时,使用此强制转换运算符。如果转换失败(即对象不是预期类型),llvm::cast将终止程序。
我们将从LLVM代码中的RTTI替换开始,并讨论其实现方式。然后,继续讨论基本容器和智能指针。最后,将探讨一些用于表示token位置的重要类,以及如何在Clang中实现诊断。稍后,在第4.6节"Clang插件项目"中,将使用这些类中的某些类在测试项目中。
LLVM由于性能问题而避免使用RTTI。LLVM引入了一些辅助函数,这些函数替换了RTTI对应物,允许从一个类型转换到另一个类型。基本函数如下:
llvm::isa<>类似于Java的instanceof运算符,根据测试对象是否属于测试类而返回true或false。
llvm::cast<>:当确定对象是指定的派生类型时,使用此强制转换运算符。如果转换失败(即对象不是预期类型),llvm::cast将终止程序。只有在确信转换不会失败时才使用。
llvm::dyn_cast<>:这可能是LLVM中最常使用的强制转换运算符。llvm::dyn_cast用于当预期转换通常会成功,但存在一些不确定性时进行安全的向下转换。如果对象不是指定的派生类型,llvm::dyn_cast<>返回nullptr。
强制转换运算符不接受nullptr作为输入,但有两个特殊的强制转换运算符可以处理nullptr:
llvm::cast_if_present<>:llvm::cast<>的变体,接受nullptr值
llvm::dyn_cast_if_present<>:llvm::dyn_cast<>的变体,接受nullptr值
这两个运算符都可以处理nullptr值。如果输入为nullptr或转换失败,会简单地返回nullptr。
重要提示
强制转换运算符llvm::cast_if_present<>和llvm::dyn_cast_if_present<>最近被引入,具体是在2022年。是流行的运算符llvm::cast_or_null<>和llvm::dyn_cast_or_null<>的替代品,这些运算符最近仍在使用。旧版本仍然受支持,现在将调用重定向到新的强制转换运算符。有关此更改的更多信息,请参见讨论:https://discourse.llvm.org/t/psa-swapping-out-or-null-with-if-present/65018
如何在没有RTTI的情况下执行动态转换操作?这可以通过某些特定的装饰来完成,如一个简单的例子所示,该例子受启发于如何为你的类层次结构设置LLVM风格的RTTI1。将从基类clangbook::Animal开始,有两个派生类:clangbook::Horse和clangbook::Sheep。每匹马都可以根据其速度(以英里/小时为单位)进行分类,而每只羊则可以根据其羊毛重量进行分类:
void testAnimal() auto AnimalPtr =
std::make_unique<clangbook::Horse>(10); if
(llvm::isa<clangbook::Horse>(AnimalPtr)) llvm::outs() << "Animal is
a Horse and the horse speed is: " <<
llvm::dyn_cast<clangbook::Horse>(AnimalPtr.get())->getSpeed() <<
"mph "; else llvm::outs() << "Animal is not a Horse";
图4.2:LLVM isa<>和dyn_cast<>使用示例
这段代码应该产生以下输出:
Animal is a Horse and the horse speed is: 10mph
图4.2中的第48行展示了llvm::isa<>的使用,而第51行展示了llvm::dyn_cast<>的使用。在后一种情况下,将基类转换为clangbook::Horse并调用该类特有的方法。
看看类的实现,这将为我们提供关于RTTI替换工作原理的见解。将从基类clangbook::Animal开始:
class Animal
public: enum AnimalKind AK_Horse, AK_Sheep ;
public: Animal(AnimalKind K) : Kind(K); AnimalKind getKind() const
return Kind;
private: const AnimalKind Kind;
;
图4.3:clangbook::Animal类
代码中最重要的部分是第11行,指定了不同类型的动物。一个枚举值用于马(AK_Horse),另一个用于羊(AK_Sheep)。clangbook::Horse和clangbook::Sheep类的实现可以在以下代码中找到:
class Horse : public Animal
public: Horse(int S) : Animal(AK_Horse), Speed(S);
static bool classof(const Animal *A) return A->getKind() == AK_Horse;
int getSpeed() return Speed;
private: int Speed;
;
class Sheep : public Animal
public: Sheep(int WM) : Animal(AK_Sheep), WoolMass(WM);
static bool classof(const Animal *A) return A->getKind() == AK_Sheep;
int getWoolMass() return WoolMass;
private: int WoolMass;
;
图4.4:clangbook::Horse和clangbook::Sheep类
第25行和第37行特别重要,包含classof静态方法的实现。这个方法对于LLVM中的强制转换运算符至关重要。一个实现可能如下所示(简化版本):
template <typename To, typename From> bool isa(const From *Val)
return To::classof(Val);
图4.5:简化版本的llvm::isa<>实现
同样的机制可以应用于其他强制转换运算符。
我们接下来将讨论各种类型的容器,是STL对应物的更强大的替代品。
LLVM
ADT(抽象数据类型)库提供了一系列容器。其中一些是LLVM特有的,而其他一些可以视为STL容器的替代品。接下来,我们将探索一些由ADT提供的最流行的类。
标准C++库中用于处理字符串的主要类是std::string。尽管这个类设计为通用,但它存在一些与性能相关的问题。一个重要的问题是复制操作。由于在编译器中复制字符串是一个常见的操作,LLVM引入了一个专门的类,llvm::StringRef,可以有效地处理这个操作,而无需使用额外的内存。这个类与C++172中的
和C++203中的std::spanstd::span类似。
llvm::StringRef类维护对数据的引用,不需要像传统的C/C++字符串那样以空字符结束。基本上持有指向数据块的指针和块的大小,使得对象的有效大小为16字节。由于llvm::StringRef保留引用而不是实际数据,必须从一个现有的数据源构造。这个类可以从基本字符串对象如const
char*、std::string和std::string_view实例化。默认构造函数创建一个空对象,llvm::StringRef的典型使用示例如图4.6所示:
#include "llvm/ADT/StringRef.h" ... llvm::StringRef StrRef("Hello,
LLVM!"); // Efficient substring, no allocations llvm::StringRef SubStr =
StrRef.substr(0, 5);
llvm::outs() << "Original StringRef: " << StrRef.str() << "";
llvm::outs() << "Substring: " << SubStr.str() << "";
图4.6:llvm::StringRef使用示例
代码的输出如下:
Original StringRef: Hello, LLVM! Substring: Hello
LLVM中用于字符串操作的另一个类是llvm::Twine,特别适合于将多个对象拼接成一个。llvm::Twine类的典型使用示例如图4.7所示:
#include "llvm/ADT/Twine.h" ... llvm::StringRef Part1("Hello, ");
llvm::StringRef Part2("Twine!"); llvm::Twine Twine = Part1 + Part2; //
Efficient concatenation
// Convert twine to a string (actual allocation happens here)
std::string TwineStr = Twine.str(); llvm::outs() << "Twine result: "
<< TwineStr << "";
图4.7:llvm::Twine使用示例
代码的输出如下:
Twine result: Hello, Twine!
另一个广泛用于字符串操作的类是llvm::SmallString<>,表示一个栈上分配的字符串,大小固定,也可以超过这个大小,此时它会分配堆内存。这是栈分配空间效率和堆分配灵活性的结合。
llvm::SmallString<>的优势在于,在编译器任务中,字符串往往很小,适合栈上分配的空间。这避免了动态内存分配的开销。但在需要更大字符串的情况下,llvm::SmallString仍然可以容纳,过渡到堆内存。llvm::SmallString<>的典型使用示例如图4.8所示:
#include "llvm/ADT/SmallString.h" ... // Stack allocate space for up to
20 characters. llvm::SmallString<20> SmallStr;
// No heap allocation happens here. SmallStr = "Hello, "; SmallStr +=
"LLVM!";
llvm::outs() << "SmallString result: " << SmallStr << "";
图4.8:llvm::SmallString<>使用示例
尽管字符串操作是编译器任务(如文本解析)中的关键,LLVM还有许多其他辅助类。我们接下来将探索它的顺序容器。
LLVM推荐了一些标准库中的array和vector的优化替代品:
llvm::ArrayRef<>:
一个辅助类,设计用于接受顺序列表元素的接口,仅用于只读访问。这个类类似于llvm::StringRef<>,因为它不拥有底层数据,只是引用它。
llvm::SmallVector<>:
对于小尺寸情况的优化向量。类似于第4.3.2节中讨论的llvm::SmallString,特别是,数组的大小不是固定的,允许存储的元素数量增长。如果元素数量保持在N(模板参数)以下,则无需进行额外的内存分配
更好地理解这些容器,如图4.9所示:
llvm::SmallVector<int, 10> SmallVector; for (int i = 0; i < 10; i++)
SmallVector.push_back(i); SmallVector.push_back(10);
图4.9:llvm::SmallVector<>的使用
该vector在Line
1以选择的10(由第二个模板参数指示)大小初始化。该容器提供了类似于std::vector<>的API,使用熟悉的push_back方法添加新元素,如图4.9,Lines
3和5所示。
前10个元素被添加到vector中,而没有进行额外的内存分配(见图4.9,Lines
2-4)。当在第5行添加第11个元素时,数组的大小超过了为10个元素预分配的空间,触发额外的内存分配。这种容器设计有效地最小化了小对象的内存分配,同时保持了在必要时容纳更大尺寸的灵活性。
标准库提供了几种用于存储键值数据的容器,如std::map<>用于通用映射和std::unordered_map<>用于哈希映射。LLVM提供了这些标准容器的替代:
llvm::StringMap<>:
使用字符串作为键的映射,这比标准的关联容器std::unordered_map<std::string,
T>更性能优化。在字符串键占主导地位,且性能至关重要的情况下经常使用,例如在LLVM这样的编译器基础设施中。与LLVM中的许多其他数据结构不同,llvm::StringMap<>不存储字符串键的副本。相反,它保留对字符串数据的引用,因此必须确保字符串数据在映射之外生存,以防止未定义的行为。
llvm::DenseMap<>:
这个map设计在大多数情况下比std::unordered_map<>更节省内存和时间,尽管它有一些约束(例如,键和值具有平凡的析构函数)。当有简单的键值类型并需要高性能查找时,特别适用。
llvm::SmallDenseMap<>:
这个映射类似于llvm::DenseMap<>,但针对通常大小较小的实例进行了优化。它为小映射栈分配,并且只在映射超过预定义的大小时才求助于堆分配。
llvm::MapVector<>:
这个容器保留了插入顺序,类似于Python的OrderedDict。它实现为std::vector和llvm::DenseMap或llvm::SmallDenseMap的混合体。
这些容器利用了二次探查哈希表机制。这种方法在解决哈希冲突时有效,因为在元素查找期间不需要重新计算缓存。这对于性能关键的应用程序至关重要。
LLVM代码中可以找到不同的智能指针。最受欢迎的来自标准模板库:std::unique_ptr<>和std::shared_ptr<>。此外,LLVM还提供了一些辅助类来使用智能指针。其中最著名的是llvm::IntrusiveRefCntPtr<>。这个智能指针设计用于与支持侵入式引用计数的对象一起工作。与std::shared_ptr不同,后者维护自己的控制块来管理引用计数,IntrusiveRefCntPtr期望对象维护自己的引用计数。这种设计可能更节省内存。一个使用示例如下所示:
class MyClass : public llvm::RefCountedBase<MyClass> // ... ;
llvm::IntrusiveRefCntPtr<MyClass> Ptr = new MyClass();
图4.10:llvm::IntrusiveRefCntPtr<>使用示例
智能指针使用了CRTP(Curiously Recurring Template
Pattern),该模式在前述第3.3节"AST遍历"中提到。CRTP当引用计数降至0时,必须释放对象。实现如下:
template <class Derived> class RefCountedBase // ... void Release()
const assert(RefCount > 0 && "Reference count is already zero."); if
(–RefCount == 0) delete static_cast<const Derived *>(this);
图4.11:llvm::RefCountedBase<>中CRTP的使用。代码来源于llvm/ADT/IntrusiveRefCntPtr.h头文件
由于图4.10中的MyClass是从RefCountedBase派生的,可以在图4.11的Line
6上对其进行类型转换。由于转换的类型已知,其是作为模板参数提供的,因此这种转换可行。
我们刚刚完成了LLVM基本库的学习,现在该转向Clang基本库了。Clang是一个编译器前端,其最重要的操作与诊断相关。诊断需要精确的源代码位置信息,让我们探索Clang为这些操作提供的基本类。
LLVM Community. How to set up LLVM-style RTTI for your class
hierarchy. 2023. URL
https://llvm.org/docs/HowToSetUpLLVMStyleRTTI.html ↩
International Organization for Standardization. International
Standard ISO/IEC 14882:2017(E) – Programming Languages – C++.
International Organization for Standardization, 2017. URL
https://www.iso.org/standard/69466.html ↩
International Organization for Standardization. International
Standard ISO/IEC 14882:2020(E) – Programming Languages – C++.
International Organization for Standardization, 2020. URL
https://www.iso.org/standard/73560.html ↩