C+++ tem sempre produzido programas rápidos. Infelizmente, até C++11, tem havido uma verruga obstinada que abranda muitos programas em C++: a criação de objetos temporários. Às vezes esses objetos temporários podem ser otimizados pelo compilador (a otimização do valor de retorno, por exemplo). Mas este não é sempre o caso, e pode resultar em cópias de objetos caros. O que eu quero dizer?

Vamos dizer que você tem o seguinte código:

#include <iostream>using namespace std;vector<int> doubleValues (const vector<int>& v){ vector<int> new_values; new_values.reserve(v.size()); for (auto itr = v.begin(), end_itr = v.end(); itr != end_itr; ++itr ) { new_values.push_back( 2 * *itr ); } return new_values;}int main(){ vector<int> v; for ( int i = 0; i < 100; i++ ) { v.push_back( i ); } v = doubleValues( v );}

Se você fez muito trabalho de alta performance em C++, desculpe a dor que isso causou. Se você não fez… Bem, vamos ver por que este código é um código C++03. (O resto deste tutorial será sobre o porquê do código C+++11.) O problema é com as cópias. Quando se chama doubleValues, ele constrói um vetor, new_values, e o preenche. Isto sozinho pode não ser a performance ideal, mas se quisermos manter o nosso vector original sem ser salpicado, precisamos de uma segunda cópia. Mas o que acontece quando carregamos na declaração de retorno?

O conteúdo inteiro de new_values deve ser copiado! Em princípio, pode haver até duas cópias aqui: uma em um objeto temporário a ser retornado, e asecond quando o operador de atribuição vetorial roda na linha v = doubleValues( v );. A primeira cópia pode ser otimizada automaticamente pelo compilador, mas não há como evitar que a atribuição para v tenha que copiar todos os valores novamente, o que requer uma nova alocação de memória e outra iteração sobre o vetor inteiro.

Este exemplo pode ser um pouco elaborado – e é claro que você pode encontrar maneiras de evitar este tipo de problema – por exemplo, armazenando e retornando o vetor pelo ponteiro, ou passando em um vetor a ser preenchido. A questão é que nenhum destes estilos de programação é particularmente natural. Além disso, uma abordagem que requer retornar um ponteiro introduziu pelo menos uma alocação de moremória, e um dos objetivos do design de C++ é evitar alocações de memória.

A pior parte de toda esta história é que o objeto retornado deValores Duplos é um valor temporário que não é mais necessário. Quando você tem a linha v = doubleValues( v ), o resultado de doubleValues( v ) vai ser jogado fora uma vez que ele é copiado! Em teoria, deve ser possível saltar a cópia completa e apenas roubar o ponteiro dentro do vetor temporário e mantê-lo no v. Na verdade, por que não podemos mover o objeto? Em C++03, a resposta é que não havia como dizer se um objeto era temporário ou não, você tinha torun o mesmo código no operador de atribuição ou construtor de cópias, não importando de onde o valor viesse, então nenhum roubo era possível. Em C++11, a resposta é… você pode!

É para isso que servem as referências de valor e a semântica de movimento! Move semantics permite que você evite cópias desnecessárias ao trabalhar com objetos temporários que estão prestes a evaporar, e cujos recursos podem ser retirados com segurança daquele objeto temporário e usados por outro.

Move semantics conta com uma nova funcionalidade de C++11, chamada rvalue references,que você vai querer entender para realmente apreciar o que está acontecendo. Então primeiro vamos falar sobre o que é um valor, e depois o que é uma referência de valor. Finalmente, vamos voltar para mover a semântica e como ela pode ser implementada com referências de valor.

Rvalues and lvalues – bitter rivals, or best of friends?

Em C+++, há valores e lvalues. Um lvalue é uma expressão cujo endereço pode ser tomado, um valor localizador -essencialmente, um lvalue fornece um pedaço (semi)permanente de memória. Você pode fazer atribuições a lvalues. Forexample:

int a;a = 1; // here, a is an lvalue

Você também pode ter lvalues que não são variáveis:

int x;int& getRef () { return x;}getRef() = 4;

