Logo

Blog


Destructors and noexcept

Since we have noexcept in the language, destructors are implicitly noexcept. One interesting part here is that this statement is true for user-defined destructors as well as for defaulted or the implicitly generated destructor. The user-defined case is interesting. We have an implicit noexcept here.

The relevant references in the standard are:

[class.dtor] p6: Note: A declaration of a destructor that does not have a noexcept-specifier has the same exception specification as if it had been implicitly declared (14.5).

and

[except.spec] p8: The exception specification for an implicitly-declared destructor, or a destructor without a noexcept-specifier, is potentially-throwing if and only if any of the destructors for any of its potentially constructed subobjects is potentially throwing or the destructor is virtual and the destructor of any virtual base class is potentially-throwing.

All right, nice, but why I'm telling you this? Well, for a long time, C++ Insight was showing, say, interesting results. In C++ Insights, the defaulted destructor did not show up as noexcept if we didn't use it. Use here means creating an object of this type. Consider the following example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct UserProvided {
    ~UserProvided() {}
};

struct Defaulted {
  ~Defaulted() = default;
};


int main()
{
  Defaulted defaulted{};
}

The resulting transformation was this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
struct UserProvided
{
  inline ~UserProvided() noexcept {}

};

struct Defaulted
{
  inline constexpr ~Defaulted() = default;
};

int main()
{
  Defaulted d = {};
  return 0;
}

The destructor of UserProvided is shown with noexcept, but the destructor of Defaulted isn't. What's interesting, and what was confusing to me, is that down in main, I created an object of Defaulted, and the destructor did still not show up as noexcept

The output changed once Defaulted contained a member requiring destruction, like UserProvided. Below is the result of a transformation with C++ Insights.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
struct UserProvided
{
  inline ~UserProvided() noexcept {}
};

struct Defaulted
{
  UserProvided val{};
  inline ~Defaulted() noexcept = default;
};


int main()
{
  Defaulted defaulted = {{}};
  return 0;
}

As you can see, I added a member val of type UserProvided to Defaulted. I didn't touch the destructor of Defaulted. But in the transformation, the destructor does not carry the noexcept.

That behavior itself isn't totally surprising. At least Clang does only the minimum in a lot of places, saving us compile times. If an object doesn't require a destructor, why border with figuring out the exception specification? I did overlook this delay for quite a while until we talked about exactly such a case in one of my training classes, and I failed to demonstrate with C++ Insights what's going on. My apologies to the participants of this class! I should have known it, but the output of my own app confused med.

I recently added a fix for this case. You will now see a noexcept comment: /* noexcept */ indicating that this destructor is potentially noexcept.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct UserProvided
{
  inline ~UserProvided() noexcept {}
};

struct Defaulted
{
  inline constexpr ~Defaulted() /* noexcept */ = default;
};

int main()
{
  Defaulted defaulted = {};
  return 0;
}

Please let me know the issue you find in C++ Insights!

Support the project

You can support the project by becoming a GitHub Sponsor or, of course, with code contributions.

Andreas