Jak uniknąć kopiowania podczas łączenia?

10

Tworzę klasę typu łańcuchowego, na przykład mały przykład poniżej. Wydaje się, że przy łączeniu funkcji składowych wywoływany jest konstruktor kopiowania. Czy istnieje sposób na pozbycie się wywołania konstruktora kopiowania? W moim poniższym przykładzie z zabawkami oczywiste jest, że mam do czynienia tylko z tymczasowymi, dlatego też „powinien” (może nie według standardów, ale logicznie) być elitarny. Drugim najlepszym wyborem, aby skopiować elision, byłoby wywołanie konstruktora ruchu, ale tak nie jest.

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;
}

Edycja: Kilka wyjaśnień. 1. Nie zależy to od poziomu optymalizacji. 2. W moim prawdziwym kodzie mam bardziej złożone obiekty (np. Przydzielone sterty) niż ints

DDaniel
źródło
Jakiego poziomu optymalizacji używasz do kompilacji?
JVApen
2
auto b = test_class{7};nie wywołuje konstruktora kopiowania, ponieważ jest tak naprawdę równoważny, test_class b{7};a kompilatory są wystarczająco inteligentne, aby rozpoznać ten przypadek i dlatego mogą z łatwością uniknąć kopiowania. Tego samego nie można zrobić b2.
Jakiś programista koleś
W pokazanym przykładzie rzeczywista różnica między przenoszeniem a kopiowaniem może być niewielka lub duża i nie wszyscy są tego świadomi. Jeśli utkniesz tam coś w rodzaju dużego wektora, może to być inna sprawa. Zwykle ruch ma sens tylko w przypadku typów wykorzystujących zasoby (takich jak użycie dużej ilości pamięci stosu itp.) - czy tak jest w tym przypadku?
darune
Przykład wygląda na wymyślony. Czy faktycznie masz we / wy ( std::cout) w swoim kreatorze kopiowania? Bez tego kopia powinna zostać zoptymalizowana.
rustyx
@rustyx, usuń std :: cout i wyraź jawnie konstruktora kopiowania. To pokazuje, że wybór kopii nie jest zależny od std :: cout.
DDaniel

Odpowiedzi:

7
  1. Częściowa odpowiedź (nie konstruuje się b2na miejscu, ale przekształca konstrukcję kopiowania w konstrukcję przenoszenia): Możesz przeciążyć funkcję incrementelementu członkowskiego w kategorii wartości powiązanej instancji:

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

    To powoduje

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

    przenieść-skonstruować, b2ponieważ test_class{7}jest to tymczasowe i wywoływane jest &&przeciążenie test_class::increment.

  2. Aby uzyskać prawdziwą konstrukcję na miejscu (tj. Nawet konstrukcję bez ruchu), możesz zamienić wszystkie specjalne i niespecjalne funkcje constexprskładowe na wersje. Następnie możesz to zrobić

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

    a ty nie jesteś ani ruchem, ani kopią konstrukcji, za którą trzeba zapłacić. Jest to oczywiście możliwe w przypadku prostego test_class, ale nie bardziej ogólnego scenariusza, który nie dopuszcza constexprfunkcji składowych.

lubgr
źródło
Druga alternatywa sprawia również, że b2nie można jej modyfikować.
Jakiś programista koleś
1
@Someprogrammerdude Dobra uwaga. Myślę, że constinitbyłby to sposób, aby tam pójść.
lubgr
Do czego służą znaki ampersand po nazwie funkcji? Nigdy wcześniej tego nie widziałem.
Philip Nelson
1
@PililNelson są kwalifikatorami referencyjnymi i mogą dyktować to thissamo, co constfunkcje członków
kmdreko,
1

Zasadniczo przypisanie do wymaga wywołania konstruktora, tj. Kopii lub przeniesienia . Różni się to od gdy wiadomo, że po obu stronach funkcji jest ten sam odrębny obiekt. Również może odnosić się do obiektu udostępnionego podobnie jak wskaźnik.


Najprostszym sposobem jest prawdopodobnie optymalizacja konstruktora kopiowania. Ustawienie wartości jest już zoptymalizowane przez kompilator, tylko std::couttego nie można zoptymalizować.

test_class(const test_class& t) = default;

(lub po prostu usuń konstruktor kopiowania i przenoszenia)

przykład na żywo


Ponieważ problem dotyczy w zasadzie referencji, rozwiązaniem prawdopodobnie nie jest zwracanie referencji do obiektu, jeśli chcesz przerwać kopiowanie w ten sposób.

  void increment();
};

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

Trzecia metoda polega przede wszystkim na wymuszeniu kopiowania - wymaga to jednak przepisania lub enkapsulacji operacji w jedną funkcję, a tym samym całkowitego uniknięcia problemu (jestem świadomy, że to może nie być to, czego chcesz, ale może być rozwiązanie dla innych użytkowników):

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

Czwarta metoda wykorzystuje zamiast tego ruch, albo jawnie wywołany

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

lub jak widać w tej odpowiedzi .

darune
źródło
@ O'Neil myślał o innym przypadku, poprawionym
darune