Devo preferir ponteiros ou referências nos dados dos membros?


138

Este é um exemplo simplificado para ilustrar a pergunta:

class A {};

class B
{
    B(A& a) : a(a) {}
    A& a;
};

class C
{
    C() : b(a) {} 
    A a;
    B b; 
};

Portanto, B é responsável por atualizar uma parte do C. Executei o código através do lint e ele zombou do membro de referência: lint # 1725 . Isso fala sobre cuidar das cópias e atribuições padrão o que é justo o suficiente, mas a cópia e a atribuição padrão também são ruins para os ponteiros, portanto, há pouca vantagem nisso.

Eu sempre tento usar referências onde posso, pois ponteiros nus apresentam incertezas sobre quem é responsável por excluir esse ponteiro. Prefiro incorporar objetos por valor, mas se precisar de um ponteiro, uso auto_ptr nos dados dos membros da classe que possui o ponteiro e transmito o objeto como referência.

Geralmente, eu usaria apenas um ponteiro nos dados dos membros quando o ponteiro pudesse ser nulo ou mudar. Existem outros motivos para preferir ponteiros do que referências para membros de dados?

É verdade que um objeto que contém uma referência não deve ser atribuído, pois uma referência não deve ser alterada depois de inicializada?


"mas a cópia e a atribuição padrão também são ruins para os ponteiros": isso não é o mesmo: um ponteiro pode ser alterado sempre que você quiser, se não for const. Uma referência é sempre sempre const! (Você verá isso se tentar alterar seu membro para "A & const a;". O compilador (pelo menos GCC) avisará que a referência é const mesmo assim sem a palavra-chave "const".
mmmmmmmm

O principal problema disso é que, se alguém usa B b (A ()), você está ferrado porque está terminando com uma referência pendente.
log0

Respostas:


68

Evite membros de referência, porque eles restringem o que a implementação de uma classe pode fazer (incluindo, como você mencionou, impedindo a implementação de um operador de atribuição) e não oferecem benefícios ao que a classe pode oferecer.

Problemas de exemplo:

  • você é forçado a inicializar a referência na lista de inicializadores de cada construtor: não há como fatorar essa inicialização em outra função ( até C ++ 0x, de qualquer forma, edite: o C ++ agora tem construtores delegadores )
  • a referência não pode ser recuperada ou ser nula. Isso pode ser uma vantagem, mas se o código precisar ser alterado para permitir a religação ou para o membro ser nulo, todos os usos do membro precisarão ser alterados.
  • ao contrário dos membros do ponteiro, as referências não podem ser facilmente substituídas por ponteiros ou iteradores inteligentes, pois a refatoração pode exigir
  • Sempre que uma referência é usada, ela se parece com o tipo de valor ( .operador etc.), mas se comporta como um ponteiro (pode oscilar) - então, por exemplo, o Google Style Guide o desencoraja

65
Todas as coisas que você mencionou são boas coisas a serem evitadas; portanto, se as referências ajudarem nisso - elas são boas e não são ruins. A lista de inicialização é o melhor local para iniciar os dados. Muitas vezes você precisa ocultar o operador de atribuição, com referências que não precisa. "Não pode ser recuperado" - bom, reutilizar variáveis ​​é uma má ideia.
Mykola Golubyev

4
@Mykola: Eu concordo com você. Prefiro que os membros sejam inicializados nas listas de inicializadores, evito ponteiros nulos e certamente não é bom que uma variável mude seu significado. Onde nossas opiniões diferem é que eu não preciso ou quero que o compilador imponha isso para mim. Não torna as aulas mais fáceis de escrever, não encontrei bugs nesta área que seriam detectados e, ocasionalmente, aprecio a flexibilidade que recebo do código que usa consistentemente membros de ponteiros (ponteiros inteligentes, quando apropriado).
2111 James Hopkin

Eu acho que não ter que ocultar tarefas é um argumento falso, já que você não precisa ocultar tarefas se a classe contiver um ponteiro que não seja responsável por excluir de qualquer maneira - o padrão será o caso. Penso que esta é a melhor resposta, porque fornece boas razões pelas quais os ponteiros devem ter preferência sobre as referências nos dados dos membros.
Markh44 22/05/09

4
James, não é isso que facilita a escrita do código, é que facilita a leitura do código. Com uma referência como membro de dados, você nunca precisa se perguntar se ele pode ser nulo no ponto de uso. Isso significa que você pode examinar o código com menos contexto necessário.
precisa saber é o seguinte

4
Isso torna os testes de unidade e a simulação muito mais difíceis, impossíveis quase dependendo de quantos são usados, combinados com a 'explosão de gráficos afetados', que são imho razões suficientes para nunca usá-los.
Chris Huang-Leaver

155

Minha própria regra de ouro:

  • Use um membro de referência quando desejar que a vida do seu objeto dependa da vida de outros objetos : é uma maneira explícita de dizer que você não permite que o objeto fique vivo sem uma instância válida de outra classe - porque não há atribuição e a obrigação de obter a inicialização das referências por meio do construtor. É uma boa maneira de projetar sua classe sem assumir nada sobre a instância ser membro ou não de outra classe. Você assume apenas que suas vidas estão diretamente ligadas a outras instâncias. Permite alterar posteriormente como você usa sua instância de classe (com nova, como uma instância local, como um membro da classe, gerada por um conjunto de memórias em um gerenciador, etc.)
  • Use o ponteiro em outros casos : Quando desejar que o membro seja alterado posteriormente, use um ponteiro ou um ponteiro const para ter certeza de ler apenas a instância apontada. Se esse tipo deve ser copiável, você não pode usar referências de qualquer maneira. Às vezes, você também precisa inicializar o membro após uma chamada de função especial (init () por exemplo) e simplesmente não tem outra opção a não ser usar um ponteiro. MAS: use declarações em todas as suas funções de membro para detectar rapidamente o estado incorreto do ponteiro!
  • Nos casos em que você deseja que a vida útil do objeto seja dependente da vida útil de um objeto externo e também precise desse tipo de cópia, use membros do ponteiro, mas faça referência ao argumento no construtor Dessa forma, você está indicando na construção que a vida útil desse objeto depende no tempo de vida do argumento, MAS a implementação usa ponteiros para continuar copiável. Contanto que esses membros sejam alterados apenas por cópia e seu tipo não tenha um construtor padrão, o tipo deverá cumprir os dois objetivos.

1
Acho que o seu e o Mykola são respostas corretas e promovem a programação sem ponteiros (leia menos ponteiros).
Amit

37

Objetos raramente devem permitir atribuições e outras coisas, como comparação. Se você considerar algum modelo de negócios com objetos como 'Departamento', 'Funcionário', 'Diretor', é difícil imaginar um caso em que um funcionário será atribuído a outro.

Portanto, para objetos de negócios, é muito bom descrever os relacionamentos um para um e um para muitos como referências e não como indicadores.

E provavelmente não há problema em descrever o relacionamento um ou zero como um ponteiro.

Portanto, nenhum fator 'não podemos atribuir'.
Muitos programadores simplesmente se acostumam com ponteiros e é por isso que eles encontram qualquer argumento para evitar o uso de referência.

Ter um ponteiro como membro forçará você ou membro de sua equipe a verificar o ponteiro várias vezes antes do uso, com o comentário "just in case". Se um ponteiro pode ser zero, o ponteiro provavelmente é usado como um tipo de sinalizador, o que é ruim, pois todo objeto tem que desempenhar seu próprio papel.


2
+1: O mesmo que experimentei. Somente algumas classes genéricas de armazenamento de dados precisam de assignemtn e copy-c'tor. E para estruturas de objetos de negócios de mais alto nível, deve haver outras técnicas de cópia que também manipulem coisas como "não copiar campos exclusivos" e "adicionar a outro pai" etc. etc. Portanto, você usa a estrutura de alto nível para copiar seus objetos de negócios e não permitir as atribuições de baixo nível.
Mmmmmmmm

2
+1: Alguns pontos de concordância: os membros de referência que impedem a definição de um operador de atribuição não são argumentos importantes. Como você afirma corretamente de uma maneira diferente, nem todos os objetos têm semântica de valor. Também concordo que não queremos 'if (p)' espalhados por todo o código sem motivo lógico. Mas a abordagem correta é através dos invariantes de classe: uma classe bem definida não deixará margem para dúvidas sobre se os membros podem ser nulos. Se qualquer ponteiro puder ser nulo no código, espero que seja bem comentado.
2111 James Hopkin

@JamesHopkin Concordo que classes bem projetadas podem usar ponteiros de maneira eficaz. Além de proteger contra NULL, as referências também garantem que sua referência foi inicializada (um ponteiro não precisa ser inicializado, para que possa apontar para qualquer outra coisa). As referências também informam sobre propriedade - ou seja, que o objeto não é o proprietário do membro. Se você usar consistentemente referências para membros agregados, terá um alto grau de confiança de que os membros do ponteiro são objetos compostos (embora não haja muitos casos em que um membro composto é representado por um ponteiro).
precisa saber é o seguinte


6

Em alguns casos importantes, a atribuição simplesmente não é necessária. Geralmente, são wrappers de algoritmo leves que facilitam o cálculo sem sair do escopo. Esses objetos são candidatos principais para membros de referência, pois você pode ter certeza de que eles sempre possuem uma referência válida e nunca precisam ser copiados.

Nesses casos, certifique-se de tornar o operador de atribuição (e geralmente também o construtor de cópias) não utilizável (herdando boost::noncopyableou declarando-o privado).

No entanto, como os usuários já comentaram, o mesmo não ocorre para a maioria dos outros objetos. Aqui, o uso de membros de referência pode ser um grande problema e geralmente deve ser evitado.


6

Como todo mundo parece estar distribuindo regras gerais, vou oferecer duas:

  • Nunca, nunca use referências como alunos. Eu nunca fiz isso no meu próprio código (exceto para provar a mim mesmo que estava certo nesta regra) e não consigo imaginar um caso em que o faria. A semântica é muito confusa, e realmente não é para isso que as referências foram criadas.

  • Sempre, sempre, use referências ao passar parâmetros para funções, exceto para os tipos básicos, ou quando o algoritmo exigir uma cópia.

Essas regras são simples e me mantiveram em boa posição. Deixo fazendo regras sobre o uso de ponteiros inteligentes (mas, por favor, não auto_ptr) como membros da classe para outras pessoas.


2
Não concordo totalmente com o primeiro, mas discordar não é um problema para avaliar a lógica. +1
David Rodríguez - dribeas 21/05

(Parece que estamos perdendo esse argumento com os bons eleitores de SO, embora)
James Hopkin

2
Votei negativamente porque "Nunca use referências como membros da classe" e a justificativa a seguir não está correta para mim. Conforme explicado na minha resposta (e comente a resposta superior) e pela minha própria experiência, há casos em que isso é simplesmente uma coisa boa. Pensei como você até ser contratado em uma empresa em que eles o estavam usando de uma maneira boa e deixou claro para mim que há casos em que isso ajuda. Na verdade, acho que o problema real é mais sobre a sintaxe e as implicações ocultas que fazem uso de referências, pois os membros exigem que o programador entenda o que está fazendo.
Klaim 21/05

1
Neil> Eu concordo, mas não é a "opinião" que me fez votar, é a afirmação "nunca". Como argumentar aqui não parece ser útil, cancelarei meu voto negativo.
Klaim

2
Essa resposta pode ser melhorada, explicando o que a semântica do membro de referência é confusa. Essa lógica é importante porque, por sua vez, os ponteiros parecem mais semanticamente ambíguos (eles podem não ser inicializados e não dizem nada sobre a propriedade do objeto subjacente).
Weberc2

4

Sim para: É verdade dizer que um objeto que contém uma referência não deve ser atribuído, pois uma referência não deve ser alterada depois de inicializada?

Minhas regras práticas para membros de dados:

  • nunca use uma referência, porque impede a atribuição
  • se sua turma for responsável pela exclusão, use o scoped_ptr do boost (que é mais seguro que um auto_ptr)
  • caso contrário, use um ponteiro ou um ponteiro const

2
Não posso nem citar um caso em que precisaria de uma atribuição do objeto de negócios.
Mykola Golubyev

1
+1: Eu tinha acabado de acrescentar que ter cuidado com auto_ptr membros - qualquer classe que tem deles deve ter atribuição e construção cópia explicitamente definido (os gerados são muito pouco provável que seja o que é necessário)
James Hopkin

O auto_ptr também impede a atribuição (sensata).

2
@Mykola: Eu também fiz a experiência de que 98% dos meus objetos de negócios não precisam de atribuição ou cópia-cópia. Eu evito isso com uma pequena macro que adiciono aos cabeçalhos dessas classes, que torna os copiadores e operadores = () privados sem implementação. Então, nos 98% dos objetos, eu também uso referências e é seguro.
Mmmmmmmm

1
Referências como membros podem ser usadas para declarar explicitamente que a vida da instância da classe depende diretamente da vida das instâncias de outras classes (ou a mesma). "Nunca use uma referência, porque impede a atribuição" está assumindo que todas as classes precisam permitir a atribuição. É como assumindo que todas as classes têm para permitir operação de cópia ...
Klaim

2

Geralmente, eu usaria apenas um ponteiro nos dados dos membros quando o ponteiro pudesse ser nulo ou mudar. Existem outros motivos para preferir ponteiros do que referências para membros de dados?

Sim. Legibilidade do seu código. Um ponteiro torna mais óbvio que o membro é uma referência (ironicamente :)) e não um objeto contido, porque quando você o usa, é necessário desassociá-lo. Sei que algumas pessoas pensam que é antiquado, mas ainda acho que isso simplesmente evita confusão e erros.


