Como eliminar a cópia ao encadear?

10

Estou criando uma classe do tipo encadeamento, como o pequeno exemplo abaixo. Parece que, ao encadear funções-membro, o construtor de cópia é chamado. Existe uma maneira de se livrar da chamada do construtor de cópia? No meu exemplo de brinquedo abaixo, é óbvio que estou lidando apenas com temporários e, portanto, "deveria" (talvez não pelos padrões, mas logicamente) haver uma elisão. A segunda melhor opção, copiar elision, seria chamar o construtor de movimento, mas esse não é o caso.

class test_class {
    private:
    int i = 5;
    public:
    test_class(int i) : i(i) {}
    test_class(const test_class& t) {
        i = t.i;
        std::cout << "Copy constructor"<< std::endl;
    }
    test_class(test_class&& t) {
        i = t.i;
        std::cout << "Move constructor"<< std::endl;
    }
    auto& increment(){
        i++;
        return *this;
    }
};
int main()
{
    //test_class a{7};
    //does not call copy constructor
    auto b = test_class{7};
    //calls copy constructor
    auto b2 = test_class{7}.increment();
    return 0;
}

Edit: Alguns esclarecimentos. 1. Isso não depende do nível de otimização. 2. No meu código real, tenho objetos mais complexos (por exemplo, alocados por heap) do que ints

DDaniel
fonte
Qual nível de otimização você usa para compilar?
usar o seguinte comando
2
auto b = test_class{7};não chama construtor de cópia porque é realmente equivalente test_class b{7};e os compiladores são inteligentes o suficiente para reconhecer esse caso e, portanto, podem facilmente excluir qualquer cópia. O mesmo não pode ser feito b2.
Algum programador cara
No exemplo mostrado, pode não haver muita ou nenhuma diferença entre mover e copiar e nem todos estão cientes disso. Se você colocar algo como um grande vetor, pode ser uma questão diferente. Normalmente, a movimentação apenas faz sentido para os tipos de recurso (como o uso de muitas memórias de pilha, etc.) - é esse o caso aqui?
darune 01/11/19
O exemplo parece artificial. Você realmente possui E / S ( std::cout) no seu copiador? Sem ele, a cópia deve ser otimizada.
Rustyx 01/11/19
@rustyx, remova o std :: cout e torne explícito o construtor de cópias. Isso demonstra que a cópia elision não depende de std :: cout.
precisa saber é o seguinte

Respostas:

7
  1. Resposta parcial (ela não é construída b2no lugar, mas transforma a construção da cópia em uma construção de movimentação): Você pode sobrecarregar a incrementfunção de membro na categoria de valor da instância associada:

    auto& increment() & {
        i++;
        return *this;
    }
    
    auto&& increment() && {
        i++;
       return std::move(*this);
    }

    Isso causa

    auto b2 = test_class{7}.increment();

    mover-construir b2porque test_class{7}é temporário e a &&sobrecarga de test_class::incrementé chamada.

  2. Para uma verdadeira construção no local (ou seja, nem mesmo uma construção de movimentação), você pode transformar todas as funções de membros especiais e não especiais em constexprversões. Então você pode fazer

    constexpr auto b2 = test_class{7}.increment();

    e você não precisa mover nem construir uma cópia para pagar. Obviamente, isso é possível para o test_classcenário simples , mas não para um cenário mais geral que não permita constexprfunções de membro.

lubgr
fonte
A segunda alternativa também torna b2não modificável.
Algum programador
11
@Someprogrammerdude Good point. Eu acho constinitque seria o caminho a percorrer lá.
lubgr
Qual é o objetivo dos e comerciais após o nome da função? Eu nunca vi isso antes.
Philip Nelson
11
@PhilipNelson são ref-eliminatórias e pode ditar o que thisé exatamente o mesmo que constas funções de membro
kmdreko
1

Basicamente, atribuir uma a um requer a invocação de um construtor, ou seja, uma cópia ou uma movimentação . Isso é diferente da onde é conhecido nos dois lados da função o mesmo objeto distinto. Também uma pode se referir a um objeto compartilhado como um ponteiro.


A maneira mais simples é provavelmente fazer com que o construtor de cópias seja totalmente otimizado. A configuração do valor já é otimizada pelo compilador, é exatamente std::coutisso que não pode ser otimizado.

test_class(const test_class& t) = default;

(ou apenas remova a cópia e mova o construtor)

exemplo ao vivo


Como seu problema é basicamente com a referência, uma solução provavelmente não está retornando uma referência ao objeto se você deseja parar de copiar dessa maneira.

  void increment();
};

auto b = test_class{7};//does not call copy constructor
b.increment();//does not call copy constructor

Um terceiro método é apenas confiar na remoção de cópias - no entanto, isso requer uma reescrita ou encapsulamento da operação em uma função e, portanto, evitando o problema completamente (sei que isso pode não ser o que você deseja, mas pode ser um solução para outros usuários):

auto b2 = []{test_class tmp{7}; tmp.increment().increment().increment(); return tmp;}(); //<-- b2 becomes 10 - copy constructor not called

Um quarto método está usando uma movimentação, invocada explicitamente

auto b2 = std::move(test_class{7}.increment());

ou como visto nesta resposta .

darune
fonte
@ O'Neil estava pensando em outro caso - ty, corrigido
darune