É uma prática recomendada escrever código que se baseia em otimizações do compilador?

99

Eu tenho aprendido um pouco de C ++ e geralmente tenho que retornar objetos grandes de funções criadas dentro da função. Sei que há a passagem por referência, retorno um ponteiro e retorno de soluções de tipo de referência, mas também li que os compiladores C ++ (e o padrão C ++) permitem otimização do valor de retorno, o que evita a cópia desses grandes objetos na memória, assim economizando tempo e memória de tudo isso.

Agora, sinto que a sintaxe é muito mais clara quando o objeto é explicitamente retornado por valor, e o compilador geralmente empregará o RVO e tornará o processo mais eficiente. É uma má prática confiar nessa otimização? Isso torna o código mais claro e legível para o usuário, o que é extremamente importante, mas devo tomar cuidado ao assumir que o compilador capturará a oportunidade do RVO?

Isso é uma micro-otimização ou algo que eu deveria ter em mente ao criar meu código?

Matt
fonte
7
Para responder à sua edição, é uma micro otimização, porque mesmo se você tentasse comparar o que está ganhando em nanossegundos, mal conseguiria vê-lo. Quanto ao resto, sou muito podre em C ++ para fornecer uma resposta estrita do porquê não funcionaria. Um deles é provável que exista casos em que você precise de alocação dinâmica e, portanto, use novas / ponteiros / referências.
21317 Walfrat
4
@ Walfrat, mesmo que os objetos sejam muito grandes, da ordem de megabytes? Minhas matrizes podem ficar enormes devido à natureza dos problemas que estou resolvendo.
Matt
6
@ Matt eu não faria. Referências / indicadores existem precisamente para isso. As otimizações do compilador devem estar além do que os programadores devem levar em consideração ao criar um programa, mesmo que sim, muitas vezes os dois mundos se sobrepõem.
Neil
5
@ Matt A menos que você esteja fazendo algo extremamente específico que suponha que exija desenvolvedores com mais de 10 anos de experiência em C / kernels, baixas interações de hardware, você não precisa disso. Se você acha que pertence a algo muito específicos, editar a sua mensagem e adicionar uma descrição exata do que você está aplicativo é suposto fazer (em tempo real computação matemática pesada ...?)
Walfrat
37
No caso particular do CVO (N) RVO, sim, contar com essa otimização é perfeitamente válido. Isso ocorre porque o padrão C ++ 17 exige especificamente que isso aconteça, nas situações em que os compiladores modernos já o estavam fazendo.
Caleth

Respostas:

130

Empregue o princípio de menor espanto .

É você e somente sempre você que usará esse código e tem certeza de que em 3 anos não ficará surpreso com o que faz?

Então vá em frente.

Em todos os outros casos, use o caminho padrão; caso contrário, você e seus colegas terão dificuldades em encontrar bugs.

Por exemplo, meu colega estava reclamando que meu código estava causando erros. Acontece que ele havia desativado a avaliação booleana de curto-circuito nas configurações do compilador. Eu quase dei um tapa nele.

Pieter B
fonte
88
@ Neil esse é o meu ponto, todos confiam na avaliação de curto-circuito. E você não precisa pensar duas vezes sobre isso, deve estar ligado. É um padrão defacto. Sim, você pode mudar, mas não deveria.
Pieter B
49
"Eu mudei como a linguagem funciona, e seu código podre e sujo quebrou! Arghh!" Uau. Tapa seria apropriado, envie seu colega para o treinamento Zen, há muito disso lá.
109
@PieterB Tenho certeza de que as especificações das linguagens C e C ++ garantem uma avaliação de curto-circuito. Portanto, não é apenas um padrão de fato, é o padrão. Sem ele, você não está mais usando C / C ++, mas algo que é suspeito: P
marcelm
47
Apenas para referência, a maneira padrão aqui é retornar por valor.
22817 DeadMG
28
@ dan04 sim, estava em Delphi. Gente, não se envolvam no exemplo, é sobre o que eu disse. Não faça coisas surpreendentes que ninguém mais faça.
Pieter B
81

