FAQ Lite
Semântica de Referência e de Valor

[ 28.1 ] O que é semântica de valor e/ou de referência, e qual delas é melhor em C++?
[ 28.2 ] O que é dado virtual, e como eu posso, e porque deveria, usá-lo em C++?
[ 28.3 ] Qual a diferença entre dado virtual e dado dinâmico?
[ 28.4 ] Normalmente eu devo usar pointers para objetos alocados dinamicamente para meus dados membro, ou devo usar composição?
[ 28.5 ] Quais são os custos relativos dos 3 eventos de degradação de performance associados a alocação dinâmica de objetos membro?
[ 28.6 ] Funções membro inline virtual são realmente executadas em modo inline?
[ 28.7 ] Parece-me então que eu nunca deveria usar semântica de referência. Certo?
[ 28.8 ] A baixa performance de semântica de referência significa que eu devo passar parâmetros por valor?

[ 28.1 ] O que é semântica de valor e/ou de referência, e qual delas é melhor em C++?

Com semântica de referência, a operação de atribuição é uma cópia de pointer, ou seja uma referência. Semântica de valor significa que a operação de atribuição copia o próprio valor, não o pointer para o valor.

C++ lhe dá as opções: use o operator de atribuição para copiar o valor (semântica de valor/cópia), ou use uma cópia de pointer para copiar um pointer (semântica de referência). C++ permite que você sobreponha o operator de atribuição para fazer qualquer coisa que se deseje, embora a opção default, e mais comum, seja copiar o valor.

Prós de semântica de referência: flexibilidade e ligação dinâmica. Você obtém ligação dinâmica em C++ apenas quando você passa um pointer ou passa uma referência, não quando você passa um valor.

Prós de semântica de valor: velocidade. Velocidade parece ser um benefício importantíssimo quando se requer que um objeto (não o pointer) seja copiado, mas o fato a considerar é que se um acessa o objeto, e mais de um copiam o objeto, o custo das múltiplas cópias será maior do que o benefício de se ter o objeto real, ao invés de um pointer para o objeto.

Há três casos em que você tem um objeto real, e não um pointer para um objeto: objetos locais, objetos globais/static e objetos membro completamente contidos na classe. Sendo que esse último é o mais importante (composição).

Os próximos FAQs dão maiores informações sobre copia vs referência. Por favor, leia-os todos para ter uma perspectiva equilibrada. Os primeiros são, propositadamente, tendenciosos pró semântica de valor, assim, se você ler apenas os primeiros próximos você terá uma perspectiva tendenciosa.

Topo
[ 28.2 ] O que é dado virtual, e como eu posso, e porque deveria, usá-lo em C++?

Dado virtual permite a uma classe derivada alterar a classe de um objeto membro da classe base. Dado virtual não é intrinsecamente suportado por C++, entretanto pode ser simulado em C++. A simulação não é muito elegante, mas funciona.

Para simular um dado virtual em C++, a classe base deve ter um pointer para um objeto membro, e a   classe derivada deve produzir um new objeto a ser apontado pelo pointer da classe base. A classe base poderia também ter um ou mais construtores normais que produzissem o seu próprio referente (de novo via new), e o destrutor da classe base deveria delete o referente.

Por exemplo, class Stack poderia ter um objeto membro Array (usando um pointer), e a derivada class StretchableStack poderia sobrepor o membro da classe base, substituindo Array por StretchableArray. Para que isso funcione, StretchableArray deveria ter que herdar de Array, de modo que Stack teria um Array*. Os construtores normais de Stack inicializariam essa Array* com uma new Array, mas Stack poderia também ter um (possivelmente protected:) construtor que aceitaria um Array* da classe derivada. O construtor de StretchableArray forneceria uma new StretchableArray para esse construtor especial.

Prós:

  • Facilidade de implementação de StretchableStack (a maioria do código seria herdada)
  • Usuários podem passar uma StretchableStack como um tipo-de-Stack

Contras:

  • Acrescenta uma nova camada de acesso indireto a Array
  • Acrescenta overhead de alocação de memória (new e delete)
  • Acrescenta algum overhead de ligação dinâmica (a razão será apresentada no próximo FAQ)

Em outras palavras, nós fomos bem sucedidos em tornar o nosso trabalho mais fácil na implementação de StretchableStack, mas todos os nossos usuários vão pagar por isso. Infelizmente um overhead extra foi imposto tanto aos usuários de StretchableStack quanto aos de Stack.

Por favor, leia o restante dessa seção para ter uma perspectiva equilibrada sobre esse assunto.

Topo
[ 28.3 ] Qual a diferença entre dado virtual e dado dinâmico?

