Weird enum in destructor

Currently, I am reading the source code of Protocol Buffer, and I found one weird enum codes defined here

  ~scoped_ptr() {
enum { type_must_be_complete = sizeof(C) };
delete ptr_;
}


void reset(C* p = NULL) {
if (p != ptr_) {
enum { type_must_be_complete = sizeof(C) };
delete ptr_;
ptr_ = p;
}
}

Why the enum { type_must_be_complete = sizeof(C) }; is defined here? what is it used for?

3935 次浏览

sizeof(C) will fail at compile time if C is not a complete type. Setting a local scope enum to it makes the statement benign at runtime.

It's a way of the programmer protecting themselves from themselves: the behaviour of a subsequent delete ptr_ on an incomplete type is undefined if it has a non-trivial destructor.

This trick avoids UB by ensuring that definition of C is available when this destructor is compiled. Otherwise the compilation would fail as the sizeof incomplete type (forward declared types) can not be determined but the pointers can be used.

In compiled binary, this code would be optimized out and would have no effect.

Note that: Deleting incomplete type may be undefined behavior from 5.3.5/5:.

if the object being deleted has incomplete class type at the point of deletion and the complete class has a non-trivial destructor or a deallocation function, the behavior is undefined.

g++ even issues the following warning:

warning: possible problem detected in invocation of delete operator:
warning: 'p' has incomplete type
warning: forward declaration of 'struct C'

Maybe a trick to be sure C is defined.

To understand the enum, start with considering the destructor without it:

~scoped_ptr() {
delete ptr_;
}

where ptr_ is a C*. If type C is incomplete at this point, i.e. all that the compiler knows is struct C;, then (1)a default-generated do-nothing destructor is used for the C instance pointed to. That's unlikely to be the right thing to do for an object managed by a smart pointer.

If deleting via a pointer to incomplete type had always had Undefined Behavior, then the standard could just require the compiler to diagnose it and fail. But it's well-defined when the real destructor is trivial: knowledge that the programmer can have, but the compiler doesn't have. Why the language defines and allows this is beyond me, but C++ supports many practices that today are not regarded as best practices.

A complete type has a known size, and hence, sizeof(C) will compile if and only if C is a complete type -- with known destructor. So it can be used as a guard. One way would be simply

(void) sizeof(C);  // Type must be complete

I would guess that with some compiler and options the compiler optimizes it away before it could notice that it shouldn't compile, and that the enum is a way to avoid such non-conforming compiler behavior:

enum { type_must_be_complete = sizeof(C) };

An alternative explanation for the choice of enum rather than just a discarded expression, is simply personal preference.

Or as James T. Hugget suggests in a comment to this answer, “The enum may be a way of creating a pseudo-portable error message at compile time”.


(1) The default-generated do-nothing destructor for an incomplete type was a problem with old std::auto_ptr. It was so insidious that it made its way into a GOTW item about the PIMPL idiom, written by the chair of the international C++ standardization committee Herb Sutter. Of course, nowadays that std::auto_ptr is deprecated, one will instead use some other mechanism.