C++ Semântica de Movimento

Na aula Cópia e Movimento, você viu que copiar um objeto pode dar muito trabalho: um std::vector com um milhão de textos precisa reservar memória nova e copiar cada texto. Mas muitas vezes o original nem vai ser usado de novo: ele está prestes a morrer, ou você só quer entregá-lo para outro lugar. Para que copiar, então? O C++11 trouxe a resposta: mover. Em vez de duplicar, o objeto novo simplesmente pega para si o que era do antigo. Nesta aula você vai ver a diferença em números reais, aprender a usar std::move e descobrir que, na maior parte do tempo, o compilador já move por você.

Você vai aprender
  • A diferença entre copiar e mover, com uma comparação do dia a dia.
  • O que são lvalue e rvalue (o suficiente para usar), e o &&.
  • O que std::move faz (e o que ele não faz).
  • Como escrever um construtor de movimento e ver, com prints, quando ele roda.
  • Que o objeto "movido" fica vazio, mas válido.
  • Como mover objetos para dentro de um std::vector, e por que o noexcept importa.
  • Quanto tempo o movimento economiza, e quando o compilador já move sozinho.

Esta aula usa Cópia e Movimento, Referências e std::vector. Os programas foram compilados com o g++ 15 na máquina da Ana, na pasta ~/cpp, com g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa.


A ideia: mudança de casa

A Ana vai se mudar. Ela tem duas opções:

  • Copiar: comprar um sofá igual, uma geladeira igual, uma cama igual, para a casa nova. A casa velha continua mobiliada. Caro e demorado.
  • Mover: pôr os móveis no caminhão e levar para a casa nova. A casa velha fica vazia, mas continua sendo uma casa: dá para mobiliá-la de novo ou vendê-la. Rápido e barato.

Se a Ana não vai mais morar na casa velha, mover é óbvio. Em C++, é igual: quando o objeto original não vai mais ser usado, mover os recursos dele (a memória do texto, os itens do vetor) é muito mais barato que copiá-los.


1. Cópia × movimento, com prints

A classe Mochila guarda o nome do dono e uma lista de itens. Vamos escrever o construtor de cópia e o de movimento com um std::cout em cada um:

#include <iostream>
#include <string>
#include <utility>
#include <vector>

class Mochila {
public:
    Mochila(std::string dono) : dono_{dono} {}

    void guardar(std::string item) { itens_.push_back(item); }
    std::size_t quantidade() const { return itens_.size(); }

    // construtor de cópia: duplica tudo
    Mochila(const Mochila& outra) : dono_{outra.dono_}, itens_{outra.itens_} {
        std::cout << "  [cópia] duplicou " << itens_.size() << " itens\n";
    }
    // construtor de movimento: pega tudo da outra
    Mochila(Mochila&& outra) noexcept
        : dono_{std::move(outra.dono_)}, itens_{std::move(outra.itens_)} {
        std::cout << "  [movimento] levou " << itens_.size() << " itens\n";
    }
private:
    std::string dono_;
    std::vector<std::string> itens_;
};

int main() {
    Mochila a{"Ana"};
    a.guardar("caderno");
    a.guardar("estojo");
    a.guardar("garrafa");

    std::cout << "Mochila b = a;\n";
    Mochila b = a;
    std::cout << "Mochila c = std::move(a);\n";
    Mochila c = std::move(a);

    std::cout << "a: " << a.quantidade() << " itens\n";
    std::cout << "b: " << b.quantidade() << " itens\n";
    std::cout << "c: " << c.quantidade() << " itens\n";
}
Mochila(Mochila&& outra)
O construtor de movimento. O && (dois "e comercial") é uma referência a rvalue: ela só aceita objetos que podem ser "esvaziados" (seção 2). Repare que não tem const: o construtor vai mexer na outra, tirando as coisas dela.
noexcept
Uma promessa: "este construtor não lança exceções". Mover só troca ponteiros por dentro, então não tem como falhar. A seção 5 mostra por que essa palavra faz diferença.
dono_{std::move(outra.dono_)}
Move cada atributo: a string e o vetor também têm construtores de movimento, que fazem o trabalho de verdade.
std::move(a)
No main: "pode mover de a, eu não preciso mais dele". Precisa de #include <utility>.
$ g++ -std=c++23 -Wall -Wextra mochila.cpp -o mochila
$ ./mochila
Mochila b = a;
  [cópia] duplicou 3 itens
