2

In C++, the compiler should implicitly insert calls to destructors at places such as delete keyword/end of scope, etc. In C++ the call to destructors should be fixed at compilation time (though the callee may be decided at runtime with vtables). (Is the above right?)

But in the dump of Clang AST while using Clang LibTooling there is no such implicit calls to destructors in the AST. For comparison, there is implicitly generated default constructor and destructor node in the AST even if they are not present in the source code scanned.

For delete keyword, there is a CXXDeleteExpr node in the AST, but it doesn't contain the destructors'CXXDestructorDecl, only contains the type of the object to delete.

For other type of call to destructors, there is no nodes in the AST at all.

Then in Clang LibTooling how to get information about at what places the destructors are called?

jw_
  • 1,663
  • 18
  • 32
  • Maybe there is no need to call any destructor as the class is POD type or simply nothing must be "done" than freeing the memory? – Klaus Jan 06 '20 at 10:18
  • 1
    It seems destructor calls are inserted as part of the AST to CFG pass. I was able to see them inserted with `clang++ --analyze -Xclang -analyzer-checker=debug.DumpCFG test.cpp` Perhaps you can [convert your AST to a CFG](https://adamrehn.com/articles/matching-cfg-blocks-to-basic-blocks/#generating-clang-source-level-cfgs) and then walk its basic blocks? – Botje Jan 06 '20 at 10:46
  • @Klaus in my source scanned, there is an explicit destructor that calls a member function and it is not called too. BTW there are implicitly generated CXXDestructorDecl in the AST node for trivial destructors. – jw_ Jan 07 '20 at 00:50
  • @Botje Confirmed by LLVM source code. In CFG.cpp and CFG.h many conditions that need destructor invocation like CFGBuilder::VisitForStmt/CFGBuilder::VisitCXXCatchStmt will finally add some CFGAutomaticObjDtor objects to the CFG set. – jw_ Jan 07 '20 at 01:37
  • @Botje Do you know any API like LibTooling to walk CFG easily? – jw_ Jan 07 '20 at 01:37
  • @Klaus atBotje But for trivial implicit destructors, there is no CFGAutomaticObjDtor objects in CFG which should have been sort of optimized – jw_ Jan 07 '20 at 01:40
  • The [clang::CFG reference](http://clang.llvm.org/doxygen/classclang_1_1CFG.html) has a few methods for walking a CFG, it seems. I can imagine that Clang doesn't bother generating destructor calls for trivial destructors. – Botje Jan 07 '20 at 08:16

1 Answers1

0

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 CFGBlocks, each of which contains a sequence of CFGElements. 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
Scott McPeak
  • 8,803
  • 2
  • 40
  • 79