A elisão da cópia foi permitida em várias circunstâncias. No entanto, mesmo que fosse permitido, o código ainda precisava funcionar como se a cópia não tivesse sido omitida. Ou seja, deve haver um construtor de cópia e / ou movimentação acessível.
Garantido elisão cópia redefine uma série de conceitos C ++, de tal forma que determinadas circunstâncias em que cópias / movimentos poderiam ser elidido na verdade, não provocam uma cópia / movimento em tudo . O compilador não está omitindo uma cópia; o padrão diz que nenhuma cópia desse tipo poderia acontecer.
Considere esta função:
T Func() {return T();}
Sob regras de elisão de cópia não garantida, isso criará um temporário e, em seguida, moverá desse temporário para o valor de retorno da função. Essa operação de movimento pode ser omitida, mas T
ainda deve ter um construtor de movimento acessível, mesmo que nunca seja usado.
Similarmente:
T t = Func();
Esta é a inicialização de cópia de t
. Isso copiará a inicialização t
com o valor de retorno de Func
. No entanto, T
ainda precisa ter um construtor de movimento, embora ele não seja chamado.
A elisão de cópia garantida redefine o significado de uma expressão prvalue . Pré-C ++ 17, prvalues são objetos temporários. No C ++ 17, uma expressão prvalue é meramente algo que pode materializar um temporário, mas ainda não é temporário.
Se você usar um prvalue para inicializar um objeto do tipo do prvalue, nenhum temporário será materializado. Ao fazer return T();
isso, o valor de retorno da função será inicializado por meio de um prvalue. Uma vez que essa função retornaT
, nenhum temporário é criado; a inicialização do prvalue simplesmente inicializa diretamente o valor de retorno.
O que se deve entender é que, como o valor de retorno é um prvalue, ainda não é um objeto . É apenas um inicializador para um objeto, assim como T()
é.
Ao fazer isso T t = Func();
, o prvalue do valor de retorno inicializa diretamente o objeto t
; não há um estágio "criar um temporário e copiar / mover". Visto que Func()
o valor de retorno de é um prvalue equivalente a T()
, t
é inicializado diretamente por T()
, exatamente como se você tivesse feito T t = T()
.
Se um prvalue for usado de qualquer outra forma, o prvalue materializará um objeto temporário, que será usado naquela expressão (ou descartado se não houver expressão). Então, se você o fizesse const T &rt = Func();
, o prvalue materializaria um temporário (usando T()
como inicializador), cuja referência seria armazenada rt
, junto com o material de extensão de vida útil temporária usual.
Uma coisa que a elisão garantida permite que você faça é retornar objetos que estão imóveis. Por exemplo,lock_guard
não pode ser copiado ou movido, então você não poderia ter uma função que o retornasse por valor. Mas com a eliminação de cópia garantida, você pode.
A elisão garantida também funciona com inicialização direta:
new T(FactoryFunction());
Se FactoryFunction
retornar T
por valor, esta expressão não copiará o valor de retorno para a memória alocada. Em vez disso, ele alocará memória e usará a memória alocada como a memória de valor de retorno para a chamada de função diretamente.
Portanto, as funções de fábrica que retornam por valor podem inicializar diretamente a memória alocada no heap, mesmo sem saber sobre isso. Contanto que funcionem internamente, sigam as regras de eliminação de cópia garantida, é claro. Eles precisam retornar um prvalue do tipo T
.
Claro, isso também funciona:
new auto(FactoryFunction());
Caso você não goste de escrever nomes de tipo.
É importante reconhecer que as garantias acima só funcionam para prvalues. Ou seja, você não tem garantia ao retornar uma variável nomeada :
T Func()
{
T t = ...;
...
return t;
}
Neste caso, t
ainda deve haver um construtor de copiar / mover acessível. Sim, o compilador pode escolher otimizar a cópia / movimentação. Mas o compilador ainda deve verificar a existência de um construtor de cópia / movimentação acessível.
Portanto, nada muda para a otimização do valor de retorno nomeado (NRVO).