Aqui, getRef retorna uma referência a uma variável global, então ele está retornando um valor que é armazenado em um local permanente. (Você poderia literalmente escrever & getRef() se você quisesse, e isso lhe daria o endereço de x.)

Rvalues are–well, rvalues não são lvalues. Uma expressão é um valor se resultar em um objeto temporário. Por exemplo:

int x;int getVal (){ return x;}getVal();

Aqui, getVal() é um valor – o valor sendo retornado não é uma referência a x, é apenas um valor temporário. Isto fica um pouco mais interessante se usarmos objetos reais ao invés de números:

string getName (){ return "Alex";}getName();

Aqui, getName retorna uma string que é construída dentro da função. Você pode atribuir o resultado de getName a uma variável:

string name = getName();

Mas você está atribuindo de um objeto temporário, não de algum valor que tem uma localização fixa. getName() é um valor rvalue.

Detectar objetos temporários com referências rvalue

O importante é que os valores se referem a objetos temporários – assim como o valor retornado a partir de valores duplos. Não seria ótimo se pudéssemos saber, sem sombra de dúvida, que um valor retornado de uma expressão era temporário, e de alguma forma escrever código que é sobrecarregado para se comportar de forma diferente para objetos temporários? Porque, sim, sim, de facto seria. E é para isto que servem as referências de valor. Uma referência de valor é uma referência que se ligará apenas a um objeto temporário. O que eu quero dizer?

Prior to C++11, se você tivesse um objeto temporário, você poderia usar uma referência “regular” ou “lvalue” para ligá-lo, mas apenas se fosse const:

const string& name = getName(); // okstring& name = getName(); // NOT ok

A intuição aqui é que você não pode usar uma referência “mutável” porque, se o fizesse, você seria capaz de modificar algum objeto que está prestes a desaparecer, e isso seria perigoso. Note, a propósito, que segurar uma referência constante a um objeto temporário garante que o objeto temporário seja destruído no tempo imediatamente. Esta é uma boa garantia de C++, mas ainda é um objeto temporário, então você não quer modificá-lo.

Em C+++11, no entanto, há um novo tipo de referência, uma “referência de valor”, que lhe permitirá ligar uma referência mutável a um valor, mas não a um lvalue. Em outras palavras, as referências de rvalue são perfeitas para detectar se um valor é ou não um objeto temporário. As referências de valor usam a sintaxe && em vez de apenas &, e podem ser constantes e não constantes, tal como as referências lvalue, embora raramente se veja uma referência de valor constante (como veremos, as referências mutantes são mais ou menos o ponto):

const string&& name = getName(); // okstring&& name = getName(); // also ok - praise be!

Até agora tudo isto está bem e bom, mas como é que isso ajuda? A coisa mais importante sobre referências lvalue vs referências rvalue é o que acontece quando você escreve funções que tomam referências lvalue ou rvalue como argumentos. Digamos que temos duas funções:

printReference (const String& str){ cout << str;}printReference (String&& str){ cout << str;}

Agora o comportamento fica interessante – a função printReference tomando aconst lvalue reference aceitará qualquer argumento que seja dado, seja anlvalue ou rvalue, e independentemente de o lvalue ou rvalue ser mutável ou não. Entretanto, na presença da segunda sobrecarga, printReference takingan rvalue reference, ela receberá todos os valores, exceto as referências mutablervalue-references. Em outras palavras, se você escrever:

string me( "alex" );printReference( me ); // calls the first printReference function, taking an lvalue referenceprintReference( getName() ); // calls the second printReference function, taking a mutable rvalue reference

Agora temos uma maneira de determinar se uma variável de referência se refere a um objeto temporário ou a um objeto permanente. A versão de referência de valor do método é como a entrada secreta da porta traseira do clube em que você só pode entrar se você for um objeto temporário (clube chato, eu acho). Agora que temos o nosso método de determinar se um objecto era temporário ou permanente, como podemos usá-lo?

Move constructor and move assignment operator

