O std :: unique_ptr <T> é necessário para conhecer a definição completa de T?

248

Eu tenho algum código em um cabeçalho que se parece com isso:

#include <memory>

class Thing;

class MyClass
{
    std::unique_ptr< Thing > my_thing;
};

Se eu incluir esse cabeçalho em um cpp que não inclua a Thingdefinição de tipo, ele não será compilado no VS2010-SP1:

1> C: \ Arquivos de Programas (x86) \ Microsoft Visual Studio 10.0 \ VC \ include \ memory (2067): erro C2027: uso do tipo indefinido 'Coisa'

Substitua std::unique_ptrpor std::shared_ptre compila.

Então, acho que é a std::unique_ptrimplementação atual do VS2010 que requer a definição completa e é totalmente dependente da implementação.

Ou é? Há algo em seus requisitos padrão que impossibilita std::unique_ptra implementação de trabalhar apenas com uma declaração de encaminhamento? Parece estranho, pois só deve segurar um ponteiro Thing, não deveria?

Klaim
fonte
20
A melhor explicação de quando você precisa e não precisa de um tipo completo com os ponteiros inteligentes do C ++ 0x é "Tipos incompletos e shared_ptr/ unique_ptr" de Howard Hinnant. A tabela no final deve responder à sua pergunta.
James McNellis
17
Obrigado pelo ponteiro James. Eu tinha esquecido onde coloquei aquela mesa! :-)
Howard Hinnant
5
@JamesMcNellis O link para o site de Howard Hinnant está inoperante. Aqui está a versão web.archive.org . Em qualquer caso, ele atendeu perfeitamente abaixo com o mesmo conteúdo :-)
Ela782
Outra boa explicação é dada no Item 22 do Effective C ++ moderno de Scott Meyers.
Fred Schoen

Respostas:

328

Adotado a partir daqui .

A maioria dos modelos na biblioteca padrão C ++ exige que eles sejam instanciados com tipos completos. No entanto shared_ptre unique_ptrsão exceções parciais . Alguns, mas nem todos os seus membros podem ser instanciados com tipos incompletos. A motivação para isso é oferecer suporte a expressões como pimpl usando ponteiros inteligentes e sem arriscar um comportamento indefinido.

O comportamento indefinido pode ocorrer quando você tem um tipo incompleto e o invoca delete:

class A;
A* a = ...;
delete a;

O código acima é legal. Ele irá compilar. Seu compilador pode ou não emitir um aviso para o código acima, como o acima. Quando executado, coisas ruins provavelmente vão acontecer. Se você tiver muita sorte, seu programa falhará. No entanto, um resultado mais provável é que seu programa vaze memória silenciosamente, pois ~A()não será chamado.

Usar auto_ptr<A>no exemplo acima não ajuda. Você ainda tem o mesmo comportamento indefinido como se tivesse usado um ponteiro bruto.

No entanto, o uso de classes incompletas em certos lugares é muito útil! Aqui é onde shared_ptre unique_ptrajuda. O uso de um desses ponteiros inteligentes permitirá que você continue com um tipo incompleto, exceto onde for necessário ter um tipo completo. E o mais importante, quando é necessário ter um tipo completo, você recebe um erro em tempo de compilação se tentar usar o ponteiro inteligente com um tipo incompleto nesse ponto.

Não há mais comportamento indefinido:

Se seu código for compilado, você utilizou um tipo completo em todos os lugares que precisar.

class A
{
    class impl;
    std::unique_ptr<impl> ptr_;  // ok!

public:
    A();
    ~A();
    // ...
};

shared_ptre unique_ptrexige um tipo completo em lugares diferentes. Os motivos são obscuros, relacionados a um deleter dinâmico versus um deleter estático. As razões precisas não são importantes. De fato, na maioria dos códigos, não é realmente importante que você saiba exatamente onde um tipo completo é necessário. Basta codificar e, se você errar, o compilador lhe dirá.

No entanto, caso seja útil, aqui está uma tabela que documenta vários membros shared_ptre unique_ptrcom relação aos requisitos de integridade. Se o membro exigir um tipo completo, a entrada terá um "C", caso contrário, a entrada da tabela será preenchida com "I".

