Recentemente, tive o seguinte
struct data {
std::vector<int> V;
};
data get_vector(int n)
{
std::vector<int> V(n,0);
return {V};
}
O problema com este código é que, quando a estrutura é criada, ocorre uma cópia e a solução é escrever return {std :: move (V)}
Existe um analisador de código ou linter que detecta operações de cópia falsas? Nem o cppcheck, o cpplint nem o clang-tidy podem fazer isso.
Edição: Vários pontos para tornar minha pergunta mais clara:
- Sei que ocorreu uma operação de cópia porque usei o compiler explorer e ele mostra uma chamada para o memcpy .
- Pude identificar que ocorreram operações de cópia observando o padrão yes. Mas minha idéia inicial errada foi que o compilador otimizaria esta cópia. Eu estava errado.
- (Provavelmente) não é um problema do compilador, pois clang e gcc produzem código que produz um memcpy .
- O memcpy pode ser barato, mas não consigo imaginar circunstâncias em que copiar a memória e excluir o original seja mais barato do que passar um ponteiro por um std :: move .
- A adição do std :: move é uma operação elementar. Eu imaginaria que um analisador de código seria capaz de sugerir essa correção.
c++
code-analysis
static-code-analysis
cppcheck
Mathieu Dutour Sikiric
fonte
fonte
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.Respostas:
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:
E agora vamos começar algumas experiências:
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ânticaExemplo 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 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émmemcpy
será otimizado e verá diretamente algumas instruções do assembler fazendo o trabalho sem chamar amemcpy
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.fonte
Você colocou seu aplicativo completo no explorer do compilador e ativou as otimizações? Caso contrário, o que você viu no explorador de compilador pode ou não ser o que está acontecendo com seu aplicativo.
Um problema com o código que você postou é que você primeiro cria um
std::vector
e depois o copia para uma instância dedata
. Seria melhor inicializardata
com o vetor:Além disso, se você der à definição do explorer do compilador
data
eget_vector()
, e mais nada, ele deverá esperar o pior. Se você realmente fornecer algum código-fonte que useget_vector()
, observe qual assembly é gerado para esse código-fonte. Veja este exemplo para saber o que a modificação acima, o uso real e as otimizações do compilador podem causar a produção do compilador.fonte