O padrão mais comum que verá ao trabalhar com referências de rvalue é tocar um constructor de movimento e move assignment operator (que segue os mesmos princípios). Um construtor de movimento, como um construtor de cópia, toma como argumento uma instância de um objeto e cria uma nova instância baseada no objeto original. No entanto, o construtor de movimentos pode evitar a realocação da memória porque, sabendo que foi fornecido um objeto temporário, então ao invés de copiar os campos do objeto, vamos movê-los.

O que significa mover um campo do objeto? Se o campo é um tipo primitivo, como o int, nós apenas o copiamos. Fica mais interessante se o campo for um apointer: aqui, em vez de alocar e inicializar nova memória, podemos simplesmente roubar o ponteiro e anular o ponteiro no objeto temporário! Sabemos que o objeto temporário não será mais necessário, então podemos tirar seu ponteiro de baixo dele.

Imagine que temos uma classe ArrayWrapper simples, como esta:

class ArrayWrapper{ public: ArrayWrapper (int n) : _p_vals( new int ) , _size( n ) {} // copy constructor ArrayWrapper (const ArrayWrapper& other) : _p_vals( new int ) , _size( other._size ) { for ( int i = 0; i < _size; ++i ) { _p_vals = other._p_vals; } } ~ArrayWrapper () { delete _p_vals; } private: int *_p_vals; int _size;};

Note que o construtor de cópias tem que alocar memória e copiar todos os valores do array, um de cada vez! Isso é muito trabalho para uma cópia. Vamos adda mover o construtor e ganhar alguma eficiência massiva.

class ArrayWrapper{public: // default constructor produces a moderately sized array ArrayWrapper () : _p_vals( new int ) , _size( 64 ) {} ArrayWrapper (int n) : _p_vals( new int ) , _size( n ) {} // move constructor ArrayWrapper (ArrayWrapper&& other) : _p_vals( other._p_vals ) , _size( other._size ) { other._p_vals = NULL; other._size = 0; } // copy constructor ArrayWrapper (const ArrayWrapper& other) : _p_vals( new int ) , _size( other._size ) { for ( int i = 0; i < _size; ++i ) { _p_vals = other._p_vals; } } ~ArrayWrapper () { delete _p_vals; }private: int *_p_vals; int _size;};

Wow, o construtor de mover é na verdade mais simples que o construtor de cópias! Isso é um feito e tanto. As principais coisas a notar são:

  1. O parâmetro é uma referência de valor não constante
  2. outros._p_vals está definido para NULL

A segunda observação explica a primeira – não poderíamos definir outros._p_vals paraNULL se tivéssemos tomado uma referência de valor constante. Mas porque é que precisamos de setother._p_vals = NULL? A razão é o destruidor… quando o objeto temporário sair do escopo, assim como todos os outros objetos C++, seu destruidor irá rodar. Quando seu destruidor rodar, ele irá liberar _p_vals. Os mesmos _p_vals que nós acabamos de copiar! Se não definirmos outros _p_vals para NULL, o movimento não seria realmente um movimento de mudança – seria apenas uma cópia que introduz um travamento mais tarde, uma vez que começamos a usar a memória livre. Este é o objetivo de um construtor de movimento: para evitar o acopy mudando o objeto temporário original!

Again, as regras de sobrecarga funcionam de tal forma que o construtor de movimento é chamado apenas para um objeto temporário – e apenas um objeto temporário que pode ser modificado. Em algo isto significa que se você tem uma função que retorna um objeto const, isso fará com que o construtor de cópia seja executado em vez do construtor de movimento – não escreva código como este:

const ArrayWrapper getArrayWrapper (); // makes the move constructor useless, the temporary is const!

Há ainda mais uma situação que ainda não discutimos como lidar no construtor de movimento – quando temos um campo que é um objeto. Por exemplo, imaginemos que em vez de termos um campo de tamanho, tínhamos um campo de metadados parecido com este:

class MetaData{public: MetaData (int size, const std::string& name) : _name( name ) , _size( size ) {} // copy constructor MetaData (const MetaData& other) : _name( other._name ) , _size( other._size ) {} // move constructor MetaData (MetaData&& other) : _name( other._name ) , _size( other._size ) {} std::string getName () const { return _name; } int getSize () const { return _size; } private: std::string _name; int _size;};

Agora o nosso array pode ter um nome e um tamanho, então talvez tenhamos que mudar a definição de ArrayWrapper assim:

class ArrayWrapper{public: // default constructor produces a moderately sized array ArrayWrapper () : _p_vals( new int ) , _metadata( 64, "ArrayWrapper" ) {} ArrayWrapper (int n) : _p_vals( new int ) , _metadata( n, "ArrayWrapper" ) {} // move constructor ArrayWrapper (ArrayWrapper&& other) : _p_vals( other._p_vals ) , _metadata( other._metadata ) { other._p_vals = NULL; } // copy constructor ArrayWrapper (const ArrayWrapper& other) : _p_vals( new int ) , _metadata( other._metadata ) { for ( int i = 0; i < _metadata.getSize(); ++i ) { _p_vals = other._p_vals; } } ~ArrayWrapper () { delete _p_vals; }private: int *_p_vals; MetaData _metadata;};

O trabalho funciona? Parece muito natural, não parece, simplesmente chamar o construtor do MetaDatamove de dentro do construtor de movimento para o ArrayWrapper? O problema é que isto simplesmente não funciona. A razão é simples: o valor de outro construtor em movimento… é uma referência de valor. Mas uma referência de valor não é, de facto, uma referência de valor. É um valor, e por isso o construtor de cópias é chamado, não o construtor de móveis. Isto é esquisito. Eu sei… é confuso. Aqui está a maneira de pensar sobre isso. Um valor é uma expressão que cria um objecto que está prestes a evaporar para o ar. Está nas suas últimas pernas em vida, ou prestes a cumprir o seu propósito de vida. De repente, passamos o temporário para um construtor em movimento, e ele ganha nova vida no novo escopo. No contexto em que a expressão do valor foi avaliada, o objeto temporário está realmente acabado e pronto. Mas em nosso construtor, o objeto temporário tem um nome; ele estará vivo por toda a duração de nossa função. Em outras palavras, podemos usar a variável mais de uma vez na função, e o objeto temporário tem uma localização definida que realmente persiste para a função theentire. É um valor no verdadeiro sentido do termo valor localizador, podemos localizar o objeto em um endereço específico que é estável para toda a duração da chamada da função. Podemos, na verdade, querer usá-lo mais tarde na função. Se um construtor de movimento fosse chamado sempre que tivéssemos um objeto em um rvaluereference, poderíamos usar um objeto movido, por acidente!

 // move constructor ArrayWrapper (ArrayWrapper&& other) : _p_vals( other._p_vals ) , _metadata( other._metadata ) { // if _metadata( other._metadata ) calls the move constructor, using // other._metadata here would be extremely dangerous! other._p_vals = NULL; }

Put a final way: ambas referências lvalue e rvalue são expressões lvalue. A diferença é que uma referência lvalue deve ser constante para manter uma referência a anrvalue, enquanto uma referência rvalue pode sempre manter uma referência a um rvalue. A coisa apontada veio de um valor, mas quando usamos uma referência de valor em si, ela resulta em um lvalue.

std::move

Então qual é o truque para lidar com este caso? Precisamos usar std::move, from<utility>–std::move é uma forma de dizer, “ok, honest to God I know I havean lvalue, but I want it to be an rvalue” std::move não move, por si só, nada; ele apenas transforma um lvalue em um rvalue, para que você possa invocar o construtor do move. Nosso código deve ser parecido com isto:

#include <utility> // for std::move // move constructor ArrayWrapper (ArrayWrapper&& other) : _p_vals( other._p_vals ) , _metadata( std::move( other._metadata ) ) { other._p_vals = NULL; }

E é claro que devemos realmente voltar ao MetaData e corrigir o seu próprio construtor de move para que ele use std::move na string que ele contém:

 MetaData (MetaData&& other) : _name( std::move( other._name ) ) // oh, blissful efficiency : _size( other._size ) {}

Move assignment operator

Apenas como temos um construtor de move, devemos também ter um operador de move assignment. Você pode facilmente escrever uma usando as mesmas técnicas que para criar um construtor de mudanças.

Move constructors and implicitly generated constructors

Como você sabe, em C++ quando você declara qualquer construtor, o compilador não irá mais gerar o construtor padrão para você. O mesmo é verdade aqui: adicionar um construtor move a uma classe irá requerer que você declare e defina o seu construtor padrão. Por outro lado, declarar um construtor move não impede o compilador de fornecer um co-construtor gerado implicitamente, e declarar um operador move não inibe a criação de um operador de atribuição padrão.

Como funciona std::move

Você pode estar se perguntando, como se escreve uma função como std::move? Como você consegue essa propriedade mágica de transformar um valor em uma referência de valor? A resposta, como você pode adivinhar, é digitando. A declaração real para std::move é um pouco mais envolvida, mas no seu cerne, é apenas um estático_cast para uma referência de valor. Isso significa, na verdade, que você não precisa realmente usar move–mas você deveria, já que é muito mais claro o que você quer dizer. O facto de ser necessário um elenco é, a propósito, uma coisa muito boa! Significa que você não pode acidentalmente converter um lvalue em um rvalue, o que seria perigoso, já que poderia permitir que um movimento acidental ocorresse. Você deve usar explicitamente std::move (ou um elenco) para converter um lvalue em uma referência de rvalue, e uma referência de rvalue nunca se ligará a um lvalue em itsown.

Retornar uma referência explícita de rvalue de uma função

Existe alguma vez em que você deve escrever uma função que retorne uma referência de rvalue? O que significa retornar uma referência de valor de qualquer maneira? As funções que retornam objetos por valor não são já valores?

Primeira pergunta: retornar uma referência explícita de valor é diferente de retornar um objeto por valor. Pegue o seguinte exemplo simples:

int x;int getInt (){ return x;}int && getRvalueInt (){ // notice that it's fine to move a primitive type--remember, std::move is just a cast return std::move( x );}

Claramente no primeiro caso, apesar de getInt() ser um valor, há uma cópia da variável x sendo feita. Podemos até ver isso escrevendo uma pequena função de ajuda:

void printAddress (const int& v) // const ref to allow binding to rvalues{ cout << reinterpret_cast<const void*>( & v ) << endl;}printAddress( getInt() ); printAddress( x );

Quando você executar este programa, você verá que há dois valores separados impressos.

Por outro lado,

printAddress( getRvalueInt() ); printAddress( x );

imprime o mesmo valor porque estamos retornando explicitamente um valor aqui.

Então retornar uma referência de valor é uma coisa diferente de não retornar uma referência de valor, mas esta diferença se manifesta mais notavelmente se você tiver um objeto pré-existente que você está retornando ao invés de um objeto temporário criado na função (onde o compilador provavelmente eliminará a cópia para você).

Agora a questão de se você quer fazer isto. A resposta é:provavelmente não. Na maioria dos casos, isso apenas torna mais provável que você acabe com uma referência pendente (um caso onde a referência existe, mas o objeto temporário a que ela se refere foi destruído). A questão é bastante semelhante ao perigo de devolver uma referência de valor – a referência – a um objeto pode não mais existir. Retornar uma referência de valor faria sentido principalmente em casos muito raros onde você tem uma função de membro e precisa retornar o resultado de callstd::mover em um campo da classe dessa função – e com que freqüência você vai fazer isso?

Move semantics and the standard library

Voltando ao nosso exemplo original — estávamos usando um vetor, e não temos controle sobre a classe vetorial e se ela tem ou não um operador de atribuição de move construtor ou move construtor. Felizmente, o comitê de padrões é sábio, e a semântica move foi adicionada à biblioteca padrão. Isto significa que você pode devolver eficientemente vetores, mapas, strings e quaisquer outros objetos de biblioteca padrão que você queira, tirando total proveito da semântica móvel.

Objetos móveis em containers STL

Na verdade, a biblioteca padrão vai um passo além. Se você habilitar moveemantics em seus próprios objetos criando operadores de atribuição de movimento e construtores de movimento, quando você armazena esses objetos em um container, o STL willautomaticamente usa std::move, aproveitando automaticamente as classes habilitadas para mover para eliminar cópias ineficientes.

Articles

Deixe uma resposta

O seu endereço de email não será publicado.