Como funciona a eliminação de cópia garantida?


90

Na reunião de Padrões Oulu ISO C ++ de 2016, uma proposta chamada Elisão de cópia garantida por meio de categorias de valor simplificadas foi votada em C ++ 17 pelo comitê de padrões.

Como funciona exatamente a eliminação de cópia garantida? Abrange alguns casos em que a eliminação da cópia já era permitida ou são necessárias alterações de código para garantir a eliminação da cópia?

Respostas:


130

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 Tainda 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 tcom o valor de retorno de Func. No entanto, Tainda 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 FactoryFunctionretornar Tpor 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).


1
@BenVoigt: Colocar tipos definidos pelo usuário não trivialmente copiáveis ​​em registradores não é uma coisa viável que uma ABI pode fazer, esteja elisão disponível ou não.
Nicol Bolas

1
Agora que as regras são públicas, pode valer a pena atualizar isso com o conceito de "prvalues ​​are initializations".
Johannes Schaub - litb

7
@ JohannesSchaub-litb: É apenas "ambíguo" se você souber muito sobre as minúcias do padrão C ++. Para 99% da comunidade C ++, sabemos a que se refere "eliminação de cópia garantida". O documento propondo o recurso é até intitulado "Eliminação de cópia garantida". Adicionar "por meio de categorias de valor simplificadas" apenas torna confuso e difícil para os usuários entenderem. Além disso, é um nome impróprio, uma vez que essas regras não "simplificam" realmente as regras em torno das categorias de valor. Quer você goste ou não, o termo "eliminação de cópia garantida" refere-se a esse recurso e nada mais.
Nicol Bolas

1
Eu quero tanto poder pegar um prvalue e carregá-lo comigo. Eu acho que isso é apenas um (um tiro), na std::function<T()>verdade.
Yakk - Adam Nevraumont

1
@LukasSalich: Essa é uma pergunta do C ++ 11. Esta resposta é sobre um recurso do C ++ 17.
Nicol Bolas
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.