Para esse caso em particular, definitivamente retorne pelo valor.

  • RVO e NRVO são otimizações conhecidas e robustas que realmente devem ser feitas por qualquer compilador decente, mesmo no modo C ++ 03.

  • A semântica de movimentação garante que os objetos sejam removidos das funções se (N) RVO não ocorrer. Isso é útil apenas se o seu objeto usa dados dinâmicos internamente (como std::vectorfaz), mas que deve realmente ser o caso, se é que grande - transbordando a pilha é um risco com grandes objetos automáticas.

  • O C ++ 17 impõe o RVO. Portanto, não se preocupe, ele não desaparecerá e só terminará de se estabelecer completamente quando os compiladores estiverem atualizados.

E, no final, forçar uma alocação dinâmica adicional para retornar um ponteiro ou forçar o tipo de resultado a ser construtível por padrão, apenas para que você possa passá-lo como parâmetro de saída são soluções feias e não-idiomáticas para um problema que você provavelmente nunca ter.

Basta escrever um código que faça sentido e agradecer aos escritores do compilador por otimizar corretamente o código que faz sentido.

Quentin
fonte
9
Apenas por diversão, veja como o Borland Turbo C ++ 3.0 de 1990-ish lida com RVO . Spoiler: Basicamente, funciona muito bem.
nwp 12/10
9
A chave aqui é que não se trata de uma otimização aleatória específica de compilador ou "recurso não documentado", mas algo que, embora tecnicamente opcional em várias versões do padrão C ++, tenha sido fortemente pressionado pelo setor e praticamente todos os principais compiladores fizeram isso por muito tempo.
7
Essa otimização não é tão robusta quanto se pode gostar. Sim, é bastante confiável nos casos mais óbvios, mas, por exemplo, no bugzilla do gcc, existem muitos casos pouco menos óbvios em que isso é esquecido.
Marc Glisse
62

Agora, sinto que a sintaxe é muito mais clara quando o objeto é explicitamente retornado por valor, e o compilador geralmente empregará o RVO e tornará o processo mais eficiente. É uma má prática confiar nessa otimização? Isso torna o código mais claro e legível para o usuário, o que é extremamente importante, mas devo tomar cuidado ao assumir que o compilador capturará a oportunidade do RVO?

Essa não é uma micro-otimização pouco conhecida, fofa, sobre a qual você lê em algum blog pequeno e pouco trafegado, e então você se sente inteligente e superior em usar.

Após o C ++ 11, o RVO é a maneira padrão de escrever esse código de código. É comum, esperado, ensinado, mencionado em conversas, mencionado em blogs, mencionados no padrão, que será relatado como um bug do compilador, se não implementado. No C ++ 17, a linguagem vai um passo além e exige a eliminação de cópias em determinados cenários.

Você deve confiar absolutamente nessa otimização.

Além disso, o retorno por valor apenas leva a códigos muito mais fáceis de ler e gerenciar do que o código retornado por referência. A semântica de valores é uma coisa poderosa, que por si só pode levar a mais oportunidades de otimização.

Barry
fonte
3
Obrigado, isso faz muito sentido e é consistente com o "princípio de menor espanto" mencionado acima. Isso tornaria o código muito claro e compreensível, além de dificultar a bagunça com as travessuras dos ponteiros.
Matt
3
@ Matt Parte do motivo de eu ter votado positivamente nesta resposta é que ela menciona "semântica de valores". À medida que você obtém mais experiência em C ++ (e programação em geral), encontrará situações ocasionais em que a semântica de valores não pode ser usada para determinados objetos porque eles são mutáveis ​​e suas alterações precisam ser tornadas visíveis para outro código que usa o mesmo objeto (um exemplo de "mutabilidade compartilhada"). Quando essas situações ocorrem, os objetos afetados precisarão ser compartilhados por meio de ponteiros (inteligentes).
rwong
16

A correção do código que você escreve nunca deve depender de uma otimização. Ele deve gerar o resultado correto quando executado na "máquina virtual" C ++ que eles usam na especificação.

No entanto, o que você fala é mais um tipo de pergunta sobre eficiência. Seu código funciona melhor se otimizado com um compilador de otimização de RVO. Tudo bem, por todos os motivos apontados nas outras respostas.

No entanto, se você precisar dessa otimização (como se o construtor de cópias realmente causasse falha no seu código), agora você está nos caprichos do compilador.