Mochila c = std::move(a);
  [movimento] levou 3 itens
a: 0 itens               « a mochila da Ana ficou vazia
b: 3 itens               « b é uma cópia
c: 3 itens               « c levou os itens originais

Sem std::move, o C++ copiou; com std::move, moveu. E a mochila a ficou com 0 itens: é a "casa vazia" depois da mudança.

E a regra dos cinco?

Esta Mochila só tem os construtores de cópia e de movimento, porque o objetivo é ver quando cada um roda. Uma classe completa teria também as duas atribuições (operator= de cópia e de movimento), como manda a regra dos cinco. Na prática, pela regra do zero, você não escreveria nenhum: com atributos std::string e std::vector, o compilador gera os cinco, e o movimento automático faz exatamente o que o nosso faz (sem os prints).

Experimente: rode e confira a cópia e o movimento. Depois acrescente a.guardar("lanche"); depois do std::move(a) e mostre a quantidade de a de novo: a mochila vazia pode ser usada outra vez? Tente também Mochila d = std::move(c);.

#include <iostream>
#include <string>
#include <utility>
#include <vector>

class Mochila {
public:
    Mochila(std::string dono) : dono_{dono} {}

    void guardar(std::string item) { itens_.push_back(item); }
    std::size_t quantidade() const { return itens_.size(); }

    Mochila(const Mochila& outra) : dono_{outra.dono_}, itens_{outra.itens_} {
        std::cout << "  [cópia] duplicou " << itens_.size() << " itens\n";
    }
    Mochila(Mochila&& outra) noexcept
        : dono_{std::move(outra.dono_)}, itens_{std::move(outra.itens_)} {
        std::cout << "  [movimento] levou " << itens_.size() << " itens\n";
    }
private:
    std::string dono_;
    std::vector<std::string> itens_;
};

int main() {
    Mochila a{"Ana"};
    a.guardar("caderno");
    a.guardar("estojo");

    Mochila b = a;
    Mochila c = std::move(a);
    std::cout << "a: " << a.quantidade() << ", b: " << b.quantidade()
              << ", c: " << c.quantidade() << '\n';
}

2. lvalue e rvalue (o suficiente para usar)

Como o C++ decide entre copiar e mover? Olhando para o tipo de valor que está do lado direito. São dois, com nomes que vêm de "lado esquerdo" (left) e "lado direito" (right) de uma atribuição:

lvaluervalue
O que éAlgo com nome e endereço, que continua existindo depois da linhaUm valor temporário, sem nome, que morre no fim da linha
Exemplosnome, a, lista[0]nome + " Souza", criar(), "Bia" virando std::string, 42
Pode mover dele?Não, a não ser que você peça (std::move)Sim: ninguém mais vai usá-lo
Referência que aceitaT& e const T&T&& e const T&

Duas funções com o mesmo nome, uma recebendo const std::string& e outra std::string&&, mostram qual é qual:

#include <iostream>
#include <string>
#include <utility>

void receber(const std::string& s) {
    std::cout << "lvalue (tem nome, vou copiar): " << s << '\n';
}

void receber(std::string&& s) {
    std::cout << "rvalue (temporário, posso pegar): " << s << '\n';
}

std::string nome_completo() {
    return "Ana Souza";
}

int main() {
    std::string nome = "Ana";
    receber(nome);                  // variável com nome
    receber(nome + " Souza");       // resultado de uma conta
    receber(nome_completo());       // retorno de função
    receber("Bia");                 // texto literal vira string temporária
    receber(std::move(nome));       // "pode tratar como temporário"
}
$ g++ -std=c++23 -Wall -Wextra valores.cpp -o valores
$ ./valores
lvalue (tem nome, vou copiar): Ana
rvalue (temporário, posso pegar): Ana Souza
rvalue (temporário, posso pegar): Ana Souza
rvalue (temporário, posso pegar): Bia
rvalue (temporário, posso pegar): Ana       « o std::move transformou nome em rvalue

O compilador escolheu a versão && para todos os temporários, sozinho. Só a variável nome foi tratada como lvalue, até o std::move(nome) da última linha.

std::move não move nada

