C ++: ponteiros inteligentes, ponteiros brutos, sem ponteiros? [fechadas]

48

No escopo do desenvolvimento de jogos em C ++, quais são seus padrões preferidos em relação ao uso de ponteiros (nenhum, bruto, com escopo definido, compartilhado ou de outra forma entre inteligente e burro)?

Você pode considerar

  • propriedade do objeto
  • fácil de usar
  • política de cópia
  • a sobrecarga
  • referências cíclicas
  • plataforma de destino
  • use com recipientes
jmp97
fonte

Respostas:

32

Depois de ter tentado várias abordagens, hoje me encontro alinhado com o Guia de estilos do Google C ++ :

Se você realmente precisa de semântica de ponteiro, o scoped_ptr é ótimo. Você só deve usar std :: tr1 :: shared_ptr sob condições muito específicas, como quando objetos precisam ser mantidos por contêineres STL. Você nunca deve usar auto_ptr. [...]

De um modo geral, preferimos projetar código com propriedade clara do objeto. A propriedade mais clara do objeto é obtida usando um objeto diretamente como um campo ou variável local, sem usar ponteiros. [..]

Embora não sejam recomendados, os ponteiros contados por referência às vezes são a maneira mais simples e elegante de resolver um problema.

jmp97
fonte
14
Hoje, você pode querer usar std :: unique_ptr em vez de scoped_ptr.
Klaim
24

Eu também sigo a linha de pensamento "forte propriedade". Eu gosto de delinear claramente que "esta classe é dona desse membro" quando apropriado.

Eu raramente uso shared_ptr. Se fizer isso, uso liberalmente weak_ptrsempre que posso, para tratá-lo como um identificador para o objeto, em vez de aumentar a contagem de referência.

Eu uso em scoped_ptrtodo o lugar. Mostra propriedade óbvia. A única razão pela qual eu não apenas tornei objetos desse tipo um membro é porque você pode declará-los se eles estiverem em um scoped_ptr.

Se eu precisar de uma lista de objetos, eu uso ptr_vector. É mais eficiente e tem menos efeitos colaterais do que usar vector<shared_ptr>. Eu acho que você pode não ser capaz de encaminhar declarar o tipo no ptr_vector (já faz um tempo), mas a semântica faz com que valha a pena na minha opinião. Basicamente, se você remover um objeto da lista, ele será excluído automaticamente. Isso também mostra propriedade óbvia.

Se eu precisar de referência a algo, tento fazer uma referência em vez de um ponteiro nu. Às vezes, isso não é prático (ou seja, sempre que você precisar de uma referência após a construção do objeto). De qualquer forma, as referências mostram obviamente que você não é o proprietário do objeto e, se estiver seguindo a semântica de ponteiros compartilhados em qualquer outro lugar, os ponteiros nus geralmente não causam confusão adicional (especialmente se você seguir uma regra "sem exclusão manual") .

Com esse método, um jogo do iPhone no qual trabalhei conseguiu ter apenas uma deletechamada e estava na ponte Obj-C para C ++ que escrevi.

Geralmente sou da opinião de que o gerenciamento de memória é importante demais para deixar para os seres humanos. Se você pode automatizar a exclusão, você deve. Se a sobrecarga do shared_ptr for muito cara em tempo de execução (supondo que você tenha desativado o suporte de encadeamento, etc), provavelmente você deve estar usando outra coisa (por exemplo, um padrão de bucket) para reduzir suas alocações dinâmicas.

Tetrad
fonte
11
Excelente resumo. Você realmente quer dizer shared_ptr em vez de mencionar smart_ptr?
precisa saber é o seguinte
Sim, eu quis dizer shared_ptr. Eu vou consertar isso.
Tetrad
10

Use a ferramenta certa para o trabalho.

Se o seu programa pode lançar exceções, verifique se o seu código reconhece as exceções. Usar ponteiros inteligentes, RAII e evitar a construção de duas fases são bons pontos de partida.

Se você tiver referências cíclicas sem semântica clara de propriedade, considere usar uma biblioteca de coleta de lixo ou refatorar seu design.

