Eu acredito que você tem a observação correta, mas a interpretação errada!
A cópia não ocorrerá retornando o valor, porque todo compilador inteligente normal usará (N) RVO nesse caso. No C ++ 17, isso é obrigatório, portanto, você não pode ver nenhuma cópia retornando um vetor gerado local da função.
OK, vamos brincar um pouco std::vectore o que acontecerá durante a construção ou preenchendo passo a passo.
Primeiro de tudo, vamos gerar um tipo de dados que torna cada cópia ou movimento visível como este:
template <typename DATA >
struct VisibleCopy
{
private:
DATA data;
public:
VisibleCopy( const DATA& data_ ): data{ data_ }
{
std::cout << "Construct " << data << std::endl;
}
VisibleCopy( const VisibleCopy& other ): data{ other.data }
{
std::cout << "Copy " << data << std::endl;
}
VisibleCopy( VisibleCopy&& other ) noexcept : data{ std::move(other.data) }
{
std::cout << "Move " << data << std::endl;
}
VisibleCopy& operator=( const VisibleCopy& other )
{
data = other.data;
std::cout << "copy assign " << data << std::endl;
}
VisibleCopy& operator=( VisibleCopy&& other ) noexcept
{
data = std::move( other.data );
std::cout << "move assign " << data << std::endl;
}
DATA Get() const { return data; }
};
E agora vamos começar algumas experiências:
using T = std::vector< VisibleCopy<int> >;
T Get1()
{
std::cout << "Start init" << std::endl;
std::vector< VisibleCopy<int> > vec{ 1,2,3,4 };
std::cout << "End init" << std::endl;
return vec;
}
T Get2()
{
std::cout << "Start init" << std::endl;
std::vector< VisibleCopy<int> > vec(4,0);
std::cout << "End init" << std::endl;
return vec;
}
T Get3()
{
std::cout << "Start init" << std::endl;
std::vector< VisibleCopy<int> > vec;
vec.emplace_back(1);
vec.emplace_back(2);
vec.emplace_back(3);
vec.emplace_back(4);
std::cout << "End init" << std::endl;
return vec;
}
T Get4()
{
std::cout << "Start init" << std::endl;
std::vector< VisibleCopy<int> > vec;
vec.reserve(4);
vec.emplace_back(1);
vec.emplace_back(2);
vec.emplace_back(3);
vec.emplace_back(4);
std::cout << "End init" << std::endl;
return vec;
}
int main()
{
auto vec1 = Get1();
auto vec2 = Get2();
auto vec3 = Get3();
auto vec4 = Get4();
// All data as expected? Lets check:
for ( auto& el: vec1 ) { std::cout << el.Get() << std::endl; }
for ( auto& el: vec2 ) { std::cout << el.Get() << std::endl; }
for ( auto& el: vec3 ) { std::cout << el.Get() << std::endl; }
for ( auto& el: vec4 ) { std::cout << el.Get() << std::endl; }
}
O que podemos observar:
Exemplo 1) Criamos um vetor a partir de uma lista de inicializadores e talvez esperemos ver 4 vezes a construção e 4 movimentos. Mas temos 4 cópias! Isso parece um pouco misterioso, mas o motivo é a implementação da lista de inicializadores! Simplesmente, não é permitido sair da lista, pois o iterador da lista é o const T*que impossibilita a movimentação de elementos dela. Uma resposta detalhada sobre este tópico pode ser encontrada aqui: initializer_list e move semântica
Exemplo 2) Nesse caso, obtemos uma construção inicial e 4 cópias do valor. Isso não é nada de especial e é o que podemos esperar.
Exemplo 3) Também aqui, apresentamos a construção e alguns movimentos conforme o esperado. Com minha implementação stl, o vetor cresce por fator 2 toda vez. Então, vemos um primeiro construto, outro e, como o vetor é redimensionado de 1 para 2, vemos o movimento do primeiro elemento. Ao adicionar o 3, vemos um redimensionamento de 2 para 4, que precisa da mudança dos dois primeiros elementos. Tudo como esperado!
Exemplo 4) Agora reservamos espaço e preenchemos mais tarde. Agora não temos mais cópia nem movimento!
Em todos os casos, não vemos nenhum movimento ou cópia retornando o vetor de volta ao chamador! (N) O RVO está ocorrendo e nenhuma ação adicional é necessária nesta etapa!
Voltar à sua pergunta:
"Como encontrar operações de cópia espúrias em C ++"
Como visto acima, você pode introduzir uma classe proxy no meio para fins de depuração.
Tornar o copiador privado pode não funcionar em muitos casos, pois você pode ter algumas cópias desejadas e outras ocultas. Como acima, apenas o código do exemplo 4 funcionará com um copiador privado! E não posso responder à pergunta, se o exemplo 4 for o mais rápido, pois enchemos a paz pela paz.
Lamento não poder oferecer uma solução geral para encontrar cópias "indesejadas" aqui. Mesmo se você digitar seu código para chamadas de memcpy, você não encontrará tudo, pois também memcpyserá otimizado e verá diretamente algumas instruções do assembler fazendo o trabalho sem chamar a memcpyfunção de sua biblioteca .
Minha dica é não focar em um problema tão pequeno. Se você tiver problemas reais de desempenho, faça uma análise e meça. Existem tantos potenciais assassinos de desempenho, que investir muito tempo no memcpyuso espúrio não parece uma idéia que vale a pena.
std::vectormesma não seja o que ela pretende ser . Seu exemplo mostra uma cópia explícita, e é natural e a abordagem correta (novamente imho) aplicar astd::movefunção conforme você sugere, se uma cópia não é o que você deseja. Observe que alguns compiladores podem omitir a cópia se os sinalizadores de otimizações estiverem ativados e o vetor não for alterado.