Eu acho que o melhor exemplo disso em minha própria prática é a otimização de chamada de cauda:

   int sillyAdd(int a, int b)
   {
      if (b == 0)
          return a;
      return sillyAdd(a + 1, b - 1);
   }

É um exemplo bobo, mas mostra uma chamada de cauda, ​​onde uma função é chamada recursivamente no final de uma função. A máquina virtual C ++ mostrará que esse código funciona corretamente, embora eu possa causar um pouco de confusão sobre o motivo de eu ter me preocupado em escrever essa rotina de adição em primeiro lugar. No entanto, nas implementações práticas do C ++, temos uma pilha e esse espaço é limitado. Se executada de forma pedântica, essa função teria que empurrar pelo menos b + 1quadros de pilha para a pilha, como faz sua adição. Se eu quiser calcular sillyAdd(5, 7), isso não é grande coisa. Se eu quiser calcular sillyAdd(0, 1000000000), posso ter um problema real de causar um StackOverflow (e não o bom ).

No entanto, podemos ver que, quando alcançamos a última linha de retorno, realmente terminamos com tudo no quadro de pilha atual. Nós realmente não precisamos mantê-lo por perto. A otimização da chamada de cauda permite "reutilizar" o quadro de pilha existente para a próxima função. Dessa forma, precisamos apenas de 1 quadro de pilha, em vez de b+1. (Ainda precisamos fazer todas essas adições e subtrações tolas, mas elas não ocupam mais espaço.) Com efeito, a otimização transforma o código em:

   int sillyAdd(int a, int b)
   {
      begin:
      if (b == 0)
          return a;
      // return sillyAdd(a + 1, b - 1);
      a = a + 1;
      b = b - 1;
      goto begin;  
   }

Em alguns idiomas, a otimização da chamada de cauda é explicitamente exigida pela especificação. C ++ não é um desses. Não posso confiar nos compiladores C ++ para reconhecer esta oportunidade de otimização de chamada final, a menos que eu vá caso a caso. Com a minha versão do Visual Studio, a versão de lançamento faz a otimização de chamada de cauda, ​​mas a versão de depuração não (por design).

Assim, seria ruim para mim depender de poder calcular sillyAdd(0, 1000000000).

Cort Ammon
fonte
2
Este é um caso interessante, mas não acho que você possa generalizá-lo para a regra em seu primeiro parágrafo. Suponha que eu tenha um programa para um dispositivo pequeno, que carregará se e somente se eu usar as otimizações de redução de tamanho do compilador - é errado fazer isso? parece bastante pedante dizer que minha única opção válida é reescrevê-la no assembler, especialmente se essa reescrita fizer as mesmas coisas que o otimizador faz para resolver o problema.
sdenham
5
@denham Suponho que haja um pouco de espaço na discussão. Se você não está mais escrevendo para "C ++", mas escrevendo para "versão do compilador WindRiver C ++ versão 3.4.1", então eu posso ver a lógica lá. No entanto, como regra geral, se você estiver escrevendo algo que não funciona corretamente de acordo com as especificações, você está em um tipo muito diferente de cenário. Eu sei que a biblioteca Boost tem código assim, mas eles sempre o colocam em #ifdefblocos e têm uma solução compatível com os padrões disponíveis.
Cort Ammon
4
isso é um erro de digitação no segundo bloco de código onde diz b = b + 1?
Stib #
2
Você pode explicar o que quer dizer com "máquina virtual C ++", pois esse não é um termo usado em nenhum documento padrão. Eu acho que você está falando sobre o modelo de execução do C ++, mas não completamente certo - e seu termo é enganosamente semelhante a uma "máquina virtual de bytecode" que se refere a algo totalmente diferente.
Toby Speight
1
O @supercat Scala também possui sintaxe explícita de recursão da cauda. O C ++ é o seu próprio animal, mas acho que a recursão da cauda é unidiomatic para linguagens não funcionais e obrigatória para linguagens funcionais, deixando um pequeno conjunto de linguagens onde é razoável ter uma sintaxe explícita da recursão da cauda. Traduzir literalmente a recursão da cauda em loops e a mutação explícita simplesmente é uma opção melhor para muitos idiomas.
Prosfilaes # 15/17
8

Na prática, os programas C ++ esperam algumas otimizações do compilador.

