Bom padrão de design para um wrapper c ++ em torno de um objeto ac

8

Eu escrevi um wrapper c ++ extensível em torno de uma biblioteca c muito difícil de usar, mas também muito útil. O objetivo é ter a conveniência de c ++ para alocar o objeto, expor suas propriedades, desalocar o objeto, copiar semântica, etc ...

O problema é o seguinte: às vezes a biblioteca c deseja o objeto subjacente (um ponteiro para o objeto) e o destruidor de classe não deve destruir a memória subjacente. Enquanto na maioria das vezes, o destruidor deve desalocar o objeto subjacente. Eu experimentei definir um bool hasOwnershipsinalizador na classe para que o destruidor, o operador de atribuição, etc ... saiba se deve ou não liberar a memória subjacente. No entanto, isso é complicado para o usuário e, também, às vezes não há como saber quando outro processo estará usando essa memória.

Atualmente, eu tenho a configuração onde, quando a atribuição vem de um ponteiro do mesmo tipo que o tipo subjacente, defino o sinalizador hasOwnership. Eu faço o mesmo quando o construtor sobrecarregado é chamado usando o ponteiro da biblioteca c. No entanto, isso ainda não lida com o caso em que o usuário criou o objeto e o passou para uma das minhas funções que chama c_api e a biblioteca armazena o ponteiro para uso posterior. Se eles fossem excluir seu objeto, sem dúvida causaria um segfault na biblioteca c.

Existe um padrão de design que simplifique esse processo? Talvez algum tipo de contagem de referência?

Jonathan Henson
fonte
você parece não ter qualquer pensamento sobre a segurança de uma discussão desses objetos ...
aberração catraca
@ratchetfreak Tudo o que o wrapper faz é chamar o acessador / mutadores, garantir boa semântica de cópia, lidar com memória, listas de cópias, etc ... Na verdade, não toco diretamente os dados. A biblioteca c já é segura para threads. Além disso, meus usuários terão acesso apenas aos meus wrappers de forma síncrona com o sistema de retornos de chamada e assim por diante. Onde necessário, tenho bloqueios nas leituras / gravações de qualquer dado.
Jonathan Henson
2
unique_ptrna maioria dos casos, já pode lidar com esse tipo de recursos, portanto, você não precisa implementar o gerenciamento de recursos. unique_ptrusa o releasemétodo para abandonar a propriedade do objeto armazenado.
Philipp

Respostas:

4

Se a responsabilidade de limpar as coisas alocadas dinamicamente mudar entre sua classe de wrapper e a biblioteca C com base em como as coisas são usadas, você está lidando com uma biblioteca C mal projetada ou está tentando fazer muito em sua classe de wrapper.

No primeiro caso, tudo o que você pode fazer é acompanhar quem é responsável pela limpeza e esperar que nenhum erro seja cometido (por você ou pelos mantenedores da biblioteca C).

No segundo caso, você deve repensar o design do seu invólucro. Toda a funcionalidade pertence à mesma classe ou pode ser dividida em várias classes. Talvez a biblioteca C use algo semelhante ao padrão de design do Facade e você deve manter uma estrutura semelhante no seu wrapper C ++.

De qualquer forma, mesmo que a biblioteca C seja responsável por limpar algumas coisas, não há nada errado em manter uma referência / ponteiro para essas coisas. Você só precisa se lembrar de que não é responsável por limpar a que o ponteiro se refere.

Bart van Ingen Schenau
fonte
E se eu garantisse que um clone foi feito do objeto subjacente quando ele é passado para a lib. Então eu posso liberar minha memória como eu quiser e isso não afetará nada. No entanto, não receberei alterações no objeto de volta na minha classe.
Jonathan Henson
E sim, algumas práticas muito ruins foram usadas nesta lib. Por exemplo: em vez de o usuário passar um const char * para uma função que define uma string em uma estrutura e depois fazer uma cópia para armazenar na estrutura, eles esperam que o usuário aloque a string rapidamente e passe um char * que apenas armazena sem fazer uma cópia. Essa é uma das principais razões pelas quais criei um invólucro - para garantir que a semântica adequada da cópia fosse seguida.
Jonathan Henson
@ JonathanHenson: Deixar o cliente alocar algo e passar a propriedade / responsabilidade para a biblioteca é uma técnica de otimização comum para evitar cópias excessivas e totalmente válidas para as bibliotecas C (desde que sejam feitas de forma consistente). Mas, de fato, significa que, quando usado no C ++, o usuário pode precisar fazer cópias adicionais para estar em conformidade com os requisitos de interface da biblioteca C.
Bart van Ingen Schenau,
3

