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:
Wiesz, że metoda swap
(w tym obowiązkowego budowy kopii gdy RHS został lwartością) nie będzie strasznie nieskuteczny .
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:
- Nie zamieniasz się z żadnym innym.
- 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 :
- Niejawnie, co jest równoważne z wywołaniem operatora przypisania przydziału
vector
.
- 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:
- lhs ma wystarczającą pojemność przez cały czas.
- 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ć.
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
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. –
Jednym z przypadków, gdzie bezwzględnie konieczne jest konstruktor kopiowania/przenoszenia, z oczywistych powodów. – celtschk