c++clangdestructorone-definition-rulelibtooling

About clang AST and odr-use of destructors


For the following code

struct X {
  int a;
};

int main() {
  X x;
  return 0;
}

The clang AST does not show a DestructorDecl:

CXXRecordDecl 0x55a415f54f00 </home/gkxx/exercises/smfgen/tmp/../tmp/a.cpp:1:1, line:4:1> line:1:8 referenced struct X definition
|-DefinitionData pass_in_registers aggregate standard_layout trivially_copyable pod trivial literal
| |-DefaultConstructor exists trivial
| |-CopyConstructor simple trivial has_const_param implicit_has_const_param
| |-MoveConstructor exists simple trivial
| |-CopyAssignment simple trivial has_const_param needs_implicit implicit_has_const_param
| |-MoveAssignment exists simple trivial needs_implicit
| `-Destructor simple irrelevant trivial needs_implicit
|-CXXRecordDecl 0x55a415f55018 <col:1, col:8> col:8 implicit struct X
|-FieldDecl 0x55a415f550c0 <line:2:3, col:7> col:7 a 'int'
|-CXXConstructorDecl 0x55a415f55320 <line:1:8> col:8 implicit used X 'void () noexcept' inline default trivial
| `-CompoundStmt 0x55a415f557d8 <col:8>
|-CXXConstructorDecl 0x55a415f55458 <col:8> col:8 implicit constexpr X 'void (const X &)' inline default trivial noexcept-unevaluated 0x55a415f55458
| `-ParmVarDecl 0x55a415f55568 <col:8> col:8 'const X &'
`-CXXConstructorDecl 0x55a415f55618 <col:8> col:8 implicit constexpr X 'void (X &&)' inline default trivial noexcept-unevaluated 0x55a415f55618
  `-ParmVarDecl 0x55a415f55728 <col:8> col:8 'X &&'

I tried to get the CXXRecordDecl of struct X and found that decl->getDestructor() returns nullptr.

This makes me a little bit confused, because the standard says that a class without a user-declared destructor will have an implicilty-declared one, and it will be implicitly-defined when odr-used. So what is an odr-used destructor? Is the destructor of this X odr-used? Or is this just about my misunderstanding of clang AST and the behavior of clang::CXXRecordDecl::getDestructor?

Note that in contrast, the clang-AST shows that the default constructor of X is defined, even though it does not do anything.


Solution

  • Is this destructor ODR-used?

    Yes. Quoting basic.def.odr:

    A destructor for a class is odr-used if it is potentially invoked.

    where "potentially invoked" has a non-trivial definition, but in short, is true here because main() declares an object of type X.

    What does it mean that Clang getDestructor() is nullptr?

    The documentation of CXXRecordDecl::getDestructor() just says:

    Returns the destructor decl for this class.

    without even acknowledging that it can return nullptr, let alone explaining what that would mean. Based on reading the source, I conclude it means the destructor is trivial, and that none of the conditions that would cause an implicit declaration to be created anyway (see below) have been satisfied.

    It does not mean that Clang is claiming the destructor is not ODR-used. The Decl::isUsed() method claims to report this information (see documentation on setIsUsed()), but it does not appear to be completely accurate; even if I cause the destructor declaration to be created by adding a virtual function, the destructor is still not marked isUsed().

    Doesn't the standard say the destructor should be defined here?

    Yes--but to be conforming, Clang only needs to produce compiled output that acts "as if" the destructor had been defined. The absence of a particular AST node doesn't make it non-conforming. Even the lack of a definition in the compiled object file doesn't, so long as it adheres to the relevant ABI, thereby cooperating with other tools, to again achieve the required "as if" behavior (under the assumption that the source code complies with the One Definition Rule).

    What are the conditions for Clang to create an implicit declaration?

    This doesn't appear to be documented, so I tried to figure it out from the source. An implicit destructor declaration is created by Sema::DeclareImplicitDestructor() at SemaDeclCXX.cpp:13803. This function is called in several places, and I didn't follow all of the chains backward, but to a first approximation, this will not be called if:

    Note that it is that last condition, overload resolution due to the declaration of x, that causes Clang to declare the three implicit constructors.

    Can I force the destructor to be declared anyway?

    Yes! You can call Sema::ForceDeclarationOfImplicitMembers on a CXXRecordDecl to force Clang to declare the implicit members even if it would not have done so otherwise. For instance, right after parsing, you can use a RecursiveASTVisitor to walk the AST and call this method on every CXXRecordDecl.

    After doing that, all of the implicit members will be available.

    How would I get a Sema object?

    You cannot get it from an ASTContext because that is just the AST, whereas Sema is part of the process for creating the AST. (If you were to save the AST to disk and then load it back, there would not be a Sema object at all.)

    My preferred approach is to use ASTUnit::LoadFromCompilerInvocation to do the initial parse. That yields an ASTUnit object immediately, from which you can get both the Sema (via getSema()) and the ASTContext (via getASTContext()).

    If you are instead using ClangTool::run, which is what the tutorial points people at:

    Tool.run(
      clang::tooling::newFrontendActionFactory<MyASTFrontendAction>().get());
    

    Grab the CompilerInstance in CreateASTConsumer:

    class MyASTFrontendAction : public clang::ASTFrontendAction {
    public:
      virtual unique_ptr<clang::ASTConsumer> CreateASTConsumer(
        clang::CompilerInstance &ci,
        llvm::StringRef inFile) override
      {
        return make_unique<MyASTConsumer>(ci);
      }
    };
    

    Then store it in the ASTConsumer, and use its getSema() when needed:

    class MyASTConsumer : public clang::ASTConsumer {
    public:
      clang::CompilerInstance &m_compilerInstance;
    
      MyASTConsumer(clang::CompilerInstance &compilerInstance)
        : m_compilerInstance(compilerInstance)
      {}
    
      virtual void HandleTranslationUnit(clang::ASTContext &Context) override
      {
        clang::Sema &sema = m_compilerInstance.getSema();
    
        // ...
      }
    };