std :: unique_ptr com um tipo incompleto não será compilado

203

Estou usando o pimpl-idiom com std::unique_ptr:

class window {
  window(const rectangle& rect);

private:
  class window_impl; // defined elsewhere
  std::unique_ptr<window_impl> impl_; // won't compile
};

No entanto, recebo um erro de compilação sobre o uso de um tipo incompleto, na linha 304 em <memory>:

Aplicativo inválido de ' sizeof' para um tipo incompleto ' uixx::window::window_impl'

Até onde eu sei, std::unique_ptrdeve poder ser usado com um tipo incompleto. Isso é um bug no libc ++ ou estou fazendo algo errado aqui?


fonte
Link de referência para os requisitos de completude: stackoverflow.com/a/6089065/576911
Howard Hinnant
1
Um pimpl é frequentemente construído e não modificado desde então. Eu normalmente uso um std :: shared_ptr <const window_impl>
mfnx
Relacionado: Gostaria muito de saber por que isso funciona no MSVC e como impedir que ele funcione (para que não quebre as compilações dos meus colegas do GCC).
Len

Respostas:

258

Aqui estão alguns exemplos de std::unique_ptrtipos incompletos. O problema está na destruição.

Se você usa o pimpl com unique_ptr, precisa declarar um destruidor:

class foo
{ 
    class impl;
    std::unique_ptr<impl> impl_;

public:
    foo(); // You may need a def. constructor to be defined elsewhere

    ~foo(); // Implement (with {}, or with = default;) where impl is complete
};

porque, caso contrário, o compilador gera um padrão e precisa de uma declaração completa de foo::impl para isso.

Se você tem construtores de modelos, está ferrado, mesmo que não construa o impl_membro:

template <typename T>
foo::foo(T bar) 
{
    // Here the compiler needs to know how to
    // destroy impl_ in case an exception is
    // thrown !
}

No escopo do espaço para nome, o uso unique_ptrnão funcionará:

class impl;
std::unique_ptr<impl> impl_;

já que o compilador deve saber aqui como destruir esse objeto de duração estática. Uma solução alternativa é:

class impl;
struct ptr_impl : std::unique_ptr<impl>
{
    ~ptr_impl(); // Implement (empty body) elsewhere
} impl_;
Alexandre C.
fonte
3
Acho que sua primeira solução (adicionar o foo destructor) permite que a própria declaração de classe seja compilada, mas declarar um objeto desse tipo em qualquer lugar resulta no erro original ("aplicação inválida de 'sizeof' ...").
precisa saber é o seguinte
38
excelente resposta, apenas para notar; ainda podemos usar o construtor / destruidor padrão colocando, por exemplo, foo::~foo() = default;no arquivo src
assem
2
Uma maneira de conviver com os construtores de modelos seria declarar, mas não definir o construtor no corpo da classe, defini-lo em algum lugar em que a definição completa do implemento seja vista e instanciar explicitamente todas as instanciações necessárias.
31415 enobayram
2
Você poderia explicar como isso funcionaria em alguns casos e em outros? Eu tenho usado o idioma pimpl com um unique_ptr e uma classe sem destrutor, e em outro projeto meu código não compilar com a OP de erro mencionada ..
Curious
1
Parece que se o valor padrão para unique_ptr estiver definido como {nullptr} no arquivo de cabeçalho da classe com o estilo c ++ 11, uma declaração completa também será necessária pelo motivo acima.
Feirainy 11/04/19
53

Como Alexandre C. mencionou, o problema se resume ao windowdestruidor de ser implicitamente definido em locais onde o tipo dewindow_impl ainda é incompleto. Além de suas soluções, outra solução alternativa que usei é declarar um functor Deleter no cabeçalho:

// Foo.h

class FooImpl;
struct FooImplDeleter
{
  void operator()(FooImpl *p);
};

class Foo
{
...
private:
  std::unique_ptr<FooImpl, FooImplDeleter> impl_;
};

// Foo.cpp

...
void FooImplDeleter::operator()(FooImpl *p)
{
  delete p;
}

Observe que o uso de uma função Deleter personalizada impede o uso de std::make_unique(disponível no C ++ 14), conforme já discutido aqui .

Fernando Costa Bertoldi
fonte
6
Esta é a solução correta para mim. Não é exclusivo para usar o pimpl-idiom, é um problema geral com o uso de std :: unique_ptr com classes incompletas. O deleter padrão usado por std :: unique_ptr <X> tenta "excluir X", o que não pode ser feito se X for uma declaração direta. Ao especificar uma função deleter, você pode colocar essa função em um arquivo de origem em que a classe X esteja completamente definida. Outros arquivos de origem podem usar std :: unique_ptr <X, DeleterFunc> mesmo que X seja apenas uma declaração de encaminhamento, desde que estejam vinculados ao arquivo de origem que contém DeleterFunc.
sheltond
1
Essa é uma boa solução alternativa quando você deve ter uma definição de função embutida criando uma instância do seu tipo "Foo" (por exemplo, um método estático "getInstance" que faça referência ao construtor e destruidor) e você não deseja movê-las para um arquivo de implementação como @ adspx5 sugere.
GameSalutes
20

use um deleter personalizado

O problema é que unique_ptr<T>deve chamar o destruidor T::~T()em seu próprio destruidor, seu operador de atribuição de movimentação e unique_ptr::reset()função de membro (apenas). No entanto, eles devem ser chamados (implícita ou explicitamente) em várias situações do PIMPL (já no destrutor da classe externa e no operador de atribuição de movimentação).