1
Lakos também argumenta sobre isso em "Design de software C ++ em larga escala".
Johan Kotliński

Acredito que o Code Complete defenda isso também e não sou contra. É embora não excessivamente atraente ...
markh44

2

Aconselho contra os membros de dados de referência, porque você nunca sabe quem irá derivar de sua classe e o que eles podem querer fazer. Eles podem não querer usar o objeto referenciado, mas, sendo uma referência, você os forçou a fornecer um objeto válido. Fiz isso comigo o suficiente para parar de usar membros de dados de referência.


0

Se eu entendi a pergunta corretamente ...

Referência como parâmetro de função em vez de ponteiro: Como você apontou, um ponteiro não deixa claro quem é o proprietário da limpeza / inicialização do ponteiro. Prefira um ponto compartilhado quando desejar um ponteiro, ele faz parte do C ++ 11 e um fraco_ptr quando a validade dos dados não for garantida durante a vida útil da classe que aceita os dados apontados .; também parte do C ++ 11. O uso de uma referência como parâmetro de função garante que a referência não seja nula. Você precisa subverter os recursos de idioma para contornar isso, e não nos importamos com codificadores de canhão soltos.

Referência aa variável de membro: Como acima, em relação à validade dos dados. Isso garante que os dados apontados e referenciados são válidos.

Mover a responsabilidade da validade variável para um ponto anterior no código não apenas limpa o código posterior (a classe A no seu exemplo), mas também deixa claro para a pessoa que está usando.

No seu exemplo, que é um pouco confuso (eu realmente tentaria encontrar uma implementação mais clara), o A usado por B é garantido por toda a vida útil do B, já que B é um membro de A, portanto, uma referência reforça isso. e é (talvez) mais claro.

E, apenas no caso (que é uma probabilidade muito baixa, pois não faria sentido no contexto de seu código), outra alternativa, usar um parâmetro A não referenciado e sem ponteiro, copiaria A e tornaria o paradigma inútil - I realmente não acho que você quis dizer isso como uma alternativa.

Além disso, isso também garante que você não poderá alterar os dados mencionados, onde um ponteiro pode ser modificado. Um ponteiro const funcionaria apenas se o ponteiro / referência aos dados não fosse mutável.

Os ponteiros são úteis se não for garantido que o parâmetro A para B seja definido ou possa ser reatribuído. E, às vezes, um ponteiro fraco é muito detalhado para a implementação, e um bom número de pessoas não sabe o que é um fraco_ptr ou apenas as detesta.

Rasgue esta resposta, por favor :)

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.