O nome engana. std::move(x) não copia, não move e não apaga: ele só muda o rótulo de x de lvalue para rvalue, como quem cola um aviso "pode levar". Quem move de verdade é o construtor (ou a atribuição) de movimento que roda depois, por causa do rótulo. Se ninguém aproveitar o aviso (como na função receber, que só imprime), nome continua intacto.


3. O objeto movido: vazio, mas válido

O que sobra de um objeto depois que alguém se mudou dele? Veja com uma std::string:

#include <iostream>
#include <string>
#include <utility>

int main() {
    std::string texto = "Lista de compras: arroz, feijão, café";
    std::string outro = std::move(texto);

    std::cout << "outro: [" << outro << "]\n";
    std::cout << "texto: [" << texto << "] tamanho " << texto.size() << '\n';

    texto = "dá para usar de novo";
    std::cout << "texto: [" << texto << "]\n";
}
$ g++ -std=c++23 -Wall -Wextra movida.cpp -o movida
$ ./movida
outro: [Lista de compras: arroz, feijão, café]
texto: [] tamanho 0      « vazia, mas ainda é uma string que funciona
texto: [dá para usar de novo]

A regra oficial do C++ para os tipos da biblioteca padrão é: o objeto movido fica num estado "válido, mas não especificado". Válido: você pode destruí-lo, atribuir um valor novo, perguntar o tamanho. Não especificado: o padrão não promete o que tem dentro. Na prática, std::string, std::vector e std::unique_ptr ficam vazios (o unique_ptr vira nullptr). A regra segura:

Depois de std::move(x), só faça duas coisas com x
  • dar a ele um valor novo (x = …;); ou
  • deixá-lo morrer no fim do escopo.

Ler o conteúdo de um objeto movido é um erro de lógica, mesmo quando "funciona".

Como "pegar para si" funciona por dentro

Uma std::string longa guarda o texto num pedaço de memória separado e tem só um ponteiro para ele. Mover é copiar o ponteiro e zerar o da origem, sem tocar no texto. Veja o mesmo truque escrito à mão, na classe Notas da aula anterior (com ponteiro cru, só para entender; no dia a dia, regra do zero):

// construtor de movimento: rouba o array e deixa a outra vazia
Notas(Notas&& outra) noexcept
    : tamanho_{outra.tamanho_}, dados_{outra.dados_} {
    outra.tamanho_ = 0;
    outra.dados_ = nullptr;     // a outra não vai apagar mais nada
}
// atribuição de movimento: devolve o antigo e rouba o novo
Notas& operator=(Notas&& outra) noexcept {
    if (this != &outra) {
        delete[] dados_;
        tamanho_ = outra.tamanho_;
        dados_ = outra.dados_;
        outra.tamanho_ = 0;
        outra.dados_ = nullptr;
    }
    return *this;
}
$ g++ -std=c++23 -Wall -Wextra -g -fsanitize=address roubar.cpp -o roubar
$ ./roubar
ana: 0, bia: 3           « Notas bia = std::move(ana);
bia: 0, caio: 3          « caio = std::move(bia);

O outra.dados_ = nullptr; é o passo essencial: sem ele, as duas teriam o mesmo array, e o double free da aula anterior voltaria. Com ele, o destrutor da origem faz delete[] nullptr, que não faz nada. O AddressSanitizer não encontrou nenhum problema.


4. Por que mover deixa o programa mais rápido

Quanto custa copiar um vetor com um milhão de textos, comparado a movê-lo? O <chrono> (aula Tempo e Datas) mede:

#include <chrono>
#include <iostream>
#include <string>
#include <utility>
#include <vector>

int main() {
    using relogio = std::chrono::steady_clock;
    std::vector<std::string> estoque(1'000'000, "produto da lojinha da Ana");

    auto t0 = relogio::now();
    std::vector<std::string> copia = estoque;              // cópia
    auto t1 = relogio::now();
    std::vector<std::string> movido = std::move(estoque);  // movimento
    auto t2 = relogio::now();

    auto us = [](auto d) {
        return std::chrono::duration_cast<std::chrono::microseconds>(d).count();
    };
    std::cout << "cópia:     " << us(t1 - t0) << " µs (" << copia.size() << " itens)\n";
    std::cout << "movimento: " << us(t2 - t1) << " µs (" << movido.size() << " itens)\n";
}
$ g++ -std=c++23 -Wall -Wextra tempo.cpp -o tempo
$ ./tempo
cópia:     180694 µs (1000000 itens)     « cerca de 0,18 segundo
movimento: 0 µs (1000000 itens)          « menos de um microssegundo