Boas bibliotecas permitem que você codifique para o conceito e não o tipo, portanto, na maioria dos casos, não importa qual tipo de ponteiro você está usando além dos problemas de gerenciamento de recursos.

Se você estiver trabalhando em um ambiente multithread, entenda se seu objeto é potencialmente compartilhado entre threads. Um dos principais motivos para considerar o uso de boost :: shared_ptr ou std :: tr1 :: shared_ptr é porque ele usa uma contagem de referência segura para threads.

Se você estiver preocupado com a alocação separada das contagens de referência, existem várias maneiras de contornar isso. Usando a biblioteca boost :: shared_ptr, é possível agrupar a alocação dos contadores de referência ou usar o boost :: make_shared (minha preferência), que aloca o objeto e a contagem de referência em uma única alocação, aliviando a maioria das preocupações de falta de cache que as pessoas têm. Você pode evitar o impacto no desempenho da atualização da contagem de referência no código crítico de desempenho mantendo uma referência ao objeto no nível superior e transmitindo referências diretas ao objeto.

Se você precisar de propriedade compartilhada, mas não quiser pagar o custo da contagem de referência ou da coleta de lixo, considere usar objetos imutáveis ​​ou uma cópia no idioma de gravação.

Lembre-se de que, de longe, suas maiores vitórias em desempenho serão no nível da arquitetura, seguidas por um nível de algoritmo, e embora essas preocupações de baixo nível sejam muito importantes, elas devem ser abordadas somente depois que você resolver os principais problemas. Se você está lidando com problemas de desempenho no nível de falhas de cache, há uma série de problemas dos quais você também deve estar ciente, como o compartilhamento falso, que não tem nada a ver com indicadores por palavra.

Se você estiver usando ponteiros inteligentes apenas para compartilhar recursos como texturas ou modelos, considere uma biblioteca mais especializada como o Boost.Flyweight.

Uma vez adotado o novo padrão, a semântica de movimentação, as referências de valor e o encaminhamento perfeito tornarão o trabalho com objetos e contêineres caros muito mais fácil e eficiente. Até lá, não armazene ponteiros com semântica de cópia destrutiva, como auto_ptr ou unique_ptr, em um Container (o conceito padrão). Considere usar a biblioteca Boost.Pointer Container ou armazenar ponteiros inteligentes de propriedade compartilhada em Containers. No código crítico de desempenho, você pode evitar esses dois a favor de contêineres intrusivos, como os do Boost.Intrusive.

A plataforma de destino não deve realmente influenciar muito sua decisão. Dispositivos embarcados, smartphones, telefones estúpidos, PCs e consoles podem executar o código perfeitamente. Os requisitos do projeto, como orçamentos rígidos de memória ou nenhuma alocação dinâmica de sempre / após o carregamento, são preocupações mais válidas e devem influenciar suas escolhas.

Jaeger
fonte
3
O tratamento de exceções nos consoles pode ser um pouco complicado - o XDK em particular é meio hostil de exceção.
Crashworks
11
A plataforma de destino realmente deve influenciar seu design. Às vezes, o hardware que transforma seus dados pode ter grandes influências no seu código-fonte. A arquitetura PS3 é um exemplo concreto em que você realmente precisa levar o hardware para projetar seu gerenciamento de recursos e memória, bem como seu renderizador.
Simon
Discordo apenas um pouco, especificamente em relação ao GC. Na maioria das vezes, as referências cíclicas não são um problema para os esquemas de referência. Geralmente, esses problemas cíclicos de propriedade surgem porque as pessoas não pensam adequadamente sobre a propriedade de objetos. Só porque um objeto precisa apontar para algo, não significa que ele deve possuir esse ponteiro. O exemplo comumente citado é ponteiros de volta nas árvores, mas o pai do ponteiro de uma árvore pode ser com segurança um ponteiro bruto, sem sacrificar a segurança.
Tim Seguine 15/09/13
4

Se você estiver usando C ++ 0x, use std::unique_ptr<T>.

Não possui sobrecarga de desempenho, diferente da std::shared_ptr<T>sobrecarga de contagem de referência. Um unique_ptr possui seu ponteiro e você pode transferir a propriedade com a semântica de movimento do C ++ 0x . Você não pode copiá-los - apenas mova-os.

