Pimpl - Why can make_unique be called on an incomplete type
Asked Answered
R

2

26

Why does the make_unique call compile? Doesn't make_unqiue require its template argument to be a complete type ?

struct F;
int main()
{
  std::make_unique<F>();
}

struct F {};

The question orignated from my "problem" with my PIMPL implementation:

I do understand why the destructor has to be user declared and defined inside the cpp file for the Implementation class (PIMPL).

But moving the constructor of the class containing the pimpl- still compiles.

class Object
{};

class CachedObjectFactory
{
public:
  CachedObjectFactory();

  ~CachedObjectFactory();
  std::shared_ptr<Object> create(int id) const;

private:
  struct CacheImpl;
  std::unique_ptr<CacheImpl> pImpl;
};

Now the cpp file:

// constructor with make_unique on incomplete type ?
CachedObjectFactory::CachedObjectFactory()
  : pImpl(std::make_unique<CacheImpl>())
{}

struct CachedObjectFactory::CacheImpl
{
  std::map<int, std::shared_ptr<Object>> idToObjects;
};

//deferred destructor
CachedObjectFactory::~CachedObjectFactory() = default;

Could someone explain why this compiles? Why is there a difference between construction and destruction? If the instantiation of the destructor and the instantiation of the default_deleter is a problem why is the instantiation of make_unique not a problem ?

Roybal answered 5/9, 2018 at 8:38 Comment(7)
You really don't include the header that defines CacheImpl in the .cpp file? I doubt that this compiles, for creation as well as desctrution, the type of CacheImpl must be known.Lefty
@Lefty - Believe it. It even builds if we put the nested struct last after everything coliru.stacked-crooked.com/a/e5f31d5306834b25Unreeve
sorry i kept the example short of course did include the defintion of my cachedobjectfactoryRoybal
@Lefty Here's a smaller example that also compiles: struct F; int main() { std::make_unique<F>(); } struct F{};Christopher
See https://mcmap.net/q/64710/-is-std-unique_ptr-lt-t-gt-required-to-know-the-full-definition-of-t for a table of which members of unique_ptr require a complete type.Declinometer
@Christopher Note this no longer compiles: struct Foo; int main(){ std::make_unique<Foo>(); } struct Foo { ~Foo() = delete; }; I've made it part of my answer, thanks for the shorthand! : )Vallonia
@AF_cpp, you could remove all the first part of your question and only keep the mcve (starting with Or even simpler). It would improve the question.Progressionist
V
13

The reason for why this compiles is here in [temp.point]¶8:

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 [...] 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.

Do notice the ending of this quote, as we will get to it in the edit below, but for now what happens in practice per the OP's snippet is the compiler uses the additionally considered instantiation point of make_unique() that is placed at the end of the translation unit, so that it will have definitions that are missing at the original point of usage in the code. It is allowed to do so according to this clause from the spec.

Note this no longer compiles:

struct Foo; int main(){ std::make_unique<Foo>(); } struct Foo { ~Foo() = delete; };

As in, the compiler doesn't miss on the point of instantiation, it only defers it in terms of which point in the translation unit it uses to generate code for the template.


Edit: Finally it seems that even though you have these multiple instantiation points, it doesn't mean that the behavior is defined if the definition is different between these points. Note the last sentence in the above quote, according to which this difference is defined by the One Definition Rule. This is taken straight from my comment to the answer by @hvd, who brought this to light here: See here in the One Definition Rule:

Every program shall contain exactly one definition of every non-inline function or variable that is odr-used in that program outside of a discarded statement; no diagnostic required. The definition can appear explicitly in the program, it can be found in the standard or a user-defined library, or ...

And so in the OP's case, the're is obviously a difference between the two instantiation points, in that, as @hvd himself noted, the first one is of an incomplete type, and the second one isn't. Indeed, this difference constitutes two different definitions and so there's very little doubt this program is ill-formed.

Vallonia answered 5/9, 2018 at 9:15 Comment(4)
In other words, the standard mandates implementations to make this work, for magic.Decapolis
@LightnessRacesinOrbit: if hvd's answer is right, then it's quite the contrary. OP's program is ill-formed, shouldn't be written this way. Another compiler may not accept it.Balthazar
@Balthazar how about this new edit? I thought about it and figured this is a phrasing I can totally live with. Let me know if you think there's anything else missing.Vallonia
@SkepticalEmpiricist: yes, now your answer basically says the same thing as hvd's (and better explained). One question remains (at least, it's not clear for me): the explanation of why OP's code contains multiple definitions. Would make_unique<F> have multiple definitions, if make_unique were an empty function (this case, F's definition doesn't matter)?Balthazar
W
16

make_unique has multiple points of instantiation: the end of the translation unit is also a point of instantiation. What you are seeing is the compiler only instantiating make_unique once CacheImpl/F is complete. Compilers are allowed to do this. Your code is ill-formed if you rely on it, and compilers are not required to detect the error.

14.6.4.1 Point of instantiation [temp.point]

8 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. [...] If two different points of instantiation give a template specialization different meanings according to the one definition rule (3.2), the program is ill-formed, no diagnostic required.