A cópia reservou memória para um milhão de textos e copiou cada um (cada texto tem 25 letras, guardadas num pedaço de memória próprio). O movimento trocou três ponteiros por dentro do vetor: o tempo nem chegou a 1 µs. Os números exatos mudam de computador para computador e de uma execução para outra, mas a diferença é sempre desse tamanho: a cópia cresce com o tamanho do vetor; o movimento custa sempre o mesmo, quase nada.


5. Mover para dentro de um vector

Guardar objetos num std::vector é onde o movimento mais aparece no dia a dia. A Mochila agora só tem o dono, para a saída ficar curta:

Mochila preparar(std::string dono) {
    Mochila m{dono};
    return m;
}

int main() {
    std::vector<Mochila> armario;
    armario.reserve(10);

    Mochila ana{"Ana"};
    std::cout << "push_back(ana):\n";
    armario.push_back(ana);
    std::cout << "push_back(std::move(ana)):\n";
    armario.push_back(std::move(ana));
    std::cout << "push_back(Mochila{\"Bia\"}):\n";
    armario.push_back(Mochila{"Bia"});
    std::cout << "emplace_back(\"Caio\"):\n";
    armario.emplace_back("Caio");
    std::cout << "Mochila d = preparar(\"Davi\"):\n";
    Mochila d = preparar("Davi");
    std::cout << "fim\n";
}
$ g++ -std=c++23 -Wall -Wextra vetor.cpp -o vetor
$ ./vetor
push_back(ana):
  [cópia] Ana            « ana tem nome: copiou
push_back(std::move(ana)):
  [movimento] Ana        « pedimos para mover
push_back(Mochila{"Bia"}):
  [movimento] Bia        « temporário: o compilador moveu sozinho
emplace_back("Caio"):            « nada: construiu direto dentro do vetor
Mochila d = preparar("Davi"):    « nada: elisão de cópia
fim
FormaO que aconteceQuando usar
v.push_back(x)Copia xVocê ainda vai usar x depois
v.push_back(std::move(x))Move x (que fica vazio)x não será mais usado
v.push_back(Classe{…})Move o temporárioRaramente: prefira o emplace_back
v.emplace_back(…)Constrói direto no vetor, com os argumentos do construtorCriar um objeto novo já dentro do vetor

O noexcept faz diferença

Quando um vetor fica cheio, ele reserva um espaço maior e passa os itens antigos para lá (aula std::vector). Se algo der errado no meio dessa mudança, o vetor promete voltar ao estado anterior. Com cópias, ele consegue (os originais estão intactos); com movimentos que podem falhar, não. Por isso o vetor só move os itens antigos se o construtor de movimento prometer noexcept. Veja o mesmo programa com e sem a palavra:

int main() {
    std::vector<Mochila> armario;
    armario.emplace_back("Ana");
    std::cout << "cabia " << armario.capacity() << "; colocando a Bia:\n";
    armario.emplace_back("Bia");
}
$ g++ -std=c++23 -Wall -Wextra cresce.cpp -o cresce
$ ./cresce                       « Mochila(Mochila&& outra) noexcept
cabia 1; colocando a Bia:
  [movimento] Ana                « a Ana foi movida para o espaço novo
$ g++ -std=c++23 -Wall -Wextra semnoexcept.cpp -o semnoexcept
$ ./semnoexcept                  « Mochila(Mochila&& outra), sem noexcept
cabia 1; colocando a Bia:
  [cópia] Ana                    « sem a promessa, o vetor preferiu copiar

Nenhum aviso, nenhum erro: só um programa mais lento. Por isso, todo construtor e atribuição de movimento que você escrever deve ser noexcept. (Os que o compilador gera sozinho já são, quando os atributos permitem.)


6. Quando o compilador já move sozinho

Você quase nunca precisa escrever std::move. O compilador move (ou faz melhor ainda, nem copia nem move) nos casos mais comuns:

SituaçãoO que o compilador faz
return obj; de uma variável localConstrói direto no destino (elisão de cópia). Se não puder, move.
Um temporário: Classe x = criar();, v.push_back(Classe{…})Elisão ou movimento: temporários são rvalues.
a = b + c; com stringsO resultado de b + c é temporário: é movido para a.
Variável com nome que você não vai mais usarNada: aqui é a sua vez de escrever std::move.

Por isso, devolver objetos grandes por valor é a forma certa e barata em C++ moderno: std::vector<std::string> ler_arquivo();. E escrever return std::move(local); é pior do que não escrever, porque atrapalha a elisão. O g++ avisa:

std::string montar_lista() {
    std::string lista = "arroz, feijão, café";
    return std::move(lista);
}
$ g++ -std=c++23 -Wall -Wextra pessimo.cpp -o pessimo
pessimo.cpp: In function ‘std::string montar_lista()’:
pessimo.cpp:7:21: warning: moving a local object in a return statement prevents copy elision [-Wpessimizing-move]
    7 |     return std::move(lista);
      |            ~~~~~~~~~^~~~~~~
pessimo.cpp:7:21: note: remove ‘std::move’ call

"Mover um objeto local num return impede a elisão de cópia". O nome do aviso diz tudo: pessimizing move, o movimento que "pessimiza" (o contrário de otimizar). A nota já dá o conserto: return lista;.


Quando dá errado

"use of deleted function" ao copiar um unique_ptr
auto caderno = std::make_unique<std::string>("caderno de receitas");
std::unique_ptr<std::string> emprestado = caderno;
$ g++ -std=c++23 -Wall -Wextra unico.cpp -o unico
unico.cpp: In function ‘int main()’:
unico.cpp:7:47: error: use of deleted function ‘std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = std::__cxx11::basic_string<char>; _Dp = std::default_delete<std::__cxx11::basic_string<char> >]’
    7 |     std::unique_ptr<std::string> emprestado = caderno;
      |                                               ^~~~~~~
In file included from /usr/include/c++/15/memory:80,
                 from unico.cpp:2:
/usr/include/c++/15/bits/unique_ptr.h:524:7: note: declared here
  524 |       unique_ptr(const unique_ptr&) = delete;
      |       ^~~~~~~~~~
unico.cpp:7:47: note: use ‘-fdiagnostics-all-candidates’ to display considered candidates
    7 |     std::unique_ptr<std::string> emprestado = caderno;
      |                                               ^~~~~~~

O que significa: o construtor de cópia do unique_ptr é = delete (a nota mostra a linha da biblioteca). Um unique_ptr é o único dono do objeto: copiar criaria dois donos. O std::__cxx11::basic_string<char> é só o nome completo de std::string. (Clang: call to deleted constructor of 'std::unique_ptr<std::string>'.)

Como consertar: se a intenção é transferir a posse, mova: std::unique_ptr<std::string> emprestado = std::move(caderno);. Depois disso, caderno vale nullptr. Se a intenção é só usar o objeto em outro lugar, passe uma referência (const std::string&).

Usei a variável depois do std::move e ela estava vazia

O que significa: não há mensagem: o programa simplesmente mostra um texto vazio, um vetor com 0 itens ou quebra ao usar um unique_ptr que virou nullptr. O objeto foi movido (seção 3).

Como consertar: mova só no último uso. Se ainda precisar do valor, copie (sem std::move) ou guarde o que precisa antes.

Escrevi std::move, mas copiou mesmo assim
const Mochila ana{"Ana"};
Mochila outra = std::move(ana);
$ g++ -std=c++23 -Wall -Wextra constmove.cpp -o constmove
$ ./constmove
  [cópia] Ana

O que significa: ana é const, e mover significa mudar a origem (esvaziá-la). O construtor de movimento recebe Mochila&&, sem const, então não aceita; o compilador cai no de cópia, que aceita const Mochila&. Nenhum aviso. O mesmo acontece se a classe não tiver construtor de movimento (por exemplo, porque você escreveu só o de cópia).

Como consertar: não declare const o que você pretende mover; e confira se a classe tem movimento (regra do zero ou dos cinco).

"moving a local object in a return statement prevents copy elision"

O que significa: return std::move(variavel_local);, como na seção 6.

Como consertar: return variavel_local;. O compilador já move (ou faz melhor) sozinho.


Missão