Também pode ser usado em contêineres, por exemplo std::vector<std::unique_ptr<T>>, que é compatível com binário e idêntico em desempenho std::vector<T*>, mas não perde memória se você apagar elementos ou limpar o vetor. Isso também tem melhor compatibilidade com os algoritmos STL do que ptr_vector.

Para muitos propósitos, o IMO é um contêiner ideal: acesso aleatório, exceção segura, evita vazamentos de memória, baixa sobrecarga para realocação de vetores (apenas embaralha os ponteiros nos bastidores). Muito útil para muitos propósitos.

AshleysBrain
fonte
3

É uma boa prática documentar quais classes possuem quais ponteiros. De preferência, basta usar objetos normais e sem ponteiros sempre que puder.

No entanto, quando você precisa acompanhar os recursos, passar os ponteiros é a única opção. Existem alguns casos:

  • Você obtém o ponteiro de outro lugar, mas não o gerencia: basta usar um ponteiro normal e documentá-lo para que nenhum codificador depois de tentar excluí-lo.
  • Você pega o ponteiro de outro lugar e o acompanha: use um scoped_ptr.
  • Você obtém o ponteiro de outro lugar e o acompanha, mas ele precisa de um método especial para excluí-lo: use shared_ptr com um método de exclusão personalizado.
  • Você precisa do ponteiro em um contêiner STL: ele será copiado para que você precise de boost :: shared_ptr.
  • Muitas classes compartilham o ponteiro, e não está claro quem o excluirá: shared_ptr (o caso acima é realmente um caso especial deste ponto).
  • Você mesmo cria o ponteiro e só precisa dele: se você realmente não pode usar um objeto normal: scoped_ptr.
  • Você cria o ponteiro e o compartilha com outras classes: shared_ptr.
  • Você cria o ponteiro e o passa: use um ponteiro normal e documente sua interface para que o novo proprietário saiba que ele próprio deve gerenciar o recurso!

Eu acho que isso abrange basicamente como eu gerencio meus recursos agora. O custo de memória de um ponteiro como shared_ptr é geralmente o dobro do custo de memória de um ponteiro normal. Não acho que essa sobrecarga seja muito grande, mas se você tiver poucos recursos, considere projetar seu jogo para reduzir o número de indicadores inteligentes. Em outros casos, eu apenas desenvolvo bons princípios, como os marcadores acima, e o criador de perfil me dirá onde precisarei de mais velocidade.

Nef
fonte
1

Quando se trata de apontar especificamente para o boost, acho que eles devem ser evitados desde que sua implementação não seja exatamente o que você precisa. Eles têm um custo maior do que qualquer um inicialmente esperaria. Eles fornecem uma interface que permite ignorar partes vitais e importantes do gerenciamento de recursos e memória.

Quando se trata de qualquer desenvolvimento de software, acho que é importante pensar nos seus dados. É muito importante como seus dados são representados na memória. A razão para isso é que a velocidade da CPU tem aumentado a uma taxa muito maior do que o tempo de acesso à memória. Isso geralmente faz dos caches de memória o principal gargalo dos jogos de computador mais modernos. Ter seus dados alinhados linearmente na memória de acordo com a ordem de acesso é muito mais amigável ao cache. Esse tipo de solução geralmente leva a designs mais limpos, código mais simples e definitivamente definido, mais fácil de depurar. Ponteiros inteligentes levam facilmente a alocações freqüentes de recursos de memória dinâmica, fazendo com que eles sejam espalhados por toda a memória.

Essa não é uma otimização prematura, é uma decisão saudável que pode e deve ser tomada o mais cedo possível. É uma questão de entendimento arquitetural do hardware em que seu software será executado e é importante.

Edit: Há algumas coisas a considerar em relação ao desempenho de ponteiros compartilhados:

  • O contador de referência é heap alocado.
  • Se você usar a segurança de thread ativada, a contagem de referência será feita através de operações intertravadas.
  • Passar o ponteiro por valor modifica a contagem de referência, o que significa operações intertravadas, provavelmente usando acesso aleatório na memória (bloqueios + provável perda de cache).
