3

Please ignore the dubious inheritance pattern from a design point of view. Thanks :)

Consider the following case:

#include <memory>

struct Foo;

struct Bar : std::unique_ptr<Foo> {
    ~Bar();
};

int main() {
    Bar b;
}

In both GCC and Clang, compiling this as an independent TU raises an error:

In instantiation of 'void std::default_delete<_Tp>::operator()(_Tp*) const [with _Tp = Foo]':
  required from 'std::unique_ptr<_Tp, _Dp>::~unique_ptr() [with _Tp = Foo; _Dp = std::default_delete<Foo>]'
error: invalid application of 'sizeof' to incomplete type 'Foo'
  static_assert(sizeof(_Tp)>0,
                      ^

This is GCC's, Clang's one is similar, and both point at the definition of struct Bar. Additionally, adding the missing definitions after main() fixes the error:

// Same as above

struct Foo { };

Bar::~Bar() = default;

It doesn't sound right to me that std::unique_ptr's destructor needs to be instantiated right when defining Bar, since it is only called by Bar's destructor which is defined out-of-line.

I find it even weirder that adding the definitions after everything else , where they shouldn't be reachable, apparently fixes the problem.

Should the first snippet be correct, and if not, why? What's happening in the second one that fixes it?

Quentin
  • 62,093
  • 7
  • 131
  • 191
  • Also declare the constructor: `Bar();` – Kerrek SB Jun 04 '17 at 16:55
  • @KerrekSB ... what the hell :D -- Edit: oooooh, this is about exceptions, isn't it. – Quentin Jun 04 '17 at 16:56
  • Think about it... what would the implied constructor do? At what point would it be defined? – Kerrek SB Jun 04 '17 at 16:57
  • @KerrekSB get called from a more derived class, then propagate an exception that emanated there, and need to destroy the base class. And the implicit default constructor is `inline` and `noexcept(false)`. Wow. – Quentin Jun 04 '17 at 16:58
  • @KerrekSB However, when I declare `Bar` `final` without an explicit default constructor, I still get the error. Is there another case that I'm not thinking about? – Quentin Jun 04 '17 at 17:03
  • It has nothing to do with exceptions, and it's all about the things I said in my previous comment. – Kerrek SB Jun 04 '17 at 18:58
  • @KerrekSB well, the implicit default constructor would be defined somewhere in `Bar`'s definition (I don't recall there be an ordering within the class). It would call `std::unique_ptr`'s default constructor, and apart from exceptions... that's it? I don't see what this has to do with `~unique_ptr` :/ -- Also, the "fix" I showcased still puzzles me. – Quentin Jun 04 '17 at 19:06
  • The base default constructor constructs the deleter, and the deleter's constructor requires the client type to be complete. – Kerrek SB Jun 04 '17 at 19:12
  • @KerrekSB I would have sworn that only the destructor needed `Foo` to be complete, but I guess dumb luck had me always have out-of-line constructors in my classes. What's up with the late definitions getting reeled in though? – Quentin Jun 04 '17 at 19:16
  • Hm, you're right, only the deleter's call operator requires the type to be complete. Something must be instantiating that call operator in your original code, though I'm not sure what. Perhaps it has to do with exceptions after all, since the `Bar` constructor may need to unwind and destroy the unique_ptr base? But I'm not entirely sure. – Kerrek SB Jun 04 '17 at 20:54

1 Answers1

1

See [class.base.init]/12

In a non-delegating constructor, the destructor for each potentially constructed subobject of class type is potentially invoked ([class.dtor]). [Note 5: This provision ensures that destructors can be called for fully-constructed subobjects in case an exception is thrown ([except.ctor]). — end note]

So when you do

Bar b;

then you force the compiler to emit a definition for the implicit default constructor Bar::Bar(), and that means that the destructor for the base class, std::unique_ptr<Foo>, is potentially invoked. That, in turn, causes it to be odr-used, which implies that its definition is required, which causes it to be instantiated, which ultimately instantiates std::default_delete<Foo>::operator()(Foo*), which is ill-formed because Foo is incomplete at that point.

If the constructor of Bar is declared in the class definition (not as = default), but then defined out-of-line at some later point where Foo has been completed, then the problem is fixed.

Note that the issue has nothing to do with whether Bar itself may be further derived from. Bar already derives from std::unique_ptr<Foo> and it is this that causes each constructor of Bar to "potentially invoke" the destructor of std::unique_ptr<Foo>. Now, in some cases, such as the case above, we can tell that a defaulted Bar::Bar() can never fail by throwing an exception, so there is no situation where this defaulted Bar::Bar() actually invokes the destructor of a base class. However, from the point of view of the language specification, it is easier to say that a (non-delegating) constructor always "potentially invokes" each base class destructor than to try to carve out a narrow set of exceptions where it does not do so (and thus does not cause the instantiation of the definitions of those destructors). So that's just how the rules are.

When you add the missing definition of Foo after the point where std::default_delete<Foo>::operator()(Foo*) is instantiated, the result you are seeing has to do with [temp.point]/7:

A specialization for a function template [...] may have multiple points of instantiations within a translation unit, and in addition to the points of instantiation described above,

  • for any such specialization that has a point of instantiation within the declaration-seq of the translation-unit, prior to the private-module-fragment (if any), the point after the declaration-seq of the translation-unit is also considered a point of instantiation, and
  • [...]

If two different points of instantiation give a template specialization different meanings according to the one-definition rule, the program is ill-formed, no diagnostic required.

Since std::default_delete<Foo>::operator()(Foo*) is a function template specialization, it has two points of instantiation: one at the end of main (see [temp.point]/1) and one at the end of the translation unit. The rule is designed to give implementations the freedom to defer instantiation of function templates until the end of the translation unit. If they choose to defer such instantiation, then they will see a complete Foo at that point, and not notice any problem. They are not required to diagnose the fact that Foo was incomplete at an earlier point of instantiation. That's what "ill-formed, no diagnostic required" means.

(Well, one little issue is that it's not totally clear whether the "ill-formed, no diagnostic required" thing actually applies here, since it's not clear whether the instantiations at the two points of instantiation actually produce "different meanings according to the one-definition rule". You have no way of knowing that unless you know the implementation of std::default_delete<Foo>::operator()(Foo*). In my opinion the rule needs to be reworded a bit to reflect the intent better.)

Brian Bi
  • 111,498
  • 10
  • 176
  • 312