Estou aprendendo C ++ no momento e tento evitar os maus hábitos. Pelo que entendi, o clang-tidy contém muitas "práticas recomendadas" e tento segui-las da melhor maneira possível (embora ainda não entenda necessariamente por que são consideradas boas), mas não tenho certeza se estou entenda o que é recomendado aqui.
Usei esta aula do tutorial:
class Creature
{
private:
std::string m_name;
public:
Creature(const std::string &name)
: m_name{name}
{
}
};
Isso leva a uma sugestão do clang-tidy de que eu deveria passar por valor em vez de referência e uso std::move
. Se o fizer, recebo a sugestão de fazer name
uma referência (para garantir que não seja copiado todas as vezes) e o aviso de que std::move
não terá qualquer efeito por name
ser um, const
portanto, devo removê-lo.
A única maneira de não receber um aviso é removendo por const
completo:
Creature(std::string name)
: m_name{std::move(name)}
{
}
O que parece lógico, já que o único benefício de const
era evitar mexer na string original (o que não acontece porque passei por valor). Mas eu li no CPlusPlus.com :
Embora, observe que, na biblioteca padrão, a movimentação implica que o objeto movido é deixado em um estado válido, mas não especificado. O que significa que, após tal operação, o valor do objeto movido só deve ser destruído ou atribuído a um novo valor; acessá-lo de outra forma produz um valor não especificado.
Agora imagine este código:
std::string nameString("Alex");
Creature c(nameString);
Como nameString
é passado por valor, std::move
só vai invalidar name
dentro do construtor e não tocar na string original. Mas quais são as vantagens disso? Parece que o conteúdo é copiado apenas uma vez - se eu passar por referência quando chamo m_name{name}
, se eu passar por valor quando eu o passar (e então ele é movido). Eu entendo que isso é melhor do que passar por valor e não usar std::move
(porque ele é copiado duas vezes).
Portanto, duas questões:
- Eu entendi corretamente o que está acontecendo aqui?
- Existe alguma vantagem em usar
std::move
passar por referência e apenas ligarm_name{name}
?
Creature c("John");
faz uma cópia extrastd::string_view
e o SSO também.clang-tidy
é uma ótima maneira de ficar obcecado por microotimizações desnecessárias em detrimento da legibilidade. A questão a fazer aqui, antes de mais nada, é quantas vezes realmente chamamos oCreature
construtor.Respostas:
Sim.
Uma assinatura de função fácil de entender sem sobrecargas adicionais. A assinatura revela imediatamente que o argumento será copiado - isso evita que os chamadores se perguntem se uma
const std::string&
referência pode ser armazenada como um membro de dados, possivelmente se tornando uma referência pendente mais tarde. E não há necessidade de sobrecarregar os argumentosstd::string&& name
econst std::string&
para evitar cópias desnecessárias quando rvalues são passados para a função. Passando um lvaluepara a função que recebe seu argumento por valor causa uma cópia e uma construção de movimento. Passando um rvalue para a mesma função
causa duas construções de movimento. Em contraste, quando o parâmetro da função é
const std::string&
, sempre haverá uma cópia, mesmo ao passar um argumento rvalue. Isso é claramente uma vantagem, desde que o tipo de argumento seja barato para mover-construir (este é o casostd::string
).Mas há uma desvantagem a considerar: o raciocínio não funciona para funções que atribuem o argumento da função a outra variável (em vez de inicializá-lo):
causará uma desalocação do recurso ao qual
m_name
se refere antes de ser reatribuído. Recomendo a leitura do item 41 em Effective Modern C ++ e também esta questão .fonte
move
, o espaço será desalocado. Se eu não usarmove
, ele só será desalocado se o espaço alocado for muito pequeno para conter a nova string, levando a um melhor desempenho. Isso é correto?m_name
partir de umconst std::string&
parâmetro, a memória interna é reutilizada enquantom_name
se ajusta. Ao atribuir um movimentom_name
, a memória deve ser desalocada de antemão. Caso contrário, seria impossível "roubar" os recursos do lado direito da atribuição.Um lvalue passado vincula-se a
name
e depois é copiado param_name
.Um rvalue passado vincula-se a
name
e depois é copiado param_name
.Um lvalue passado é copiado para e
name
, em seguida, movido param_name
.Um passou rvalue é movida para
name
, em seguida, é transferida param_name
.Um lvalue passado vincula-se a
name
e depois é copiado param_name
.Um rvalue passado liga-se a
rname
e depois é movido param_name
.Como as operações de movimentação são geralmente mais rápidas do que as cópias, (1) é melhor do que (0) se você passar muitos temporários. (2) é ideal em termos de cópias / movimentos, mas requer repetição de código.
A repetição do código pode ser evitada com encaminhamento perfeito :
Você pode opcionalmente querer restringir a
T
fim de restringir o domínio de tipos com os quais este construtor pode ser instanciado (como mostrado acima). C ++ 20 visa simplificar isso com Conceitos .Em C ++ 17, os prvalues são afetados pela eliminação de cópia garantida , que - quando aplicável - reduzirá o número de cópias / movimentos ao passar argumentos para funções.
fonte
Creature(const std::string &name) : m_name{std::move(name)} { }
no (2) ?Como você passa não é a única variável aqui, o que você passa faz a grande diferença entre os dois.
Em C ++, nós temos todos os tipos de categorias de valor e este "idioma" existe para os casos em que você passa em um rvalue (como
"Alex-string-literal-that-constructs-temporary-std::string"
oustd::move(nameString)
), o que resulta em 0 cópias destd::string
ser feito (o tipo nem sequer tem que ser copy-constructible para argumentos rvalue) e usa apenasstd::string
o construtor de movimento.Perguntas e respostas relacionadas .
fonte
Existem várias desvantagens da abordagem de passagem por valor e movimento sobre a referência de passagem por (rv):
fonte
m_name{name}
parte em que ele é copiado?std::string nameString("Alex"); Creature c(nameString);
um objeto énameString
, outro é um argumento de função e o terceiro é um campo de classe.No meu caso, mudar para passar por valor e, em seguida, fazer um std: move causou um erro heap-use-after-free no Address Sanitizer.
https://travis-ci.org/github/acgetchell/CDT-plusplus/jobs/679520360#L3165
Então, eu desliguei, assim como a sugestão do clang-tidy.
https://github.com/acgetchell/CDT-plusplus/compare/80c96789f0a2...0d78fd63b332
fonte