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
fonte
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 liberalmenteweak_ptr
sempre que posso, para tratá-lo como um identificador para o objeto, em vez de aumentar a contagem de referência.Eu uso em
scoped_ptr
todo 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 usarvector<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
delete
chamada 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.
fonte
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.
fonte
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 desempenhostd::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 queptr_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.
fonte
É 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:
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.
fonte
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:
fonte
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.
fonte
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.
fonte
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.
fonte
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:
T
nunca 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.Dito isto, a conveniência é uma desvantagem e também a segurança do tipo. Não pode acessar uma instância
T
sem ter acesso a ambos recipiente e índice. E um velho comumint32_t
nã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 umBar
usando um índice paraFoo
. Para atenuar o segundo problema, costumo fazer esse tipo de coisa: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 umFoo
sem 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 usarshared_ptr
era 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_ptr
GC 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_ptr
imediatamente.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.
fonte