O modo mais simples de perceber essa distinção é por analogia com funções virtuais. Uma função membro virtual significa que a declaração da função (assinatura) deve ser a mesma nas subclasses, mas a definição da função (corpo) pode ser sobreposto. A sobreposição de funções membro herdadas é uma propriedade estática da subclasse; não pode ser alterada dinamicamente ao longo da vida do objeto, e não é possível que distintos objetos da subclasse tenham distintas definições de funções membro.

Agora releia o parágrafo anterior, fazendo as seguintes substituições:

  • Função membro por objeto membro
  • Assinatura por tipo
  • Corpo por classe específica

Após as substituições você terá produzido a definição de dado virtual.

Uma outra maneira de encarar isso é distinguir funções membros por-objeto de funções membro dinâmicas. Uma função membro por-objeto é uma função membro que é potencialmente diferente a cada dada instância de um objeto, e poderia ser implementada incorporando-se ao objeto um pointer para função; esse pointer poderia ser const, já que não seria alterado ao longo da existência do objeto. Uma função dinâmica é uma função que será alterada ao longo do tempo; também poderia ser implementada através de um pointer de função, mas esse pointer não poderia ser const.

Estendendo a analogia, temos três conceitos distintos de dados membro:

  • Dado virtual: a definição (class) do objeto membro pode ser sobreposta nas subclasses, mas sua declaração (tipo) permanece a mesma. Essa sobreposição é uma propriedade estática da subclasse.
  • Dado-por-objeto: qualquer objeto de uma classe, após inicializado, pode instanciar um objeto membro em conformidade com seu próprio tipo (mesmo tipo), O objeto que gera as novas instâncias é normalmente denominado empacotador. A classe específica do objeto empacotador é uma propriedade estática do objeto instanciado.
  • Dado dinâmico: a classe específica do objeto membro pode ser alterada dinamicamente ao longo do tempo.

O que leva todos essas variantes a parecerem, no fundo, a mesma coisa é o fato de que nenhum deles é suportado pelo C++. São meramente permitidos, e nesse caso o mecanismo de falsificação é o mesmo: um pointer para uma classe base, provavelmente abstrata. Em uma linguagem que suporta efetivamente esses três mecanismos de abstração, a diferença é mais evidente, já que cada um tem uma variante sintática diferente.  

Topo
[ 28.4 ] Normalmente eu devo usar pointers para objetos alocados dinamicamente para meus dados membro, ou devo usar composição?

Composição.

Seus objetos membro devem normalmente estar contidos em um objeto composto, mas nem sempre.

Objetos empacotadores são um bom exemplo de onde você quer um pointer/referência; mais ainda. relações N-para-1 precisam de algo como um pointer/referência

Há três razões pelas quais objetos membro plenamente contidos (composição) tem melhor performance que pointers, para objetos alocados dinamicamente:

  • Camada extra de acesso indireto a cada vez que você precisa acessar o objeto membro
  • Alocação extra de memória (new no construtor e delete no destrutor)
  • Ligação dinâmica extra (a razão será dada a seguir)
Topo
[ 28.5 ] Quais são os custos relativos dos 3 eventos de degradação de performance associados a alocação dinâmica de objetos membro?

Os três eventos estão listados na FAQ anterior.
  • Por si só, uma camada extra de acesso indireto representa um custo irrelevante.
  • Alocação dinâmica de memória pode gerar um problema de performance - a performance típica da implementação de malloc() degrada-se quando há várias alocações; se você não tiver cuidado, o programa OO pode facilmente esgotar a memória disponível para alocação dinâmica.
  • A ligação dinâmica extra decorre de se ter um pointer ao invés de um objeto. Sempre que o C++ pode determinar a classe específica do objeto, as chamadas a funções virtual podem ser feitas por ligação estática, o que permite o uso do mecanismo inline. O mecanismo inline permite inúmeras de oportunidades de otimização. O compilador C++ pode determinar a classe específica de um objeto em três circunstâncias: variáveis locais, variáveis global/static e objetos membro plenamente contidos

Os objetos membro plenamente contidos permitem otimizações significativas, que não seriam possíveis com a abordagem objetos membro via pointer. Essa é a principal razão pela qual as linguagens que impõem a semântica de referências visam melhoria de performance.

Por favor, leia as próximas 3 FAQs dessa seção para ter uma perspectiva equilibrada sobre esse assunto.    

Topo

[ 28.6 ] Funções membro inline virtual são realmente executadas em modo inline?

Ocasionalmente.

Quando um objeto é referenciado via um pointer ou uma referência, uma chamada para uma função virtual não pode ser feita em modo inline, já que a chamada deverá ser resolvida dinamicamente. Por quê: o compilador não pode determinar qual o código realmente chamar até o momento da execução do programa (ou seja, dinamicamente) já que o código pode pertencer a uma classe derivada, que foi criada após a compilação do código chamador.