Observe os cabeçalhos padrão das implementações de contêineres padrão . Com o GCC , você pode solicitar o formulário pré-processado ( g++ -C -E) e a representação interna do GIMPLE ( g++ -fdump-tree-gimpleou Gimple SSA com -fdump-tree-ssa) da maioria dos arquivos de origem (tecnicamente unidades de tradução) usando contêineres. Você ficará surpreso com a quantidade de otimização realizada (com g++ -O2). Portanto, os implementadores de contêineres confiam nas otimizações (e na maioria das vezes, o implementador de uma biblioteca padrão C ++ sabe qual otimização aconteceria e grava a implementação do contêiner com isso em mente; às vezes ele também escreve a passagem de otimização no compilador para lidar com os recursos exigidos pela biblioteca C ++ padrão).

Na prática, são as otimizações do compilador que tornam o C ++ e seus contêineres padrão suficientemente eficientes. Então você pode confiar neles.

E da mesma forma para o caso RVO mencionado em sua pergunta.

O padrão C ++ foi co-projetado (principalmente experimentando otimizações boas o suficiente ao propor novos recursos) para funcionar bem com as possíveis otimizações.

Por exemplo, considere o programa abaixo:

#include <algorithm>
#include <vector>

extern "C" bool all_positive(const std::vector<int>& v) {
  return std::all_of(v.begin(), v.end(), [](int x){return x >0;});
}

compile com g++ -O3 -fverbose-asm -S. Você descobrirá que a função gerada não executa nenhuma CALLinstrução da máquina. Assim, a maioria passos C ++ (construção de um fecho de lambda, a sua aplicação repetida, recebendo o begine enditeradores, etc ...) foram otimizados. O código da máquina contém apenas um loop (que não aparece explicitamente no código-fonte). Sem essas otimizações, o C ++ 11 não será bem-sucedido.

adendas

(dezembro adicionado 31 r 2017)

Veja CppCon 2017: Matt Godbolt “O que meu compilador fez por mim ultimamente? Abrir a tampa do compilador ” .

Basile Starynkevitch
fonte
4

Sempre que você usa um compilador, o entendimento é que ele produzirá código de máquina ou de byte para você. Ele não garante nada sobre como é esse código gerado, exceto que implementará o código-fonte de acordo com a especificação da linguagem. Observe que essa garantia é a mesma, independentemente do nível de otimização usado e, portanto, em geral, não há razão para considerar uma saída como mais 'correta' que a outra.

Além disso, nesses casos, como o RVO, onde é especificado no idioma, parece inútil evitar o uso, principalmente se isso simplificar o código-fonte.

Muito esforço é feito para fazer os compiladores produzirem resultados eficientes, e claramente a intenção é que esses recursos sejam usados.

Pode haver razões para o uso de código não otimizado (para depuração, por exemplo), mas o caso mencionado nesta pergunta não parece ser um (e se o seu código falhar apenas quando otimizado, isso não é uma consequência de alguma peculiaridade do dispositivo em que está sendo executado, existe um erro em algum lugar e é improvável que esteja no compilador.)

Sdenham
fonte
3

Acho que outros abordaram bem o ângulo específico sobre C ++ e RVO. Aqui está uma resposta mais geral:

Quando se trata de correção, você não deve confiar nas otimizações do compilador ou no comportamento específico do compilador em geral. Felizmente, você parece não estar fazendo isso.

Quando se trata de desempenho, você precisa confiar no comportamento específico do compilador em geral e nas otimizações do compilador em particular. Um compilador compatível com o padrão é livre para compilar seu código da maneira que desejar, desde que o código compilado se comporte de acordo com a especificação da linguagem. E não conheço nenhuma especificação para uma linguagem convencional que especifique a rapidez com que cada operação deve ser.

svick
fonte
1

As otimizações do compilador devem afetar apenas o desempenho, não os resultados. Confiar nas otimizações do compilador para atender a requisitos não funcionais não é apenas razoável, é frequentemente a razão pela qual um compilador é escolhido em detrimento de outro.

Os sinalizadores que determinam como determinadas operações são executadas (condições de índice ou estouro, por exemplo), são frequentemente agrupados com otimizações do compilador, mas não devem ser. Eles explicitamente efetuam os resultados dos cálculos.

