Pimpl - Why can make_unique be called on an incomp

2019-02-02 03:59发布

问题:

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 incompete 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 ?

回答1:

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.



回答2:

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.