2013-08-18 15 views
21

I widać, że mówi się, że operator= zapisywane się parametr samego rodzaju przez wartość służy zarówno jako operatora przypisania kopiowania i przenoszenia operatorowi przypisanie C++ 11:Kiedy przeciążanie przechodzi przez odniesienie (wartość l i wartość r) preferowane do wartości przechodzącej?

Foo& operator=(Foo f) 
{ 
    swap(f); 
    return *this; 
} 

Jeżeli alternatywą byłoby więcej niż dwa razy tyle linii z partii kodu powtórzenia i możliwości błędu:

Foo& operator=(const Foo& f) 
{ 
    Foo f2(f); 
    swap(f2); 
    return *this; 
} 

Foo& operator=(Foo&& f) 
{ 
    Foo f2(std::move(f)); 
    swap(f2); 
    return *this; 
} 

W jakich okolicznościach jest gdy ref-to-const i przeciążenia wartość R korzystne przejściu przez wartość, czy jest to konieczne? Myślę o std::vector::push_back, na przykład, który jest zdefiniowany jako dwa przeciążeń:

void push_back (const value_type& val); 
void push_back (value_type&& val); 

po pierwszym przykładzie, gdzie przechodzą przez wartość służy jako kopia przypisania operatora i przenieść operator przypisania, nie może być push_back zdefiniowane w Standard, aby był pojedynczą funkcją?

void push_back (value_type val); 
+0

Formularz wartości pośredniej może być droższy (dwa ruchy zamiast jednego), gdy obiekt jest ciężki do przenoszenia (na przykład zawiera wiele elementów danych). – Angew

+0

Może to być częściowo spowodowane przyczynami historycznymi, ponieważ 'std :: vector' ma już' T const & 'przeciążenie w C++ 03. Może istnieć kod, który zależy od tego, czy istnieje przeciążenie (powiedzmy, że ktoś wybrał adres funkcji członka). Zauważ też, że w standardzie nie ma potrzeby optymalizacji linii kodu, które będą musiały zostać zaimplementowane, ponieważ jest napisane raz przez implementator kompilatora, ale używane prawie wszędzie. Dodatkowy koszt rozwoju jest znikomy. –

+0

Jednym z przypadków, gdzie bezwzględnie konieczne jest konstruktor kopiowania/przenoszenia, z oczywistych powodów. – celtschk

Odpowiedz

26

Dla typów, których kopia przypisanie operator może recyklingu zasobów, zamieniając z kopią prawie nigdy nie jest najlepszym sposobem realizowania operatora przypisania kopia. Na przykład spojrzeć na std::vector:

Klasa ta zarządza dynamicznie wielkości bufora i utrzymuje zarówno capacity (maksymalna długość bufor może pomieścić) i size (aktualna długość). Jeśli operator przypisania kopii vector jest zaimplementowany swap, to bez względu na wszystko, nowy bufor jest zawsze przydzielany, jeśli rhs.size() != 0.

Jednak, jeśli lhs.capacity() >= rhs.size(), w ogóle nie trzeba przydzielać nowego bufora. Można po prostu przypisać/skonstruować elementy z rhs do lhs. Kiedy typ elementu może być trywialnie kopiowany, może to sprowadzić się do niczego poza memcpy. Może to być dużo, dużo łatwiejsze niż przydzielanie i zwalnianie bufora.

Ten sam numer dla std::string.

sam problem dla MyType gdy MyType ma członków danych, które są std::vector i/lub std::string.

Istnieją tylko 2 razy można rozważyć realizację zadania kopiowania z zamiany:

  1. Wiesz, że metoda swap (w tym obowiązkowego budowy kopii gdy RHS został lwartością) nie będzie strasznie nieskuteczny .

  2. Wiesz, że zawsze będziesz potrzebował , aby operator przypisywania kopii miał wyjątkową gwarancję bezpieczeństwa.

Jeśli nie jesteś pewien, około 2, innymi słowy uważasz, że operator przypisania kopia może czasami potrzebują silnej gwarancji bezpieczeństwa wyjątek, nie realizować zadania w zakresie wymiany. Klienci mogą z łatwością uzyskać tę samą gwarancję, jeśli podasz jedno z następujących:

  1. Nie zamieniasz się z żadnym innym.
  2. Nie należy przenosić operatora przypisania.

Na przykład:

template <class T> 
T& 
strong_assign(T& x, T y) 
{ 
    using std::swap; 
    swap(x, y); 
    return x; 
} 

czyli

template <class T> 
T& 
strong_assign(T& x, T y) 
{ 
    x = std::move(y); 
    return x; 
} 

Teraz będzie kilka rodzajów gdzie realizacji zadania kopiowania z wymiany będzie sensu. Jednak te typy będą wyjątkiem, nie regułą.

On:

void push_back(const value_type& val); 
void push_back(value_type&& val); 

Wyobraź vector<big_legacy_type> gdzie:

class big_legacy_type 
{ 
public: 
     big_legacy_type(const big_legacy_type&); // expensive 
     // no move members ... 
}; 

Gdybyśmy mieli tylko:

void push_back(value_type val); 

Następnie push_back ing lwartością big_legacy_type w vector wymagałoby 2 kopie zamiast 1, nawet gdy capacity było wystarczające. To byłaby katastrofa, mądre wykonanie.

Aktualizacja

Oto HelloWorld, że powinieneś być w stanie uruchomić na dowolnej platformie C++ 11 zgodnego:

#include <vector> 
#include <random> 
#include <chrono> 
#include <iostream> 

class X 
{ 
    std::vector<int> v_; 
public: 
    explicit X(unsigned s) : v_(s) {} 

#if SLOW_DOWN 
    X(const X&) = default; 
    X(X&&) = default; 
    X& operator=(X x) 
    { 
     v_.swap(x.v_); 
     return *this; 
    } 
#endif 
}; 

std::mt19937_64 eng; 
std::uniform_int_distribution<unsigned> size(0, 1000); 

std::chrono::high_resolution_clock::duration 
test(X& x, const X& y) 
{ 
    auto t0 = std::chrono::high_resolution_clock::now(); 
    x = y; 
    auto t1 = std::chrono::high_resolution_clock::now(); 
    return t1-t0; 
} 

int 
main() 
{ 
    const int N = 1000000; 
    typedef std::chrono::duration<double, std::nano> nano; 
    nano ns(0); 
    for (int i = 0; i < N; ++i) 
    { 
     X x1(size(eng)); 
     X x2(size(eng)); 
     ns += test(x1, x2); 
    } 
    ns /= N; 
    std::cout << ns.count() << "ns\n"; 
} 

Mam zakodowanej X „s skopiować dwa sposoby operator przypisania :

  1. Niejawnie, co jest równoważne z wywołaniem operatora przypisania przydziału vector.
  2. Idiomem kopiowanie/zamiana, sugestywnie pod makro SLOW_DOWN. Pomyślałem o nazwaniu go SLEEP_FOR_AWHILE, ale ten sposób jest znacznie gorszy niż instrukcje snu, jeśli używasz urządzenia zasilanego baterią.

Test konstruuje losowo wybrane wielkości vector<int> s od 0 do 1000 i przypisuje je milion razy. Mierzy każdy z nich, sumuje czasy, a następnie znajduje średni czas w nanosekundach zmiennoprzecinkowych i drukuje je na zewnątrz. Jeśli dwa kolejne wywołania zegara wysokiej rozdzielczości nie zwrócą wartości mniejszej niż 100 nanosekund, możesz zwiększyć długość wektorów.

Oto moje wyniki:

$ clang++ -std=c++11 -stdlib=libc++ -O3 test.cpp 
$ a.out 
428.348ns 
$ a.out 
438.5ns 
$ a.out 
431.465ns 
$ clang++ -std=c++11 -stdlib=libc++ -O3 -DSLOW_DOWN test.cpp 
$ a.out 
617.045ns 
$ a.out 
616.964ns 
$ a.out 
618.808ns 

widzę 43% spadek wydajności dla idiomu copy/wymiany z tego prostego testu. YMMV.

Powyższy test ma przeciętnie wystarczającą pojemność na połowę lhs. Jeśli podejmiemy to albo skrajnie:

  1. lhs ma wystarczającą pojemność przez cały czas.
  2. lhs ma wystarczającą pojemność w żadnym z przypadków.

wtedy przewaga wydajności domyślnego przypisania kopii w stosunku do idiomu kopiowania/wymiany waha się od około 560% do 0%. Idiom kopiowania/wymiany nigdy nie jest szybszy i może być znacznie wolniejszy (dla tego testu).

Chcesz prędkość? Zmierzyć.

+0

Czy można by skopiować pomoc dotyczącą elizacji? Czy to usunie * drugą * kopię w drugim przypadku? Próbuję zejść z http://cpp-next.com/archive/2009/08/want-speed-pass-by-value/ Dzięki – cdmh

+8

To nie pomaga, gdy rhs jest lwartością. Technicznie nie ma nic złego w tym artykule. Pozostawia jednak złe wrażenie: Zawsze kopiuj według wartości jest straszną radą. Jeśli o to chodzi, zawsze "cokolwiek" jest okropną radą. Artykuł powinien pozostawiać wrażenie, że ** czasami ** może być wartością po wartości parametru, lub nawet najlepszą rzeczą. Ale zbyt wiele osób przeoczyło "czasami", ponieważ artykuł tego nie wyjaśnia. Zachowaj wartość pass-by-value w swoim przyborniku. Ale używanie go domyślnie, bez projektowania, wymaga problemów z wydajnością. –

+0

@HowardHinnant Jestem jednym z *** "czasami" *** * znaczy * *** "zawsze" *** głupcy. Dzięki za punkt. – Manu343726

Powiązane problemy