Se uma otimização do compilador causar resultados diferentes, isso é um erro - um erro no compilador. Confiar em um bug no compilador é, a longo prazo, um erro - o que acontece quando ele é corrigido?

O uso de sinalizadores do compilador que alteram o funcionamento dos cálculos deve ser bem documentado, mas usado conforme necessário.

jmoreno
fonte
Infelizmente, muita documentação do compilador faz um mau trabalho ao especificar o que é ou não garantido em vários modos. Além disso, os escritores de compiladores "modernos" parecem alheios às combinações de garantias que os programadores fazem e não precisam. Se um programa funcionaria bem se, x*y>zarbitrariamente, produzir 0 ou 1 em caso de estouro, desde que não tenha outros efeitos colaterais , exigindo que um programador evite estouros a todo custo ou force o compilador a avaliar a expressão de uma maneira específica. desnecessário prejudicar otimizações vs. dizer que ... #
308
... o compilador pode em seu lazer se comportam como se x*ypromove seus operandos para alguns mais tipo arbitrário (permitindo assim formas de elevação e redução da força que iria mudar o comportamento de alguns casos de excesso). Muitos compiladores, no entanto, exigem que os programadores impeçam o estouro a todo custo ou forçam os compiladores a truncar todos os valores intermediários em caso de estouro.
supercat
1

Não.

É o que faço o tempo todo. Se eu precisar acessar um bloco arbitrário de 16 bits na memória, faço isso

void *ptr = get_pointer();
uint16_t u16;
memcpy(&u16, ptr, sizeof(u16)); // ntohs omitted for simplicity

... e confie no compilador fazendo o possível para otimizar esse pedaço de código. O código funciona em ARM, i386, AMD64 e praticamente em todas as arquiteturas existentes. Em teoria, um compilador não otimizador poderia realmente chamar memcpy, resultando em um desempenho totalmente ruim, mas isso não é problema para mim, pois uso otimizações do compilador.

Considere a alternativa:

void *ptr = get_pointer();
uint16_t *u16ptr = ptr;
uint16_t u16;
u16 = *u16ptr;  // ntohs omitted for simplicity

Esse código alternativo falha ao trabalhar em máquinas que exigem alinhamento adequado, se get_pointer()retornar um ponteiro não alinhado. Além disso, pode haver problemas de alias na alternativa.

A diferença entre -O2 e -O0 ao usar o memcpytruque é grande: 3,2 Gbps de desempenho de soma de verificação IP versus 67 Gbps de desempenho de soma de verificação IP. Sobre uma diferença de ordem de magnitude!

Às vezes você pode precisar ajudar o compilador. Portanto, por exemplo, em vez de confiar no compilador para desenrolar loops, você pode fazer isso sozinho. Ou implementando o famoso dispositivo de Duff , ou de uma maneira mais limpa.

A desvantagem de confiar nas otimizações do compilador é que, se você executar o gdb para depurar seu código, poderá descobrir que muito foi otimizado. Portanto, pode ser necessário recompilar com -O0, o que significa que o desempenho será totalmente ruim ao depurar. Eu acho que isso é uma desvantagem, considerando os benefícios de otimizar compiladores.

Faça o que fizer, verifique se o seu caminho não é realmente um comportamento indefinido. Certamente, acessar algum bloco aleatório de memória como número inteiro de 16 bits é um comportamento indefinido devido a problemas de alias e alinhamento.

juhist
fonte
0

Todas as tentativas de código eficiente escritas em qualquer coisa, exceto em montagem, dependem muito, muito intensamente, das otimizações do compilador, começando com a alocação de registro mais básica, como eficiente, para evitar derramamentos de pilha supérfluos por todo o lado e pelo menos razoavelmente boa, se não excelente, seleção de instruções. Caso contrário, estaríamos de volta aos anos 80, onde tínhamos que colocar registersugestões em todo o lugar e usar o número mínimo de variáveis ​​em uma função para ajudar os compiladores C arcaicos ou ainda mais cedo quando gotoera uma otimização de ramificação útil.

Se não sentíssemos que poderíamos confiar na capacidade do otimizador de otimizar nosso código, todos nós ainda estaríamos codificando caminhos de execução críticos de desempenho na montagem.