Muitas vezes você pode usar um padrão como este:

class C {
public:
  void foo() {
    underlying_foo(handle.get());
  }

  void bar() {
    // transfers ownership
    underlying_bar(handle.release());
  }

  // use default copy/move constructor and assignment operator

private:
  struct deleter {
    void operator()(T* ptr) {
      deleter_fn(ptr);
    }
  };
  std::unique_ptr<T, deleter> handle;
};

Ao usar, releasevocê pode transferir explicitamente a propriedade. No entanto, isso é confuso e você deve evitá-lo, se possível.

A maioria das bibliotecas C possui um ciclo de vida do objeto semelhante ao C ++ (alocação, acessadores, destruição de objetos) que mapeia bem o padrão C ++ sem transferência de propriedade.

Se os usuários precisarem de propriedade compartilhada, eles deverão usar shared_ptrcom suas classes. Não tente implementar nenhuma propriedade compartilhada por você mesmo.


Atualização: se você deseja tornar a transferência de propriedade mais explícita, pode usar um qualificador de referência:

void bar() && { ... }

Em seguida, os usuários devem chamar barlvalues ​​como este:

C o;
std::move(o).bar();  // transfer of ownership is explicit at call site
Philipp
fonte
@Phillip, parece exatamente o que eu preciso. Vou tentar isso e voltar com você.
Jonathan Henson
Uma questão. O que exatamente é propriedade neste caso. Isso significa o privilégio de acessar / modificar o objeto, ou apenas a capacidade de construir e destruir o objeto?
Jonathan Henson
basicamente, depois de passar a memória subjacente para a c-lib, não devo fazer uma chamada gratuita para a memória. No entanto, eu posso e muitas vezes terei que acessar os dados no objeto.
Jonathan Henson
2
Propriedade é o direito de destruir o objeto.
precisa saber é o seguinte
@ JonathanHenson: esse é um caso interessante que ainda não está coberto neste idioma: depois que você lança um unique_ptr, ele não armazena mais o ponteiro que ele contém. Modelar a transferência de propriedade e ao mesmo tempo permitir o acesso a recursos renegados parece ser bastante confuso para os usuários. Como você controla quando a biblioteca C libera os objetos que agora possui?
Philipp
0

Há uma resposta direta ao seu problema, indicadores inteligentes. Ao usar um ponteiro inteligente para reter a memória da biblioteca C e adicionar uma referência quando o ponteiro também estiver na biblioteca (e soltar a referência quando a biblioteca C retornar), você liberará automaticamente a memória quando a contagem de referência cair para zero (e apenas então)

Michael Shaw
fonte
Isso não resolve o problema, a menos que eu entenda mal os indicadores inteligentes. O problema é que, às vezes, meu destruidor deve limpar a memória, outras vezes a c-library limpa a memória. Essas estruturas têm funções especiais init, livres. A biblioteca pode ser gratuita por conta própria, antes que a contagem de referência do ponteiro inteligente atinja 0. Preciso basicamente decidir a propriedade e deixar passar. Não preciso de um ponteiro inteligente para saber quando limpar a memória, o problema é que, às vezes, eu não sou o responsável por limpá-la e preciso liberá-lo no destruidor - nunca.
Jonathan Henson
Sim, eu concordo, eu não entendi a questão. Apenas uma observação, as diferentes bibliotecas compartilham a mesma tabela de alocação de memória? Minhas coisas intensas em C / C ++ ocorreram há 10 anos e, no Visual Studio, alocar memória em uma biblioteca e liberá-la em outra biblioteca seria um vazamento de memória, a menos que você alterasse algumas opções do compilador para ambas as bibliotecas. A consequência de fazer isso seria a contenção de gerenciamento de memória em aplicativos altamente encadeados. É bem provável que seja uma informação obsoleta.
Michael Shaw
não se tudo estiver carregado no mesmo espaço de endereço e estiver. De qualquer forma, estou recompilando a c-lib no mesmo assembly que o meu wrapper.
Jonathan Henson
0

Se a biblioteca puder liberar as coisas internamente, e os cenários em que isso acontecer estiver bem documentado, tudo o que você poderá fazer é definir um sinalizador, como você já está fazendo.

James
fonte
basicamente, depois de passar a memória subjacente para a c-lib, não devo fazer uma chamada gratuita para a memória. No entanto, eu posso e muitas vezes terei que acessar os dados no objeto.
Jonathan Henson