15

Code:

#include <memory>

struct Data;
std::unique_ptr<Data> make_me();

int main()
{
    std::unique_ptr<Data> m = make_me();
    return 0;
}

Which of course fails:

In file included from <source>:1:
In file included from /opt/compiler-explorer/gcc-7.1.0/lib/gcc/x86_64-linux-gnu/7.1.0/../../../../include/c++/7.1.0/memory:80:
/opt/compiler-explorer/gcc-7.1.0/include/c++/7.1.0/bits/unique_ptr.h:76:16: error: invalid application of 'sizeof' to an incomplete type 'Data'
        static_assert(sizeof(_Tp)>0,
                      ^~~~~~~~~~~
/opt/compiler-explorer/gcc-7.1.0/include/c++/7.1.0/bits/unique_ptr.h:268:4: note: in instantiation of member function 'std::default_delete<Data>::operator()' requested here
          get_deleter()(__ptr);
          ^
8 : <source>:8:31: note: in instantiation of member function 'std::unique_ptr<Data, std::default_delete<Data> >::~unique_ptr' requested here
    std::unique_ptr<Data> m = make_me();
                              ^
3 : <source>:3:8: note: forward declaration of 'Data'
struct Data;
       ^
1 error generated.
Compiler returned: 1

But adding below line at the end of above code compiles fine:

struct Data {};

My question is why this code compiles and works when Data is declared after point of instantiation of std::unique_ptr? Seemingly, both cases should fail with the same/similar error..

Whole example on godbolt: https://godbolt.org/g/FQqxwN

Vittorio Romeo
  • 90,666
  • 33
  • 258
  • 416
Michał Łoś
  • 633
  • 4
  • 15
  • This issue is explored in closely related question https://stackoverflow.com/q/8595471/103167 (Not a duplicate, because mine only addresses whether the code is correct, not whether it compiles) – Ben Voigt Dec 21 '17 at 20:19

2 Answers2

11

This works because the point of instantiation of a template is located after the definition of Data. From the Standard:

[temp.point]

For a class template specialization, a class member template specialization, or a specialization for a class member of a class template, if the specialization is implicitly instantiated because it is referenced from within another template specialization, if the context from which the specialization is referenced depends on a template parameter, and if the specialization is not instantiated previous to the instantiation of the enclosing template, the point of instantiation is immediately before the point of instantiation of the enclosing template. Otherwise, the point of instantiation for such a specialization immediately precedes the namespace scope declaration or definition that refers to the specialization.

A specialization for a function template, a member function template, or of a member function or static data member of a class 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 translation unit, the end of the translation unit is also considered a point of instantiation. A specialization for a class template has at most one point of instantiation within a translation unit. A specialization for any template may have points of instantiation in multiple translation units. 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.

Note that this is probably ill-formed (NDR) due to the last sentence in the quote. I am not confident enough to tell whether this is certainly ill-formed or not.

Vittorio Romeo
  • 90,666
  • 33
  • 258
  • 416
  • 2
    So, given the last sentence in your quote, doesn't that mean that constructor of std::unique_ptr has two points of instantiation (one in main(), second one at the end of translation unit), which differ in "meaning", which means that the program is ill-formed? – Michał Łoś Dec 21 '17 at 17:07
  • @MichałŁoś: that is indeed a possibility. I am honestly not sure if that's the case here. *Summoning language laywers...* – Vittorio Romeo Dec 21 '17 at 17:12
  • I think this *happens* to work, because the point of instantiation of the template happens to be the one at which the struct has a complete definition. But if the compiler decided to chose the other one, the outcome of the compilation would be different, which means that the last sentence of the quote is in effect. This is like the limerick, only with an implicit instantiation point at the end of the TU, which is something I've only learned after Michał has shown me this example earlier today. – Griwes Dec 21 '17 at 17:18
  • 3
    Bottom line: I am pretty sure this is ill-formed, NDR, because the instantiations would be different, and only happens to compile because of a compiler implementation strategy. Remember that "do what the user intended" is a valid outcome of UB (and "ill-formed, NDR" is virtually just an alias for "the behavior is undefined"). – Griwes Dec 21 '17 at 17:19
  • 2
    Yep, this is ill-formed NDR. – T.C. Dec 22 '17 at 09:21
  • @Griwes: Just for the record: this behaviour rely neither on destructors nor on main, it's more general, as specification suggests: https://godbolt.org/g/1QzYur – Michał Łoś Dec 22 '17 at 10:05
6

If you read a little closer, the problem is with the deletion of the contained Data object. The

get_deleter()(__ptr)

part is the big hint.

What happens here is that the unique pointer object m goes out of scope at the end of the main function, so the data pointed to needs to be deleted. However, since there is no destructor the default deleter can not handle it.

To solve it you can add a definition of the structure, which will make it defined and the default deleter will be able to know the type. Or you could add a new deleter for the pointer, one which (in this case) might do nothing:

auto null_deleter = [](Data*){ /* Do nothing */ };
...
std::unique_ptr<Data, decltype(null_deleter)> m = make_me();

Of course, if you want to actually delete the data, then either define the structure or modify the deleter so it delete the pointer (which makes it need the full structure definition anyway, but then the deleter could be defined in another tranaslation-unit, probably the same where make_me is defined).

Some programmer dude
  • 400,186
  • 35
  • 402
  • 621
  • 2
    While your comment is valuable, it's not the answer for my question: I was asking why this code **compiles** after adding structure definition at the end, rather than **how to make it compile** : ). – Michał Łoś Dec 21 '17 at 16:35