É realmente uma questão de quão confiável você sente que a otimização pode ser feita, o que é melhor resolvido através da criação de perfil e olhando para os recursos dos compiladores que você possui e, possivelmente, até desmontando se houver um ponto de acesso que você não consegue descobrir onde o compilador parece. falharam ao fazer uma otimização óbvia.

O RVO é algo que existe há muito tempo e, pelo menos excluindo casos muito complexos, é algo que os compiladores têm se aplicado de maneira confiável há muito tempo. Definitivamente, não vale a pena solucionar um problema que não existe.

Errar do lado de confiar no otimizador, sem medo dele

Pelo contrário, eu diria que o erro é confiar demais nas otimizações do compilador, e essa sugestão vem de um profissional que trabalha em campos muito críticos para o desempenho, nos quais a eficiência, a capacidade de manutenção e a qualidade percebida entre os clientes é muito alta. todo um borrão gigante. Prefiro que você confie com muita confiança no seu otimizador e encontre alguns casos obscuros em que confiava demais do que confiava muito pouco e apenas codificasse medos supersticiosos o tempo todo pelo resto de sua vida. Pelo menos, você terá que procurar um criador de perfil e investigar adequadamente se as coisas não forem executadas tão rapidamente quanto deveriam e obter um conhecimento valioso, não superstições, ao longo do caminho.

Você está se saindo bem com o otimizador. Mantem. Não se torne o cara que começa a solicitar explicitamente todas as funções chamadas em loop antes mesmo de criar um perfil de um medo equivocado das falhas do otimizador.

Criação de perfil

A criação de perfil é realmente a resposta indireta, mas definitiva, à sua pergunta. O problema que os iniciantes que desejam escrever código eficiente frequentemente enfrenta não é o que otimizar, é o que não otimizar, porque eles desenvolvem todo tipo de palpites equivocados sobre ineficiências que, embora humanamente intuitivas, são computacionalmente erradas. O desenvolvimento da experiência com um criador de perfil começará realmente a dar uma apreciação adequada não apenas dos recursos de otimização de seus compiladores, nos quais você pode confiar com confiança, mas também dos recursos (e também das limitações) do seu hardware. É indiscutível ainda mais valor na criação de perfis para aprender o que não valia a pena otimizar do que aprender o que era.


fonte
-1

O software pode ser escrito em C ++ em plataformas muito diferentes e para diversos fins.

Depende completamente da finalidade do software. Deve ser fácil manter, expandir, corrigir, refatorar etc. ou outras coisas mais importantes, como desempenho, custo ou compatibilidade com algum hardware específico ou o tempo necessário para o desenvolvimento.

mathreadler
fonte
-2

Eu acho que a resposta chata para isso é: 'depende'.

É uma prática recomendada escrever código que se baseia em uma otimização de compilador que provavelmente será desativada e onde a vulnerabilidade não está documentada e onde o código em questão não é testado por unidade para que, se ele quebrasse, você o conhecesse ? Provavelmente.

É uma prática ruim escrever código que se baseia em uma otimização de compilador que provavelmente não será desativada , documentada e testada por unidade ? Talvez não.

Dave Cousineau
fonte
-6

A menos que haja mais que você não esteja nos dizendo, isso é uma prática ruim, mas não pelo motivo que você sugere.

Possivelmente ao contrário de outras linguagens que você usou anteriormente, retornar o valor de um objeto em C ++ produz uma cópia do objeto. Se você modificar o objeto, estará modificando um objeto diferente . Ou seja, se eu tenho Obj a; a.x=1;e Obj b = a;, então sim b.x += 2; b.f();, a.xainda é igual a 1, não a 3.

Portanto, não, o uso de um objeto como valor e não como referência ou ponteiro não fornece a mesma funcionalidade e você pode acabar com bugs no seu software.

Talvez você saiba disso e isso não afeta negativamente o seu caso de uso específico. No entanto, com base no texto da sua pergunta, parece que você pode não estar ciente da distinção; palavras como "criar um objeto na função".

"criar um objeto na função" soa como new Obj;onde "retornar o objeto por valor" soa comoObj a; return a;

