To get Clang to indicate where implicit destructors go, use its
CFG
class
to build a control flow graph (CFG). The elements of that graph
include implicit destructors if the right
BuildOptions
have been set.
Given a
FunctionDecl
that has a definition, plus a pointer to the
ASTContext
that was used to create the AST, you can build the CFG using
CFG::buildCFG
:
clang::CFG::BuildOptions buildOptions;
buildOptions.AddImplicitDtors = true; // Want CFGImplicitDtors.
buildOptions.AddTemporaryDtors = true; // Want CFGTemporaryDtors.
std::unique_ptr<clang::CFG> cfg(clang::CFG::buildCFG(
functionDecl,
functionDecl->getBody(),
&m_astContext,
buildOptions));
The CFG consists of a set of
CFGBlock
s,
each of which contains a sequence of
CFGElement
s.
There are several kinds of elements, but the subclass relevant here is
CFGImplicitDtor
,
which has a getDestructorDecl
method that returns a
CXXDestructorDecl
.
To iterate over this structure is fairly straightforward:
for (clang::CFGBlock const *block : cfg->const_nodes()) {
for (auto elementRef : block->refs()) {
if (auto idtorElt = elementRef->getAs<clang::CFGImplicitDtor>()) {
clang::CXXDestructorDecl const *dtor =
idtorElt->getDestructorDecl(m_astContext);
assert(dtor);
// Do something with 'dtor'.
}
}
}
However, one complication is that, as of Clang+LLVM 16.0.0, the
getDestructorDecl
method is only partially implemented. In
particular, it returns nullptr
for the
CFGBaseDtor
variant. Consequently, we need to add more code before the above if
in order to navigate to the destructor declaration:
if (auto baseDtorElt = elementRef->getAs<clang::CFGBaseDtor>()) {
clang::CXXBaseSpecifier const *baseSpec =
baseDtorElt->getBaseSpecifier();
auto baseType = dyn_cast<clang::RecordType>(
baseSpec->getType().getDesugaredType(m_astContext).getTypePtr());
assert(baseType);
auto baseClass = dyn_cast<clang::CXXRecordDecl>(
baseType->getDecl());
assert(baseClass);
clang::CXXDestructorDecl const *dtor =
baseClass->getDestructor();
assert(dtor);
// Do something with 'dtor'.
}
Two more caveats:
The CFG
module does not insert calls to trivial (empty) destructors,
so those won't show up.
CFG elements do not have any source location information, so there is
not an easy way to associate implicit destructor calls with a
particular source location. For members and base classes, you can
just use FunctionDecl::getEndLoc()
, but for the other variants,
more work would be needed to compute a good location.
Complete example
Here is implicit-dtors.cc
, a complete program to print all
implicit destructors:
// implicit-dtors.cc
// Report implicit destructor calls.
#include "clang/AST/RecursiveASTVisitor.h" // clang::RecursiveASTVisitor
#include "clang/Analysis/CFG.h" // clang::CFG
#include "clang/Basic/Diagnostic.h" // clang::DiagnosticsEngine
#include "clang/Basic/DiagnosticOptions.h" // clang::DiagnosticOptions
#include "clang/Frontend/ASTUnit.h" // clang::ASTUnit
#include "clang/Frontend/CompilerInstance.h" // clang::CompilerInstance
#include "clang/Serialization/PCHContainerOperations.h" // clang::PCHContainerOperations
#include "llvm/Support/raw_os_ostream.h" // llvm::raw_os_ostream
#include <cstdlib> // std::getenv
#include <iostream> // std::cout
#include <sstream> // std::ostringstream
#include <string> // std::string
#include <assert.h> // assert
// Convenient string construction.
#define stringb(stuff) \
(static_cast<std::ostringstream&>(std::ostringstream() << stuff).str())
using clang::dyn_cast;
using std::cout;
using std::endl;
using std::string;
class Visitor : public clang::RecursiveASTVisitor<Visitor> {
public: // data
clang::ASTUnit *m_astUnit;
clang::ASTContext &m_astContext;
bool m_printCFG;
public: // methods
Visitor(clang::ASTUnit *astUnit)
: m_astUnit(astUnit),
m_astContext(astUnit->getASTContext()),
m_printCFG(false)
{}
// Convenience methods to stringify some things.
string locStr(clang::SourceLocation loc) const;
string declLocStr(clang::Decl const *decl) const;
string declNameLocStr(clang::NamedDecl const *decl) const;
// Visitor methods.
bool VisitFunctionDecl(clang::FunctionDecl *functionDecl);
// Print destructors that 'functionDecl' implicitly calls.
void printImplicitDtors(clang::FunctionDecl *functionDecl);
// Kick off the traversal.
void traverseTU();
};
string Visitor::locStr(clang::SourceLocation loc) const
{
return loc.printToString(m_astContext.getSourceManager());
}
string Visitor::declLocStr(clang::Decl const *decl) const
{
return locStr(decl->getLocation());
}
string Visitor::declNameLocStr(clang::NamedDecl const *decl) const
{
return stringb(decl->getQualifiedNameAsString() <<
" declared at " << declLocStr(decl));
}
bool Visitor::VisitFunctionDecl(clang::FunctionDecl *functionDecl)
{
if (functionDecl->isThisDeclarationADefinition()) {
printImplicitDtors(functionDecl);
}
return true;
}
void Visitor::printImplicitDtors(clang::FunctionDecl *functionDecl)
{
clang::CFG::BuildOptions buildOptions;
buildOptions.AddImplicitDtors = true; // Want CFGImplicitDtors.
buildOptions.AddTemporaryDtors = true; // Want CFGTemporaryDtors.
std::unique_ptr<clang::CFG> cfg(clang::CFG::buildCFG(
functionDecl,
functionDecl->getBody(),
&m_astContext,
buildOptions));
// For debugging, print the entire CFG.
if (m_printCFG) {
cout << "CFG for " << declNameLocStr(functionDecl) << "\n";
llvm::raw_os_ostream roo(cout);
cfg->print(roo, m_astContext.getLangOpts(), false /*showColors*/);
}
for (clang::CFGBlock const *block : cfg->const_nodes()) {
for (auto elementRef : block->refs()) {
// According to the API documentation, it should not be necessary
// to special-case 'CFGBaseDtor'. But as of Clang+LLVM 16.0.0,
// 'CFGImplicitDtor::getDestructorDecl' is simply missing the
// implementation of that case, and has a comment that says, "Not
// yet supported."
if (auto baseDtorElt = elementRef->getAs<clang::CFGBaseDtor>()) {
clang::CXXBaseSpecifier const *baseSpec =
baseDtorElt->getBaseSpecifier();
auto baseType = dyn_cast<clang::RecordType>(
baseSpec->getType().getDesugaredType(m_astContext).getTypePtr());
assert(baseType);
auto baseClass = dyn_cast<clang::CXXRecordDecl>(
baseType->getDecl());
assert(baseClass);
clang::CXXDestructorDecl const *dtor =
baseClass->getDestructor();
assert(dtor);
cout << declLocStr(functionDecl)
<< ": Implicit base class dtor call: "
<< declNameLocStr(dtor) << "\n";
}
// If it were not for the above unimplemented functionality, we
// would only need this block.
else if (auto idtorElt = elementRef->getAs<clang::CFGImplicitDtor>()) {
clang::CXXDestructorDecl const *dtor =
idtorElt->getDestructorDecl(m_astContext);
assert(dtor);
cout << declLocStr(functionDecl)
<< ": Implicit dtor call: "
<< declNameLocStr(dtor) << "\n";
}
}
}
}
void Visitor::traverseTU()
{
this->TraverseDecl(m_astContext.getTranslationUnitDecl());
}
int main(int argc, char const **argv)
{
// Copy the arguments into a vector of char pointers since that is
// what 'createInvocationFromCommandLine' wants.
std::vector<char const *> commandLine;
{
// Path to the 'clang' binary that I am behaving like. This path is
// used to compute the location of compiler headers like stddef.h.
commandLine.push_back(CLANG_LLVM_INSTALL_DIR "/bin/clang");
for (int i = 1; i < argc; ++i) {
commandLine.push_back(argv[i]);
}
}
// Parse the command line options.
std::shared_ptr<clang::CompilerInvocation> compilerInvocation(
clang::createInvocation(llvm::ArrayRef(commandLine)));
if (!compilerInvocation) {
// Command line parsing errors have already been printed.
return 2;
}
// Boilerplate.
std::shared_ptr<clang::PCHContainerOperations> pchContainerOps(
new clang::PCHContainerOperations());
clang::IntrusiveRefCntPtr<clang::DiagnosticsEngine> diagnosticsEngine(
clang::CompilerInstance::createDiagnostics(
&(compilerInvocation->getDiagnosticOpts())));
// Run the Clang parser to produce an AST.
std::unique_ptr<clang::ASTUnit> ast(
clang::ASTUnit::LoadFromCompilerInvocationAction(
compilerInvocation,
pchContainerOps,
diagnosticsEngine));
if (ast == nullptr ||
diagnosticsEngine->getNumErrors() > 0) {
// Error messages have already been printed.
return 2;
}
Visitor visitor(ast.get());
if (std::getenv("PRINT_CFG")) {
visitor.m_printCFG = true;
}
visitor.traverseTU();
return 0;
}
// EOF
Its Makefile
:
# Makefile
# Default target.
all:
.PHONY: all
# ---- Configuration ----
# Installation directory from a binary distribution.
CLANG_LLVM_INSTALL_DIR = $(HOME)/opt/clang+llvm-16.0.0-x86_64-linux-gnu-ubuntu-18.04
# ---- llvm-config query results ----
# Program to query the various LLVM configuration options.
LLVM_CONFIG := $(CLANG_LLVM_INSTALL_DIR)/bin/llvm-config
# C++ compiler options to ensure ABI compatibility.
LLVM_CXXFLAGS := $(shell $(LLVM_CONFIG) --cxxflags)
# Directory containing the clang library files, both static and dynamic.
LLVM_LIBDIR := $(shell $(LLVM_CONFIG) --libdir)
# Other flags needed for linking, whether statically or dynamically.
LLVM_LDFLAGS_AND_SYSTEM_LIBS := $(shell $(LLVM_CONFIG) --ldflags --system-libs)
# ---- Compiler options ----
# C++ compiler.
#CXX = g++
CXX := $(CLANG_LLVM_INSTALL_DIR)/bin/clang++
# Compiler options, including preprocessor options.
CXXFLAGS =
CXXFLAGS += -Wall
CXXFLAGS += -Werror
# Silence a warning about a multi-line comment in DeclOpenMP.h.
CXXFLAGS += -Wno-comment
# Get llvm compilation flags.
CXXFLAGS += $(LLVM_CXXFLAGS)
# Tell the source code where the clang installation directory is.
CXXFLAGS += -DCLANG_LLVM_INSTALL_DIR='"$(CLANG_LLVM_INSTALL_DIR)"'
# Linker options.
LDFLAGS =
# Pull in clang+llvm via libclang-cpp.so, which has everything, but is
# only available as a dynamic library.
LDFLAGS += -lclang-cpp
# Arrange for the compiled binary to search the libdir for that library.
# Otherwise, one can set the LD_LIBRARY_PATH envvar before running it.
# Note: the -rpath switch does not work on Windows.
LDFLAGS += -Wl,-rpath=$(LLVM_LIBDIR)
# It appears that llvm::raw_os_ostream::~raw_os_ostream is missing from
# libclang-cpp, so I have to link with LLVMSupport statically.
LDFLAGS += -lLLVMSupport
# Get the needed -L search path, plus things like -ldl.
LDFLAGS += $(LLVM_LDFLAGS_AND_SYSTEM_LIBS)
# ---- Recipes ----
# Compile a C++ source file.
%.o: %.cc
$(CXX) -c -o $@ $(CXXFLAGS) $<
# Executable.
all: implicit-dtors.exe
implicit-dtors.exe: implicit-dtors.o
$(CXX) -g -Wall -o $@ $^ $(LDFLAGS)
# Test.
.PHONY: run
run: implicit-dtors.exe
./implicit-dtors.exe test.cc
.PHONY: clean
clean:
$(RM) *.o *.exe
# EOF
Testcase:
// test.cc
// Test for implicit-dtors.exe.
class HasDtor {
public:
// Note that the CFG omits calls to trivial destructors, so we have to
// provide a definition here to get output.
~HasDtor() {}
};
class MemberHasDtor {
public:
HasDtor m_hasDtor;
// Implicit call to member ~HasDtor expected here.
~MemberHasDtor() {}
};
class InheritsHasDtor : public HasDtor {
public:
// Implicit call to base class ~HasDtor expected here.
~InheritsHasDtor() {}
};
void callsTempObjDtor()
{
// Implicit temporary object destructor.
HasDtor();
}
void callsAutomaticObjDtor()
{
// Implicit destructor of "automatic" object, which is different from
// a temporary because the former arises when a local reference is
// bound, and consequently its lifetime extended.
HasDtor const &localReference = HasDtor();
}
void callsDeleteDtor()
{
// Destructor due to 'delete'.
delete (new HasDtor());
}
// EOF
Test output:
test.cc:16:3: Implicit dtor call: HasDtor::~HasDtor declared at test.cc:8:3
test.cc:22:3: Implicit base class dtor call: HasDtor::~HasDtor declared at test.cc:8:3
test.cc:25:6: Implicit dtor call: HasDtor::~HasDtor declared at test.cc:8:3
test.cc:31:6: Implicit dtor call: HasDtor::~HasDtor declared at test.cc:8:3
test.cc:39:6: Implicit dtor call: HasDtor::~HasDtor declared at test.cc:8:3