Wimbush answered 5/9, 2018 at 9:14 Comment(19)
plz 2 see comments on SkepticalEmpiricist's answer and provide el secondo opinionDecapolis
@LightnessRacesinOrbit You started with "But these two different points of instantiation do not give the template specialization different meanings according to the one definition rule." -- At the first point of instantiation, the template specialisation is ill-formed due to the invalid use of an incomplete type, and at the second, it is well-formed, right? Are you saying that that effectively requires compilers to use the second point of instantiation, or am I misunderstanding you?Wimbush
@hvd: I've asked a question about this: #52185964Balthazar
And additionally undefined by [res.on.functions]/2.5.Parsimony
I'm sorry, might be lagging behind here, or not, but what does if you rely on it exactly means in Your code is ill-formed if you rely on it?Vallonia
@Balthazar I appreciate the contribution here very much indeed, but unfortunately as of now, after going over your off-shoot question, I'm still not quite sure what is the additional information given here. I'll be glad to get some help on this one as you seem to have delved right into the heart of the ill-formed-ness issue here more then anyone else, or at least more than I.Vallonia
@SkepticalEmpiricist: I put that comment here, because I supposed that hvd is right, and it seemed right. Note, that my off-shoot question is a little-bit different (because it is about class templates, not function templates - not intentionally, I didn't think that matters, but as it seems, it does). So I need to investigate this further. Further note: I've never seen T.C. being wrong. Yet T.C. says "And additionally undefined...". The additional information is that OP's code is ill-formed. If you think otherwise, please edit your question, and describe it why it is not ill-formed.Balthazar
@SkepticalEmpiricist: of course, if it turns out, that it is not ill-formed, I'll delete that comment, and message to OP to accept your answer instead.Balthazar
@Balthazar You sound apologetic but I have no beef with you : ) this is quite the legitimate concern you had there. Question: Are you treating UB and ill-formed as the same thing?Vallonia
@Balthazar Note the exact phrasing of T.C's excellent linked clause which you are referring to: "the effects are undefined in the following cases ... if an incomplete type ([basic.types]) is used as a template argument when instantiating a template component or evaluating a concept, unless specifically allowed for that component." -- But the whole point of the answer to this question was that the instantiation itself (not for a class template, indeed) is being "delayed" to when the type is no longer incomplete. Am I seeing this right?Vallonia
@Balthazar but you quote T.C's link and yet claim hvd's answer has the additional information. One is saying UB and the other ill-formed. Am I missing anything?Vallonia
@SkepticalEmpiricist: I mean (maybe I misunderstand T.C.'s words though), that res.on.functions/2.5 makes OP's code undefined additionally. So it is ill-formed (reasons described by hvd's answer), and additionally UB.Balthazar
@SkepticalEmpiricist Keep in mind the difference between ill-formed and undefined behaviour is that the former requires a diagnostic. "Ill-formed, no diagnostic required" is effectively the same thing as UB. See #22180812Wimbush
@hvd Question: When you write about the ill-formed-ness in this answer, did you mean that it arises due to this sentence?Vallonia
@hvd OH about the no diagnostic required -- I actually never encountered this particular juxtaposition: "ill-formed; no diagnostic required" and "undefined behavior" are the same thing. So you say that T.C's comment regarding this is the cause of the same ill-formed (no diagnostics...) you were referring to?Vallonia
@SkepticalEmpiricist No, I meant the ill-formedness arises due to "If two different points of instantiation give a template specialization different meanings according to the one definition rule (3.2), the program is ill-formed, no diagnostic required." I may have been wrong about that though: it may be that that is only about user-provided templates, not standard library templates, and that T.C.'s comment would actually make for a better answer.Wimbush
@hvd I'm not yet sure you were wrong: See here in the ODR: "Every program shall contain exactly one definition of every non-inline function or variable that is odr-used in that program outside of a discarded statement; no diagnostic required. The definition can appear explicitly in the program, it can be found in the standard or a user-defined library, or ...". So I think what T.C. has brought might really just be an addition, like he himself stated.Vallonia
If eventually this is right then of course I'll add an edit to my original answer : )Vallonia
@SkepticalEmpiricist Thanks, I missed that.Wimbush
V
13

The reason for why this compiles is here in [temp.point]¶8:

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 [...] 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.

Do notice the ending of this quote, as we will get to it in the edit below, but for now what happens in practice per the OP's snippet is the compiler uses the additionally considered instantiation point of make_unique() that is placed at the end of the translation unit, so that it will have definitions that are missing at the original point of usage in the code. It is allowed to do so according to this clause from the spec.

Note this no longer compiles:

struct Foo; int main(){ std::make_unique<Foo>(); } struct Foo { ~Foo() = delete; };

As in, the compiler doesn't miss on the point of instantiation, it only defers it in terms of which point in the translation unit it uses to generate code for the template.


Edit: Finally it seems that even though you have these multiple instantiation points, it doesn't mean that the behavior is defined if the definition is different between these points. Note the last sentence in the above quote, according to which this difference is defined by the One Definition Rule. This is taken straight from my comment to the answer by @hvd, who brought this to light here: See here in the One Definition Rule:

Every program shall contain exactly one definition of every non-inline function or variable that is odr-used in that program outside of a discarded statement; no diagnostic required. The definition can appear explicitly in the program, it can be found in the standard or a user-defined library, or ...

And so in the OP's case, the're is obviously a difference between the two instantiation points, in that, as @hvd himself noted, the first one is of an incomplete type, and the second one isn't. Indeed, this difference constitutes two different definitions and so there's very little doubt this program is ill-formed.

Vallonia answered 5/9, 2018 at 9:15 Comment(4)
In other words, the standard mandates implementations to make this work, for magic.Decapolis
@LightnessRacesinOrbit: if hvd's answer is right, then it's quite the contrary. OP's program is ill-formed, shouldn't be written this way. Another compiler may not accept it.Balthazar
@Balthazar how about this new edit? I thought about it and figured this is a phrasing I can totally live with. Let me know if you think there's anything else missing.Vallonia
@SkepticalEmpiricist: yes, now your answer basically says the same thing as hvd's (and better explained). One question remains (at least, it's not clear for me): the explanation of why OP's code contains multiple definitions. Would make_unique<F> have multiple definitions, if make_unique were an empty function (this case, F's definition doesn't matter)?Balthazar

© 2022 - 2024 — McMap. All rights reserved.