Obj a;e Obj* a = new Obj;são coisas muito, muito diferentes; o primeiro pode resultar em corrupção de memória se não for usado e entendido adequadamente, e o segundo pode resultar em vazamento de memória se não for usado e entendido corretamente.

Aaron
fonte
8
A otimização do valor de retorno (RVO) é uma semântica bem definida, em que o compilador constrói um objeto retornado um nível acima no quadro da pilha, evitando especificamente cópias desnecessárias de objetos. Esse é um comportamento bem definido que foi suportado muito antes de ser mandatado no C ++ 17. Até 10 a 15 anos atrás, todos os principais compiladores suportavam esse recurso e o faziam de forma consistente.
@ Snowman Não estou falando sobre o gerenciamento físico de memória de baixo nível e não discuti inchaço ou velocidade da memória. Como mostrei especificamente em minha resposta, estou falando sobre os dados lógicos. Logicamente , fornecer o valor de um objeto está criando uma cópia dele, independentemente de como o compilador é implementado ou qual assembly é usado nos bastidores. O material de baixo nível dos bastidores é uma coisa, e a estrutura lógica e o comportamento da linguagem são outra; eles estão relacionados, mas não são a mesma coisa - ambos devem ser entendidos.
Aaron
6
sua resposta diz que "retornar o valor de um objeto em C ++ produz uma cópia do objeto", o que é completamente falso no contexto do RVO - o objeto é construído diretamente no local de chamada e nenhuma cópia é feita. Você pode testar isso excluindo o construtor de cópia e retornando o objeto que é construído na returninstrução que é um requisito para o RVO. Além disso, você passa a falar sobre palavras new- chave e ponteiros, o que não é o objetivo do RVO. Eu acredito que você não entende a pergunta, ou RVO, ou possivelmente ambos.
-7

Pieter B está absolutamente correto ao recomendar menos espanto.

Para responder sua pergunta específica, o que isso (provavelmente) significa em C ++ é que você deve retornar std::unique_ptra ao objeto construído.

A razão é que isso é mais claro para um desenvolvedor de C ++ quanto ao que está acontecendo.

Embora sua abordagem provavelmente funcione, você está sinalizando efetivamente que o objeto é um tipo de valor pequeno quando, na verdade, não é. Além disso, você está descartando qualquer possibilidade de abstração de interface. Isso pode ser bom para seus propósitos atuais, mas geralmente é muito útil ao lidar com matrizes.

Compreendo que, se você veio de outras línguas, todos os sigilos podem ser confusos inicialmente. Mas tome cuidado para não supor que, ao não usá-los, você torna seu código mais claro. Na prática, é provável que o oposto seja verdadeiro.

Alex
fonte
Quando em Roma, faça como os romanos.
14
Essa não é uma boa resposta para tipos que não executam alocações dinâmicas. O fato de o OP achar que o natural em seu caso de uso é retornar por valor indica que seus objetos têm duração de armazenamento automático no lado do chamador. Para objetos simples e não muito grandes, mesmo uma implementação ingênua de cópia-retorno-valor será uma ordem de magnitude mais rápida que uma alocação dinâmica. (Se, por outro lado, a função retorna um recipiente, em seguida, o retorno de uma unique_pointer pode mesmo ser vantajosa em comparação com um retorno compilador ingénua pelo valor.)
Peter A. Schneider
9
@ Matt Caso você não tenha percebido que isso não é uma prática recomendada. Fazer alocações de memória desnecessariamente e forçar a semântica de ponteiros nos usuários é ruim.
nwp 12/10
5
Primeiro de tudo, ao usar ponteiros inteligentes, deve-se retornar std::make_unique, não std::unique_ptrdiretamente. Segundo, o RVO não é uma otimização esotérica específica do fornecedor: ele é incorporado ao padrão. Mesmo quando não era, era um comportamento amplamente suportado e esperado. Não faz sentido retornar a std::unique_ptrquando um ponteiro não é necessário.
4
@ Snowman: Não há "quando não era". Embora apenas recentemente tenha se tornado obrigatório , todos os padrões C ++ já reconheceram [N] RVO e fizeram acomodações para habilitá-lo (por exemplo, o compilador sempre recebeu permissão explícita para omitir o uso do construtor de cópia no valor de retorno, mesmo que efeitos colaterais visíveis).
Jerry Coffin