TL; DR: Passar pela referência const ainda é uma boa idéia em C ++, considerando todas as coisas. Não é uma otimização prematura.
TL; DR2: A maioria dos provérbios não faz sentido, até que eles façam.
Alvo
Esta resposta apenas tenta estender o item vinculado nas Diretrizes Principais do C ++ (mencionadas pela primeira vez no comentário da amon) um pouco.
Esta resposta não tenta abordar a questão de como pensar e aplicar adequadamente os vários provérbios que foram amplamente divulgados nos círculos dos programadores, especialmente a questão da reconciliação entre conclusões ou evidências conflitantes.
Aplicabilidade
Esta resposta se aplica apenas a chamadas de função (escopos aninhados não destacáveis no mesmo encadeamento).
(Nota lateral.) Quando coisas passáveis podem escapar do escopo (ou seja, ter uma vida útil que potencialmente exceda o escopo externo), torna-se mais importante satisfazer a necessidade do aplicativo de gerenciamento da vida útil do objeto antes de qualquer outra coisa. Geralmente, isso requer o uso de referências que também são capazes de gerenciar a vida útil, como ponteiros inteligentes. Uma alternativa pode estar usando um gerente. Observe que lambda é um tipo de escopo destacável; As capturas lambda se comportam como ter escopo de objeto. Portanto, tenha cuidado com capturas lambda. Também tenha cuidado com o modo como o próprio lambda é passado - por cópia ou por referência.
Quando passar por valor
Para valores escalares (primitivas padrão que se ajustam a um registro de máquina e têm valor semântico) para os quais não há necessidade de comunicação por mutabilidade (referência compartilhada), passe por valor.
Para situações em que o chamado exige uma clonagem de um objeto ou agregado, passe por valor, em que a cópia do chamado atende à necessidade de um objeto clonado.
Quando passar por referência, etc.
para todas as outras situações, passe por ponteiros, referências, ponteiros inteligentes, alças (consulte: idioma do corpo da alça), etc. Sempre que este conselho for seguido, aplique o princípio da correção constante como de costume.
Coisas (agregados, objetos, matrizes, estruturas de dados) que são suficientemente grandes no espaço ocupado pela memória devem sempre ser projetadas para facilitar a passagem por referência, por razões de desempenho. Esse conselho se aplica definitivamente quando tem centenas de bytes ou mais. Este conselho é limítrofe quando tem dezenas de bytes.
Paradigmas incomuns
Existem paradigmas de programação para fins especiais, que são pesados por intenção. Por exemplo, processamento de strings, serialização, comunicação de rede, isolamento, quebra de bibliotecas de terceiros, comunicação entre processos de memória compartilhada, etc. Nessas áreas de aplicação ou paradigmas de programação, os dados são copiados de estruturas em estruturas ou, às vezes, reembalados matrizes de bytes.
Como a especificação do idioma afeta essa resposta antes que a otimização seja considerada.
Sub-TL; DR A propagação de uma referência não deve invocar nenhum código; passar por const-reference satisfaz este critério. No entanto, todos os outros idiomas atendem a esse critério sem esforço.
(Programadores iniciantes em C ++ são aconselhados a ignorar esta seção completamente.)
(O início desta seção é parcialmente inspirado na resposta de gnasher729. No entanto, uma conclusão diferente é alcançada.)
O C ++ permite construtores de cópia definidos pelo usuário e operadores de atribuição.
(Esta é (foi) uma escolha ousada que é (foi) incrível e lamentável. É definitivamente uma divergência da norma aceitável de hoje em design de linguagem.)
Mesmo que o programador C ++ não defina um, o compilador C ++ deve gerar esses métodos com base nos princípios da linguagem e determinar se outro código precisa ser executado além de memcpy
. Por exemplo, um class
/ struct
que contém um std::vector
membro precisa ter um construtor de cópias e um operador de atribuição que não sejam triviais.
Em outros idiomas, os construtores de cópias e a clonagem de objetos são desencorajados (exceto quando absolutamente necessário e / ou significativo para a semântica do aplicativo), porque os objetos têm semântica de referência, por design da linguagem. Esses idiomas geralmente têm um mecanismo de coleta de lixo baseado na acessibilidade, em vez de propriedade baseada no escopo ou contagem de referência.
Quando uma referência ou ponteiro (incluindo referência const) é passada em C ++ (ou C), o programador tem a garantia de que nenhum código especial (funções definidas pelo usuário ou funções geradas pelo compilador) será executado, além da propagação do valor do endereço (referência ou ponteiro). Essa é uma clareza de comportamento com a qual os programadores de C ++ acham confortável.
No entanto, o pano de fundo é que a linguagem C ++ é desnecessariamente complicada, de modo que essa clareza de comportamento é como um oásis (um habitat sobrevivível) em algum lugar em torno de uma zona de precipitação nuclear.
Para adicionar mais bênçãos (ou insulto), o C ++ introduz referências universais (valores-r) para facilitar os operadores de movimentação definidos pelo usuário (construtores de movimentação e operadores de atribuição de movimentação) com bom desempenho. Isso beneficia um caso de uso altamente relevante (a movimentação (transferência) de objetos de uma instância para outra), por meio da redução da necessidade de cópia e clonagem profunda. No entanto, em outras línguas, é ilógico falar dessa movimentação de objetos.
(Seção fora do tópico) Uma seção dedicada a um artigo, "Quer velocidade? Passe por valor!" escrito por volta de 2009.
Esse artigo foi escrito em 2009 e explica a justificativa de design para o valor r em C ++. Esse artigo apresenta um contra-argumento válido para minha conclusão na seção anterior. No entanto, o exemplo de código do artigo e a declaração de desempenho foram refutados há muito tempo.
Sub-TL; DR O design da semântica de valor-r em C ++ permite uma semântica surpreendentemente elegante do lado do usuário em uma Sort
função, por exemplo. Este elegante é impossível de modelar (imitar) em outras línguas.
Uma função de classificação é aplicada a toda uma estrutura de dados. Como mencionado acima, seria lento se houver muitas cópias envolvidas. Como uma otimização de desempenho (que é praticamente relevante), uma função de classificação é projetada para ser destrutiva em várias linguagens além do C ++. Destrutivo significa que a estrutura de dados de destino é modificada para atingir a meta de classificação.
No C ++, o usuário pode optar por chamar uma das duas implementações: uma destrutiva com melhor desempenho ou uma normal que não modifica a entrada. (O modelo é omitido por questões de brevidade.)
/*caller specifically passes in input argument destructively*/
std::vector<T> my_sort(std::vector<T>&& input)
{
std::vector<T> result(std::move(input)); /* destructive move */
std::sort(result.begin(), result.end()); /* in-place sorting */
return result; /* return-value optimization (RVO) */
}
/*caller specifically passes in read-only argument*/
std::vector<T> my_sort(const std::vector<T>& input)
{
/* reuse destructive implementation by letting it work on a clone. */
/* Several things involved; e.g. expiring temporaries as r-value */
/* return-value optimization, etc. */
return my_sort(std::vector<T>(input));
}
/*caller can select which to call, by selecting r-value*/
std::vector<T> v1 = {...};
std::vector<T> v2 = my_sort(v1); /*non-destructive*/
std::vector<T> v3 = my_sort(std::move(v1)); /*v1 is gutted*/
Além da classificação, essa elegância também é útil na implementação do algoritmo destrutivo de mediana de localização em uma matriz (inicialmente não classificada), por particionamento recursivo.
No entanto, observe que a maioria dos idiomas aplicaria uma abordagem de árvore de pesquisa binária equilibrada à classificação, em vez de aplicar um algoritmo destrutivo de classificação às matrizes. Portanto, a relevância prática dessa técnica não é tão alta quanto parece.
Como a otimização do compilador afeta esta resposta
Quando o inlining (e também a otimização de todo o programa / otimização do tempo do link) é aplicado em vários níveis de chamadas de função, o compilador pode ver (às vezes exaustivamente) o fluxo de dados. Quando isso acontece, o compilador pode aplicar muitas otimizações, algumas das quais podem eliminar a criação de objetos inteiros na memória. Normalmente, quando essa situação se aplica, não importa se os parâmetros são passados por valor ou por referência constante, porque o compilador pode analisar exaustivamente.
No entanto, se a função de nível inferior chamar algo que está além da análise (por exemplo, algo em uma biblioteca diferente fora da compilação ou um gráfico de chamada que seja simplesmente muito complicado), o compilador deverá otimizar defensivamente.
Objetos maiores que um valor de registro de máquina podem ser copiados por instruções explícitas de carregamento / armazenamento de memória ou por uma chamada para a memcpy
função venerável . Em algumas plataformas, o compilador gera instruções SIMD para mover-se entre dois locais de memória, cada instrução movendo dezenas de bytes (16 ou 32).
Discussão sobre a questão da verbosidade ou desordem visual
Os programadores de C ++ estão acostumados a isso, ou seja, desde que um programador não odeie C ++, a sobrecarga de escrever ou ler referências constantes no código fonte não é horrível.
As análises de custo-benefício podem ter sido feitas muitas vezes antes. Não sei se há algum científico que deva ser citado. Eu acho que a maioria das análises seria não científica ou não reproduzível.
Aqui está o que eu imagino (sem provas ou referências credíveis) ...
- Sim, isso afeta o desempenho do software escrito neste idioma.
- Se os compiladores puderem entender o objetivo do código, ele poderá ser inteligente o suficiente para automatizar esse código.
- Infelizmente, em linguagens que favorecem a mutabilidade (em oposição à pureza funcional), o compilador classificaria a maioria das coisas como sendo mutadas; portanto, a dedução automática da constância rejeitaria a maioria das coisas como não-const
- A sobrecarga mental depende das pessoas; as pessoas que consideram isso uma sobrecarga mental alta teriam rejeitado o C ++ como uma linguagem de programação viável.