Como já apontado em outra resposta, uma forma de evitar que é mover todas as operações que requerem unique_ptr::~unique_ptr(), unique_ptr::operator=(unique_ptr&&)e unique_ptr::reset()para o arquivo de origem onde a classe pimpl helper é realmente definido.

No entanto, isso é bastante inconveniente e desafia o próprio sentido da cafeteira idoim até certo ponto. Uma solução muito mais limpa que evita tudo o que é usar um deleter personalizado e apenas move sua definição para o arquivo de origem onde vive a classe auxiliar de espinhas. Aqui está um exemplo simples:

// file.h
class foo
{
  struct pimpl;
  struct pimpl_deleter { void operator()(pimpl*) const; };
  std::unique_ptr<pimpl,pimpl_deleter> m_pimpl;
public:
  foo(some data);
  foo(foo&&) = default;             // no need to define this in file.cc
  foo&operator=(foo&&) = default;   // no need to define this in file.cc
//foo::~foo()          auto-generated: no need to define this in file.cc
};

// file.cc
struct foo::pimpl
{
  // lots of complicated code
};
void foo::pimpl_deleter::operator()(foo::pimpl*ptr) const { delete ptr; }

Em vez de uma classe deleter separada, você também pode usar uma função livre ou staticmembro fooem conjunto com uma lambda:

class foo {
  struct pimpl;
  static void delete_pimpl(pimpl*);
  std::unique_ptr<pimpl,[](pimpl*ptr){delete_pimpl(ptr);}> m_pimpl;
};
Walter
fonte
15

Provavelmente você tem alguns corpos de função no arquivo .h na classe que usa o tipo incompleto.

Verifique se na janela .h da classe você tem apenas declaração de função. Todos os corpos de função da janela devem estar no arquivo .cpp. E para window_impl também ...

Btw, você precisa adicionar explicitamente a declaração de destruidor para a classe windows no seu arquivo .h.

Mas você NÃO PODE colocar o corpo vazio do dtor no seu arquivo de cabeçalho:

class window {
    virtual ~window() {};
  }

Deve ser apenas uma declaração:

  class window {
    virtual ~window();
  }
adspx5
fonte
Esta foi a minha solução também. Muito mais conciso. Apenas tenha seu construtor / destruidor declarado no cabeçalho e definido no arquivo cpp.
Kris Morness 14/09/19
2

Para adicionar às respostas dos outros sobre o deleter personalizado, em nossa "biblioteca de utilitários" interna, adicionei um cabeçalho auxiliar para implementar esse padrão comum ( std::unique_ptrde um tipo incompleto, conhecido apenas por algumas das TU por exemplo, para evitar longos tempos de compilação ou fornecer apenas um identificador opaco para os clientes).

Ele fornece o andaime comum para esse padrão: uma classe deleter personalizada que chama uma função deleter definida externamente, um alias de tipo para a unique_ptrcom essa classe deleter e uma macro para declarar a função deleter em uma TU que possui uma definição completa do tipo. Eu acho que isso tem alguma utilidade geral, então aqui está:

#ifndef CZU_UNIQUE_OPAQUE_HPP
#define CZU_UNIQUE_OPAQUE_HPP
#include <memory>

/**
    Helper to define a `std::unique_ptr` that works just with a forward
    declaration

    The "regular" `std::unique_ptr<T>` requires the full definition of `T` to be
    available, as it has to emit calls to `delete` in every TU that may use it.

    A workaround to this problem is to have a `std::unique_ptr` with a custom
    deleter, which is defined in a TU that knows the full definition of `T`.

    This header standardizes and generalizes this trick. The usage is quite
    simple:

    - everywhere you would have used `std::unique_ptr<T>`, use
      `czu::unique_opaque<T>`; it will work just fine with `T` being a forward
      declaration;
    - in a TU that knows the full definition of `T`, at top level invoke the
      macro `CZU_DEFINE_OPAQUE_DELETER`; it will define the custom deleter used
      by `czu::unique_opaque<T>`
*/

namespace czu {
template<typename T>
struct opaque_deleter {
    void operator()(T *it) {
        void opaque_deleter_hook(T *);
        opaque_deleter_hook(it);
    }
};

template<typename T>
using unique_opaque = std::unique_ptr<T, opaque_deleter<T>>;
}

/// Call at top level in a C++ file to enable type %T to be used in an %unique_opaque<T>
#define CZU_DEFINE_OPAQUE_DELETER(T) namespace czu { void opaque_deleter_hook(T *it) { delete it; } }

#endif
Matteo Italia
fonte
1

Pode não ser a melhor solução, mas às vezes você pode usar shared_ptr . Se é claro que é um pouco exagerado, mas ... quanto ao unique_ptr, talvez eu espere mais 10 anos até que os fabricantes de padrões C ++ decidam usar o lambda como um deleter.

Outro lado. Pelo seu código, pode acontecer que, no estágio de destruição, window_impl esteja incompleto. Isso pode ser uma razão de comportamento indefinido. Veja o seguinte: por que, na verdade, excluir um tipo incompleto é um comportamento indefinido?

Então, se possível, eu definiria um objeto muito básico para todos os seus objetos, com destruidor virtual. E você é quase bom. Você deve ter em mente que o sistema chamará destruidor virtual para o seu ponteiro; portanto, você deve defini-lo para todos os ancestrais. Você também deve definir a classe base na seção de herança como virtual (consulte isso para obter detalhes).

Stepan Dyatkovskiy
fonte