Complete type requirements for unique_ptr and shared_ptr

                            unique_ptr       shared_ptr
+------------------------+---------------+---------------+
|          P()           |      I        |      I        |
|  default constructor   |               |               |
+------------------------+---------------+---------------+
|      P(const P&)       |     N/A       |      I        |
|    copy constructor    |               |               |
+------------------------+---------------+---------------+
|         P(P&&)         |      I        |      I        |
|    move constructor    |               |               |
+------------------------+---------------+---------------+
|         ~P()           |      C        |      I        |
|       destructor       |               |               |
+------------------------+---------------+---------------+
|         P(A*)          |      I        |      C        |
+------------------------+---------------+---------------+
|  operator=(const P&)   |     N/A       |      I        |
|    copy assignment     |               |               |
+------------------------+---------------+---------------+
|    operator=(P&&)      |      C        |      I        |
|    move assignment     |               |               |
+------------------------+---------------+---------------+
|        reset()         |      C        |      I        |
+------------------------+---------------+---------------+
|       reset(A*)        |      C        |      C        |
+------------------------+---------------+---------------+

Quaisquer operações que requeiram conversões de ponteiro requerem tipos completos para ambos unique_ptre shared_ptr.

O unique_ptr<A>{A*}construtor pode se livrar de um incompleto Aapenas se o compilador não precisar configurar uma chamada para ~unique_ptr<A>(). Por exemplo, se você colocar o unique_ptrheap, poderá se livrar de um incompleto A. Mais detalhes sobre esse ponto podem ser encontrados na resposta de BarryTheHatchet aqui .

Howard Hinnant
fonte
3
Excelente resposta. Eu o daria +5 se pudesse. Tenho certeza de que voltarei a isso no meu próximo projeto, no qual estou tentando fazer pleno uso de ponteiros inteligentes.
Matthias
4
se alguém pode explicar o que significa a mesa que eu acho que vai ajudar mais pessoas
Ghita
8
Mais uma observação: um construtor de classe fará referência aos destruidores de seus membros (no caso em que uma exceção é lançada, esses destruidores precisam ser chamados). Portanto, embora o destruidor de unique_ptr precise de um tipo completo, não é suficiente ter um destruidor definido pelo usuário em uma classe - ele também precisa de um construtor.
Johannes Schaub - litb
7
@ Mehrdad: Esta decisão foi tomada para o C ++ 98, que é antes do meu tempo. No entanto, acredito que a decisão veio de uma preocupação com a implementabilidade e a dificuldade de especificação (ou seja, exatamente quais partes de um contêiner exigem ou não um tipo completo). Ainda hoje, com 15 anos de experiência desde o C ++ 98, seria uma tarefa não trivial relaxar a especificação de contêiner nessa área e garantir que você não proíba importantes técnicas ou otimizações de implementação. Eu acho que poderia ser feito. Eu sei que daria muito trabalho. Estou ciente de uma pessoa fazendo a tentativa.
Howard Hinnant 01/12/13
9
Como não é óbvio pelos comentários acima, para quem tem esse problema porque define a unique_ptrcomo variável de membro de uma classe, apenas declare explicitamente um destruidor (e construtor) na declaração da classe (no arquivo de cabeçalho) e prossiga para defini- los no arquivo de origem (e coloque o cabeçalho com a declaração completa da classe apontada no arquivo de origem) para impedir que o compilador incline automaticamente o construtor ou destruidor no arquivo de cabeçalho (que dispara o erro). stackoverflow.com/a/13414884/368896 também ajuda a me lembrar disso.
Dan Nissenbaum 7/0318
42

O compilador precisa da definição de Thing para gerar o destruidor padrão para MyClass. Se você declarar explicitamente o destruidor e mover sua implementação (vazia) para o arquivo CPP, o código deverá ser compilado.