Portanto a única situação em que a chamada a uma função inline virtual pode ser processada de modo inline é quando o compilador conhece a classe específica do objeto alvo da chamada a função virtual. Isso acontece apenas quando o compilador tem um objeto real, ao invés de um pointer ou uma referência para o objeto. Ou seja, uma variável local, um objeto global/static ou um objeto plenamente contido dentro de uma composição.

Note que a diferença entre o modo inline e o modo não-inline é normalmente mais significativa do que a diferença entre uma chamada a uma função normal e a chamada a uma função virtual. Por exemplo, a diferença entre uma chamada a uma função normal e a chamada a uma função virtual é de duas referências extras a endereços de memória, mas a diferença entre uma função inline e uma função não-inline pode ser de uma magnitude muito maior (para ziliões de chamadas para funções membro insignificantes, a perda de oportunidade de invocar as funções virtual de modo inline pode resultar em degradação de performance de até 25 vêzes. Vide Doug Lea, "Customization in C++," proc Usenix C++ 1990)

Uma conseqüência prática dessa percepção: não se perca em discussões intermináveis (ou em táticas de vendas) com vendedores de compiladores que comparam o custo de uma chamada a uma função virtual em sua linguagem com o de uma outra linguagem ou compilador. Tais comparações não tem qualquer quando comparadas com a habilidade do compilador/linguagem para expandir chamadas inline de funções membro. Ou seja, os vendedores de algumas implementações de linguagem fazem um grande alarde sobre o quanto é boa sua estratégia de invocação de funções, mas se a implementação não faz chamadas a funções membro de modo inline, a performance geral do sistema será pobre, já que é o modo inline, e não a estratégia de ativação, que tem grande impacto na performance. 

Por favor, leia as próximas 2 FAQs dessa seção para ver o outro lado dessa moeda.

Topo
[ 28.7 ] Parece-me então que eu nunca deveria usar semântica de referência. Certo?

Errado.

Semântica de referência é uma boa coisa. Nós não podemos viver sem pointers. Apenas não queremos que nosso software se prejudique por usar pointers. Em C++ você pode escolher onde usar semântica de referência (pointers/referências) e onde usar semântica de valor (onde os objetos contêm fisicamente outros objetos). Em grandes sistemas é necessário procurar uma posição de equilíbrio. Se você implementar absolutamente tudo via pointer, você terá um enorme impacto na performance do sistema.

Os objetos que levam o programa a cair nesse problema são os objetos maiores em tamanho, mais do que os objetos de alto nível. Identificar as abstrações que podem gerar problemas de espaço é normalmente mais importante do que querer manejar uma cópia real do objeto. Semântica de referência pode ser aplicada aos objetos que levam a problemas de espaço.

Note que esses objetos com problema de espaço estão normalmente em um nível de abstração mais alto do que os objetos que não criam problemas de espaço, de modo que os objetos com problema de espaço têm uma freqüência menor de interações. C++ nos dá uma situação ideal: nós escolhemos semântica de referência para objetos que precisam apenas ser identificados ou para aqueles que são muito grandes para serem copiados, e escolhemos semântica de valor para os outros objetos. Assim os objetos usados com maior freqüência são processados com semântica de valor. Colocamos flexibilidade onde não nos prejudica, e performance onde mais precisamos.  

Essas são algumas questões que surgem em projetos reais OO. O domínio de OO/C++ leva tempo e treinamento de alta qualidade. Se você quer usar uma ferramenta poderosa, você tem que investir em seu aprendizado.

Não pare agora. Leia também a próxima FAQ.

Topo
[ 28.8 ] A baixa performance de semântica de referência significa que eu devo passar parâmetros por valor?

Não.

As FAQs anteriores tratam de objetos membro, não de parâmetros. Geralmente, objetos que fazem parte de uma hierarquia de herança devem ser passados por referência ou por pointer, não por valor, porque somente referência lhe permite (a desejada) ligação dinâmica. Passar parâmetros por valor não é aconselhável em herança, porque objetos muito grandes de subclasses são repartidos quando passados por valor como um objeto da classe base.

A menos que outras razões o obriguem ao contrário, objetos membro devem ser por valor e parâmetros devem ser por referência. A discussão nas FAQs anteriores indicaram algumas das razões que podem forçar a que objetos membro sejam por referência.

Topo Anterior Próximo Índice
C++ FAQ Lite
Copyright © 1991-98 by Marshall Cline Ph.D., cline@parashift.com
Tradução: Dagoberto Haele Arnaut

| Home | Bookmarks | Universidades | Para Saber mais | Universidades | WEB Directory | Mapa do site |