Simon
fonte
2
Você me perdeu em 'evitado a todo custo'. Em seguida, você descreve um tipo de otimização que raramente é uma preocupação para jogos do mundo real. A maior parte do desenvolvimento de jogos é caracterizada por problemas de desenvolvimento (atrasos, bugs, jogabilidade etc.) e não por falta de desempenho do cache da CPU. Portanto, discordo totalmente da ideia de que esse conselho não é uma otimização prematura.
precisa saber é o seguinte
2
Eu tenho que concordar com o design inicial do layout dos dados. É importante obter qualquer desempenho de um console / dispositivo móvel moderno e é algo que nunca deve ser negligenciado.
Olly
11
Esse é um problema que eu já vi em um dos estúdios da AAA em que trabalho. Você também pode ouvir o arquiteto-chefe da Insomniac Games, Mike Acton. Não estou dizendo que o impulso é uma biblioteca ruim, não é apenas adequada para jogos de alto desempenho.
Simon
11
@ kevin42: A coerência do cache é provavelmente a principal fonte de otimizações de baixo nível no desenvolvimento de jogos atualmente. @ Simon: A maioria das implementações shared_ptr evita bloqueios em qualquer plataforma que suporte comparação e troca, que inclui PCs Linux e Windows, e acredito que inclua o Xbox.
11
@ Joe Wreschnig: Isso é verdade, a falta de cache ainda é mais provável, apesar de causar qualquer inicialização de um ponteiro compartilhado (copiar, criar a partir de um ponteiro fraco, etc.). Uma falta de cache L2 nos PCs modernos é como 200 ciclos e no PPC (xbox360 / ps3) é maior. Com um jogo intenso, você pode ter até 1000 objetos de jogo, já que cada objeto de jogo pode ter alguns recursos. Estamos analisando problemas em que o dimensionamento é uma grande preocupação. Isso provavelmente causará problemas no final de um ciclo de desenvolvimento (quando você atingirá a grande quantidade de objetos do jogo).
Simon
0

Eu costumo usar ponteiros inteligentes em todos os lugares. Não tenho certeza se essa é uma idéia totalmente boa, mas sou preguiçosa e não consigo ver nenhuma desvantagem real [exceto se eu quisesse fazer alguma aritmética de ponteiro no estilo C]. Uso boost :: shared_ptr porque sei que posso copiá-la - se duas entidades compartilham uma imagem, se uma morre, a outra também não deve perder a imagem.

A desvantagem disso é que, se um objeto exclui algo para o qual aponta e possui, mas algo mais está apontando para ele, ele não é excluído.

O Pato Comunista
fonte
11
Também uso o share_ptr em quase todos os lugares - mas hoje tento pensar se preciso ou não de propriedade compartilhada para alguns dados. Caso contrário, pode ser razoável tornar esses dados um membro não ponteiro para a estrutura de dados pai. Acho que uma propriedade clara simplifica os projetos.
precisa saber é o seguinte
0

Os benefícios do gerenciamento de memória e da documentação fornecidos por bons indicadores inteligentes significam que eu os uso regularmente. No entanto, quando o criador de perfil aparecer e me disser que um uso em particular está me custando, voltarei a um gerenciamento mais neolítico de ponteiro.

tenpn
fonte
0

Eu sou velho, velho e um contador de ciclos. Em meu próprio trabalho, uso ponteiros brutos e nenhuma alocação dinâmica em tempo de execução (exceto os próprios pools). Tudo é agrupado e a propriedade é muito rigorosa e nunca transferível. Se realmente necessário, escrevo um alocador de blocos pequenos personalizado. Eu garanto que haja um estado durante o jogo para cada piscina se limpar. Quando as coisas ficam peludas, envolvo objetos em alças para que eu possa realocá-los, mas prefiro que não. Os recipientes são personalizados e extremamente simples. Também não reuso o código.
Embora eu nunca discuta a virtude de todos os indicadores, contêineres, iteradores e outros itens inteligentes, sou conhecido por ser capaz de codificar extremamente rápido (e razoavelmente confiável - embora não seja aconselhável que outras pessoas entrem no meu código por razões óbvias, como ataques cardíacos e pesadelos perpétuos).

No trabalho, é claro, tudo é diferente, a menos que eu faça um protótipo, o que felizmente faço muito.