Missão: a Mochila conta quantas cópias e quantos movimentos aconteceram. Do jeito que está, guardar a Ana e a Bia no armário faz 2 cópias. Como as duas variáveis não são usadas depois, mude o main para a saída ficar cópias: 0, movimentos: 2.

#include <iostream>
#include <string>
#include <utility>
#include <vector>

class Mochila {
public:
    inline static int copias = 0;
    inline static int movimentos = 0;

    Mochila(std::string dono) : dono_{dono} {}
    Mochila(const Mochila& outra) : dono_{outra.dono_} { ++copias; }
    Mochila(Mochila&& outra) noexcept : dono_{std::move(outra.dono_)} { ++movimentos; }
private:
    std::string dono_;
};

int main() {
    std::vector<Mochila> armario;
    armario.reserve(3);

    Mochila ana{"Ana"};
    Mochila bia{"Bia"};
    armario.push_back(ana);
    armario.push_back(bia);

    std::cout << "cópias: " << Mochila::copias << ", movimentos: " << Mochila::movimentos << '\n';
}
#include <iostream>
#include <string>
#include <utility>
#include <vector>

class Mochila {
public:
    inline static int copias = 0;
    inline static int movimentos = 0;

    Mochila(std::string dono) : dono_{dono} {}
    Mochila(const Mochila& outra) : dono_{outra.dono_} { ++copias; }
    Mochila(Mochila&& outra) noexcept : dono_{std::move(outra.dono_)} { ++movimentos; }
private:
    std::string dono_;
};

int main() {
    std::vector<Mochila> armario;
    armario.reserve(3);

    Mochila ana{"Ana"};
    Mochila bia{"Bia"};
    armario.push_back(std::move(ana));
    armario.push_back(std::move(bia));

    std::cout << "cópias: " << Mochila::copias << ", movimentos: " << Mochila::movimentos << '\n';
}

A saída da solução no laboratório:

$ ./contador
cópias: 0, movimentos: 2

Os contadores são membros static: pertencem à classe, não a cada mochila. Para ir além: e se, em vez de criar ana e bia, você usasse armario.emplace_back("Ana");? Quantos movimentos sobrariam?


Referência

CódigoO que faz
std::move(x)Marca x como "pode mover de mim" (rvalue). Não move nada sozinho. #include <utility>
Classe(Classe&& outra) noexceptConstrutor de movimento: pega os recursos de outra
Classe& operator=(Classe&& outra) noexceptAtribuição de movimento
T&&Referência a rvalue: só aceita temporários (ou o que passou por std::move)
v.push_back(std::move(x))Move x para dentro do vetor
v.emplace_back(args…)Constrói o objeto direto dentro do vetor
return local;Elisão de cópia ou movimento automático. Nunca return std::move(local);
Objeto movidoVálido, mas não especificado (na prática, vazio). Só atribua ou deixe morrer
const + std::moveVira cópia, sem aviso

Exercícios

1. Entregar sem copiar

Complete para colocar a string nome no vetor lista sem copiar (você não vai mais usar nome):

lista.push_back();

2. A assinatura

Complete o construtor de movimento da classe Cesta:

Cesta() noexcept

3. O que sobrou

Depois de std::string b = std::move(a);, em que a valia "feijão", quanto vale a.size() na prática (g++)?

4. O que std::move(x) faz sozinho?

5. Qual destes é um rvalue?

6. Por que o construtor de movimento deve ser noexcept?


Resumo

  • Mover = o objeto novo pega os recursos do antigo, que fica vazio. É muito mais barato que copiar (0 µs × 0,18 s no teste com um milhão de textos).
  • lvalue tem nome e continua existindo; rvalue é temporário. Temporários são movidos automaticamente.
  • std::move(x) só marca x como "pode mover"; o construtor de movimento Classe(Classe&&) noexcept faz o trabalho.
  • O objeto movido fica válido, mas sem conteúdo garantido: atribua um valor novo ou deixe-o morrer.
  • Em vetores: push_back(std::move(x)) ou emplace_back(…). Movimento sem noexcept faz o vetor copiar ao crescer.
  • O compilador já move (ou elimina a cópia) em return local; e com temporários. Não escreva return std::move(local);.
  • std::move num objeto const vira cópia. unique_ptr só pode ser movido, nunca copiado.