When is a type considered complete?
Asked Answered
C

1

8

Consider the following code snippet. The destructor of boost::scoped_ptr is invoked at the end of the main function. The destructor uses boost::checked_delete to deallocate the encapsulated Widget pointer.

#include <boost/scoped_ptr.hpp>
#include <iostream>

class Widget;
Widget *make_widget();

int main()
{  
  boost::scoped_ptr<Widget> sp(make_widget());
  // std::cout << sizeof(Widget) << std::endl;
}

class Widget
{
public:
  Widget() {}
  ~Widget() { std::cout << "Widget destructor called." << std::endl; }
};

Widget *make_widget()
{
  return new Widget;
}

I expected this code to fail to compile as the class Widget is incomplete at the point the destructor of scoped_ptr<Widget> is invoked. However this compiles cleanly on g++ 4.8 and Visual Studio 2010. Note the commented statement with the sizeof(Widget) expression in the main function. If I uncomment it, it will fail to compile implying that Widget must be incomplete at that point.

What is the correct explanation for this behavior?

EDIT: Some answers (now deleted) pointed to undefined behavior but I would have expected the use of checked_delete in scoped_ptr's destructor to cause a compilation failure. FWIW, I'm using Boost 1.55.

Claudieclaudina answered 19/7, 2014 at 13:47 Comment(3)
Here is the code in action; much like you I would have expected check_delete to catch the use of an incomplete type so I am pretty surprised this compile with sizeof commented.Lyndy
@MatthieuM.: Got it explained.Briquette
I think the difference lies in whether you write struct SP { ~SP() { depete ptr; } /* ... */ }; or struct SP { ~SP(); /* ... */ };. If the destructor isn't inline, then it doesn't need to be known at the time where it's called, and so the type of ptr is also not needed.Scleroma
B
4

5.3.5 Delete [expr.delete]

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.

Thus, you certainly would expect it to be UB, as Widget::~Widget() is non-trivial, and you would expect the safe-guard in boost to error out.

Now, let's dig higher:

2.2 Phases of translation [lex.phases]

8 Translated translation units and instantiation units are combined as follows: [ Note: ... ] Each translated translation unit is examined to produce a list of required instantiations. [ Note: This may include instantiations which have been explicitly requested (14.7.2). —end note ] The definitions of the required templates are located. It is implementation-defined whether the source of the translation units containing these definitions is required to be available. [ Note: An implementation could encode sufficient information into the translated translation unit so as to ensure the source is not required here. —end note ] All the required instantiations are performed to produce instantiation units. [ Note: These are similar to translated translation units, but contain no references to uninstantiated templates and no template definitions. —end note ] The program is ill-formed if any instantiation fails.

You are saved by phases of translation:
Translate the translation unit, then instantiate templates...

Briquette answered 19/7, 2014 at 13:53 Comment(5)
Why doesn't checked_delete fail? checked_delete works correctly only when the type is complete. It is a safeguard precisely for such situations.Claudieclaudina
Thanks for making me think around those corners.Briquette
So roughly speaking, the instantiations may be deferred till a full scan of the translation unit. For such implementations, defining a forward declared type anywhere in the translation unit is as good as making the definition available everywhere after the forward declaration.Claudieclaudina
This is what I also found by moving the class defs out of the TUPaillette
@CppNoob: Not may. All conforming implementations must defer template instantiation until the end of translation.Briquette

© 2022 - 2024 — McMap. All rights reserved.