c++ templates language-lawyer unique-ptr pimpl-idiom

c++ - Pimpl-¿Por qué se puede llamar a make_unique en un tipo incompleto?



templates language-lawyer (2)

¿Por qué se compila la llamada make_unique? ¿Make_unqiue no requiere que su argumento de plantilla sea un tipo completo?

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

La pregunta se originó en mi "problema" con mi implementación de PIMPL :

Entiendo por qué el destructor debe ser declarado y definido por el usuario dentro del archivo cpp para la clase de implementación (PIMPL).

Pero moviendo el constructor de la clase que contiene el pimpl, todavía se compila.

class Object {}; class CachedObjectFactory { public: CachedObjectFactory(); ~CachedObjectFactory(); std::shared_ptr<Object> create(int id) const; private: struct CacheImpl; std::unique_ptr<CacheImpl> pImpl; };

Ahora el archivo cpp:

// 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;

¿Podría alguien explicar por qué esto compila? ¿Por qué hay una diferencia entre la construcción y la destrucción? Si la creación de instancias del destructor y la creación de la instancia de default_deleter es un problema, ¿por qué la creación de instancias de make_unique no es un problema?


La razón por la que esto se compila está aquí en [temp.point]¶8 :

Una especialización para una plantilla de función, una plantilla de función miembro o de una función miembro o miembro de datos estáticos de una plantilla de clase puede tener múltiples puntos de instanciación dentro de una unidad de traducción, y además de los puntos de instanciación descritos anteriormente, para cualquiera de estos La especialización que tiene un punto de instanciación dentro de la unidad de traducción, el final de la unidad de traducción también se considera un punto de instanciación. Una especialización para una plantilla de clase tiene a lo sumo un punto de instanciación dentro de una unidad de traducción [...] Si dos puntos diferentes de instanciación dan a una especialización de plantilla diferentes significados de acuerdo con la regla de una definición, el programa está mal formado, no diagnóstico requerido.

Observe el final de esta cita, ya que lo veremos en la edición siguiente, pero por ahora lo que sucede en la práctica según el fragmento de OP es que el compilador utiliza el punto de make_unique() de instancias adicionalmente considerado de make_unique() que se coloca al final de La unidad de traducción, para que tenga definiciones que faltan en el punto original de uso en el código. Está permitido hacerlo de acuerdo con esta cláusula de la especificación.

Tenga en cuenta que esto ya no compila:

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

Al igual que en, el compilador no se pierde en el punto de creación de instancias, solo lo difiere en términos de qué punto de la unidad de traducción utiliza para generar código para la plantilla.

Edición: Finalmente, parece que aunque tenga estos múltiples puntos de instanciación, no significa que el comportamiento se defina si la definición es diferente entre estos puntos. Observe la última oración en la cita anterior, según la cual esta diferencia se define en la Regla de una definición . Esto se toma directamente de mi comentario a la respuesta de @hvd, quien lo reveló aquí: vea aquí en la Regla de una definición :

Cada programa contendrá exactamente una definición de cada función o variable no en línea que se use odr en ese programa fuera de una declaración descartada; no requiere diagnóstico La definición puede aparecer explícitamente en el programa, se puede encontrar en la biblioteca estándar o definida por el usuario, o ...

Y así, en el caso del OP, obviamente, hay una diferencia entre los dos puntos de instanciación, ya que, como lo señaló @hvd mismo, el primero es de un tipo incompleto, y el segundo no lo es. De hecho, esta diferencia constituye dos definiciones diferentes y, por lo tanto, hay muy pocas dudas de que este programa está mal formado.


make_unique tiene múltiples puntos de instanciación: el final de la unidad de traducción es también un punto de instanciación. Lo que está viendo es que el compilador solo make_unique una instancia de make_unique una vez que CacheImpl / F está completo. Los compiladores tienen permitido hacer esto. Su código está mal formado si confía en él, y los compiladores no están obligados a detectar el error.

14.6.4.1 Punto de instanciación [temp.point]

8 Una especialización para una plantilla de función, una plantilla de función miembro o de una función miembro o miembro de datos estáticos de una plantilla de clase puede tener múltiples puntos de instanciación dentro de una unidad de traducción, y además de los puntos de instanciación descritos anteriormente, para cualquier tal especialización que tiene un punto de instanciación dentro de la unidad de traducción, el final de la unidad de traducción también se considera un punto de instanciación. [...] Si dos puntos diferentes de instanciación dan a una especialización de plantilla diferentes significados de acuerdo con la regla de una definición (3.2), el programa está mal formado, no se requiere diagnóstico.