Igor Nazarenko
fonte
5
Eu acho que esta é a oportunidade perfeita para usar uma função padrão. MyClass::~MyClass() = default;no arquivo de implementação, parece menos provável que seja removido inadvertidamente mais tarde no caminho por alguém que suponha que o corpo do destruidor tenha sido apagado em vez de deliberadamente deixado em branco.
Dennis Zickefoose
@Dennis Zickefoose: Infelizmente, o OP está usando o VC ++, e o VC ++ ainda não suporta membros da classe defaulted ed delete.
Ildjarn
6
+1 para saber como mover a porta para o arquivo .cpp. Também parece MyClass::~MyClass() = defaultque não o move para o arquivo de implementação no Clang. (ainda?)
Eonil
Você também precisa mover a implementação do construtor para o arquivo CPP, pelo menos no VS 2017. Veja, por exemplo, esta resposta: stackoverflow.com/a/27624369/5124002
jciloa
15

Isso não depende da implementação. O motivo pelo qual ele funciona é porque shared_ptrdetermina o destruidor correto a ser chamado no tempo de execução - não faz parte da assinatura do tipo. No entanto, unique_ptro destruidor de faz parte de seu tipo e deve ser conhecido em tempo de compilação.

Cachorro
fonte
8

Parece que as respostas atuais não são exatamente exatamente por que o construtor padrão (ou o destruidor) é um problema, mas as vazias declaradas no cpp não são.

Aqui está o que está acontecendo:

Se a classe externa (ou seja, MyClass) não tiver construtor ou destruidor, o compilador gerará os padrões. O problema é que o compilador essencialmente insere o construtor / destruidor vazio padrão no arquivo .hpp. Isso significa que o código do construtor / destruidor padrão é compilado junto com o binário do executável do host, não com os binários da sua biblioteca. No entanto, essas definições não podem realmente construir as classes parciais. Portanto, quando o vinculador entra no binário da sua biblioteca e tenta obter o construtor / destruidor, ele não encontra nenhum e você recebe um erro. Se o código do construtor / destruidor estava no seu .cpp, o binário da sua biblioteca tem esse disponível para vinculação.

Isso não tem nada a ver com o uso de unique_ptr ou shared_ptr e outras respostas parecem ser possíveis erros confusos no antigo VC ++ para implementação de unique_ptr (o VC ++ 2015 funciona bem na minha máquina).

A moral da história é que seu cabeçalho precisa permanecer livre de qualquer definição de construtor / destruidor. Só pode conter a declaração deles. Por exemplo, ~MyClass()=default;no hpp não funcionará. Se você permitir que o compilador insira o construtor ou destruidor padrão, você receberá um erro no vinculador.

Outra observação: Se você ainda estiver recebendo esse erro mesmo depois de ter o construtor e o destruidor no arquivo cpp, provavelmente o motivo é que sua biblioteca não está sendo compilada corretamente. Por exemplo, uma vez eu simplesmente mudei o tipo de projeto de Console para Biblioteca no VC ++ e recebi esse erro porque o VC ++ não adicionou o símbolo de pré-processador _LIB e produziu exatamente a mesma mensagem de erro.

Shital Shah
fonte
Obrigado! Essa foi uma explicação muito sucinta de uma peculiaridade incrivelmente obscura do C ++. Me salvou um monte de problemas.
JPNotADragon 01/08/19
5

Apenas para completar:

Cabeçalho: Ah

class B; // forward declaration

class A
{
    std::unique_ptr<B> ptr_;  // ok!  
public:
    A();
    ~A();
    // ...
};

Fonte A.cpp:

class B {  ...  }; // class definition

A::A() { ... }
A::~A() { ... }

A definição de classe B deve ser vista pelo construtor, destruidor e qualquer coisa que possa excluir implicitamente B. (Embora o construtor não apareça na lista acima, no VS2017, mesmo o construtor precisa da definição de B. E isso faz sentido ao considerar que, no caso de uma exceção no construtor, o unique_ptr seja destruído novamente.)

Joachim
fonte
1

A definição completa da Coisa é necessária no momento da instanciação do modelo. Esta é a razão exata pela qual o idioma pimpl é compilado.

Se não fosse possível, as pessoas não fariam perguntas como esta .

BЈовић
fonte
-2

A resposta simples é apenas usar shared_ptr.

deltanina
fonte
-7

Quanto a mim,

QList<QSharedPointer<ControllerBase>> controllers;

Basta incluir o cabeçalho ...

#include <QSharedPointer>
Sanbrother
fonte
Resposta não relacionada e não relevante para a pergunta.
Mikus 28/01/19