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::vector
e 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 memcpy
será otimizado e verá diretamente algumas instruções do assembler fazendo o trabalho sem chamar a memcpy
funçã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 memcpy
uso espúrio não parece uma idéia que vale a pena.
std::vector
mesma 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::move
funçã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.