Aby zrozumieć, dlaczego jest to dobry wzorzec, powinniśmy zbadać alternatywy, zarówno w C ++ 03, jak i C ++ 11.
Mamy metodę C ++ 03 polegającą na zrobieniu std::string const&
:
struct S
{
std::string data;
S(std::string const& str) : data(str)
{}
};
w takim przypadku zawsze zostanie wykonana jedna kopia. Jeśli konstruujesz z surowego ciągu C, std::string
zostanie skonstruowany, a następnie skopiowany ponownie: dwie alokacje.
Istnieje metoda C ++ 03 polegająca na pobraniu odwołania do a std::string
, a następnie zamianie go na lokalny std::string
:
struct S
{
std::string data;
S(std::string& str)
{
std::swap(data, str);
}
};
to jest wersja „semantyki przenoszenia” w języku C ++ 03 i swap
często można ją zoptymalizować, aby była bardzo tania (podobnie jak a move
). Należy to również analizować w kontekście:
S tmp("foo"); // illegal
std::string s("foo");
S tmp2(s); // legal
i zmusza cię do utworzenia nietymczasowego std::string
, a następnie odrzuć go. (Tymczasowy std::string
puszka nie wiążą się const odnośnik). Dokonuje się jednak tylko jednego przydziału. Wersja C ++ 11 &&
wymagałaby wywołania go z std::move
lub z wartością tymczasową: wymaga to jawnego utworzenia kopii poza wywołaniem i przeniesienia tej kopii do funkcji lub konstruktora.
struct S
{
std::string data;
S(std::string&& str): data(std::move(str))
{}
};
Posługiwać się:
S tmp("foo"); // legal
std::string s("foo");
S tmp2(std::move(s)); // legal
Następnie możemy wykonać pełną wersję C ++ 11, która obsługuje zarówno kopiowanie, jak i move
:
struct S
{
std::string data;
S(std::string const& str) : data(str) {} // lvalue const, copy
S(std::string && str) : data(std::move(str)) {} // rvalue, move
};
Następnie możemy sprawdzić, jak to jest używane:
S tmp( "foo" ); // a temporary `std::string` is created, then moved into tmp.data
std::string bar("bar"); // bar is created
S tmp2( bar ); // bar is copied into tmp.data
std::string bar2("bar2"); // bar2 is created
S tmp3( std::move(bar2) ); // bar2 is moved into tmp.data
Jest całkiem jasne, że ta technika 2 przeciążenia jest co najmniej tak samo wydajna, jeśli nie bardziej, niż dwa powyższe style C ++ 03. Nazwę tę wersję z 2 przeciążeniami jako „najbardziej optymalną”.
Teraz przyjrzymy się wersji do pobrania:
struct S2 {
std::string data;
S2( std::string arg ):data(std::move(x)) {}
};
w każdym z tych scenariuszy:
S2 tmp( "foo" ); // a temporary `std::string` is created, moved into arg, then moved into S2::data
std::string bar("bar"); // bar is created
S2 tmp2( bar ); // bar is copied into arg, then moved into S2::data
std::string bar2("bar2"); // bar2 is created
S2 tmp3( std::move(bar2) ); // bar2 is moved into arg, then moved into S2::data
Jeśli porównasz tę wersję obok siebie z „najbardziej optymalną” wersją, zrobimy dokładnie jedną dodatkową move
! Ani razu nie robimy nic więcej copy
.
Więc jeśli założymy, że move
jest tania, ta wersja zapewnia nam prawie taką samą wydajność, jak wersja najbardziej optymalna, ale 2 razy mniej kodu.
A jeśli bierzesz powiedzmy od 2 do 10 argumentów, redukcja kodu jest wykładnicza - 2x mniej z 1 argumentem, 4x z 2, 8x z 3, 16x z 4, 1024x z 10 argumentami.
Teraz możemy obejść ten problem poprzez doskonałe przekazywanie i SFINAE, pozwalające na napisanie pojedynczego konstruktora lub szablonu funkcji, który przyjmuje 10 argumentów, robi SFINAE, aby upewnić się, że argumenty są odpowiedniego typu, a następnie przenosi lub kopiuje je do stan lokalny zgodnie z wymaganiami. Chociaż zapobiega to tysiąckrotnemu wzrostowi rozmiaru programu, nadal może istnieć cały stos funkcji generowanych z tego szablonu. (instancje funkcji szablonu generują funkcje)
A wiele generowanych funkcji oznacza większy rozmiar kodu wykonywalnego, co samo w sobie może zmniejszyć wydajność.
Kosztem kilku move
sekund otrzymujemy krótszy kod i prawie taką samą wydajność, a często łatwiejszy do zrozumienia kod.
Teraz to działa tylko dlatego, że wiemy, kiedy wywoływana jest funkcja (w tym przypadku konstruktor), że będziemy potrzebować lokalnej kopii tego argumentu. Chodzi o to, że jeśli wiemy, że będziemy robić kopię, powinniśmy poinformować dzwoniącego, że robimy kopię, umieszczając ją na naszej liście argumentów. Następnie mogą zoptymalizować fakt, że dadzą nam kopię (na przykład przechodząc do naszej argumentacji).
Inną zaletą techniki „weź według wartości” jest to, że często konstruktory przenoszenia nie są wyjątkiem. Oznacza to, że funkcje, które pobierają wartość i wychodzą z argumentu, często nie są wyjątkiem, przenosząc dowolne throw
s z ciała do zakresu wywołującego (kto może czasami tego uniknąć poprzez bezpośrednią konstrukcję lub skonstruować przedmioty i move
wprowadzić je do argumentu, aby kontrolować, gdzie ma miejsce rzucanie) .Robienie metod zamiast rzutów często jest tego warte.