Kaj
fonte
0

Quase nenhuma, apesar de ser uma resposta estranha e provavelmente nem de longe adequada para todos.

Mas, no meu caso pessoal, achei muito mais útil armazenar todas as instâncias de um tipo específico em uma sequência central de acesso aleatório (seguro para threads) e, em vez disso, trabalhar com índices de 32 bits (endereços relativos, por exemplo) , em vez de indicadores absolutos.

Para começar:

  1. Ele reduz pela metade os requisitos de memória do ponteiro analógico em plataformas de 64 bits. Até agora, nunca precisei de mais de 4,29 bilhões de instâncias de um tipo de dados específico.
  2. Ele garante que todas as instâncias de um tipo específico Tnunca sejam muito dispersas na memória. Isso tende a reduzir as falhas de cache para todos os tipos de padrões de acesso, passando pelas estruturas vinculadas, como árvores, se os nós forem vinculados usando índices em vez de ponteiros.
  3. É fácil associar dados paralelos usando matrizes paralelas baratas (ou matrizes esparsas) em vez de árvores ou tabelas de hash.
  4. As interseções de conjuntos podem ser encontradas em tempo linear ou melhor usando, por exemplo, um conjunto de bits paralelo.
  5. Podemos ordenar rapidamente os índices e obter um padrão de acesso seqüencial muito amigável para cache.
  6. Podemos acompanhar quantas instâncias e quanto a um determinado tipo de dados foram alocados.
  7. Minimiza o número de lugares que precisam lidar com coisas como segurança de exceção, se você se importa com esse tipo de coisa.

Dito isto, a conveniência é uma desvantagem e também a segurança do tipo. Não pode acessar uma instância Tsem ter acesso a ambos recipiente e índice. E um velho comum int32_tnão nos diz nada sobre a que tipo de dados se refere, então não há segurança de tipo. Poderíamos tentar acessar acidentalmente um Barusando um índice para Foo. Para atenuar o segundo problema, costumo fazer esse tipo de coisa:

struct FooIndex
{
    int32_t index;
};

O que parece meio bobo, mas me devolve o tipo de segurança, para que as pessoas não possam tentar acidentalmente acessar um Baríndice através de um Foosem um erro do compilador. Pelo lado da conveniência, aceito apenas o pequeno inconveniente.

Outra coisa que pode ser um grande inconveniente para as pessoas é que não posso usar o polimorfismo baseado em herança no estilo OOP, pois isso exigiria um ponteiro de base que pode apontar para todos os tipos de subtipos diferentes, com diferentes requisitos de tamanho e alinhamento. Mas hoje em dia não uso muito a herança - prefiro a abordagem ECS.

Quanto a shared_ptr, eu tento não usá-lo tanto. Na maioria das vezes, acho que não faz sentido compartilhar propriedade, e fazê-lo aleatoriamente pode levar a vazamentos lógicos. Freqüentemente, pelo menos em um nível superior, uma coisa tende a pertencer a uma coisa. Onde eu costumava achar tentador usar shared_ptrera prolongar a vida útil de um objeto em lugares que realmente não lidavam muito com propriedade, como apenas uma função local em um encadeamento para garantir que o objeto não seja destruído antes que o encadeamento seja concluído usando isso.

Para resolver esse problema, em vez de usar um shared_ptrGC ou algo assim, geralmente sou a favor de tarefas de curta duração executadas em um pool de threads e o faço se esse thread solicitar a destruição de um objeto, que a destruição real seja adiada para um cofre momento em que o sistema pode garantir que nenhum encadeamento precise acessar o tipo de objeto.

Às vezes, ainda acabo usando a contagem de ref, mas a trato como uma estratégia de último recurso. E há alguns casos em que realmente faz sentido compartilhar a propriedade, como a implementação de uma estrutura de dados persistente, e acho que faz todo sentido fazer sentido shared_ptrimediatamente.

De qualquer forma, uso principalmente índices e uso ponteiros brutos e inteligentes com moderação. Gosto de índices e os tipos de portas que eles abrem quando você sabe que seus objetos são armazenados de forma contígua e não espalhados pelo espaço da memória.

user77245
fonte