C++ har alltid gett snabba program. Tyvärr har det fram till C++11 funnits en envis vårta som saktar ner många C++-program: skapandet av tillfälliga objekt. Ibland kan dessa tillfälliga objekt optimeras bort av kompilatorn (till exempel genom optimering av returvärdet). Men detta är inte alltid fallet, och det kan resultera i dyra objektkopior. Vad menar jag?
Vi kan säga att du har följande kod:
#include <iostream>using namespace std;vector<int> doubleValues (const vector<int>& v){ vector<int> new_values; new_values.reserve(v.size()); for (auto itr = v.begin(), end_itr = v.end(); itr != end_itr; ++itr ) { new_values.push_back( 2 * *itr ); } return new_values;}int main(){ vector<int> v; for ( int i = 0; i < 100; i++ ) { v.push_back( i ); } v = doubleValues( v );}
Om du har gjort mycket högpresterande arbete i C++, ber jag om ursäkt för den smärta som det medförde. Om du inte har det – låt oss gå igenom varför den här koden är hemsk C++03-kod. (Resten av den här handledningen kommer att handla om varför det är bra C++11-kod.) Problemet är med kopiorna. När doubleValues anropas konstruerar den en vektor, new_values, och fyller den. Detta i sig kanske inte är idealisk prestanda, men om vi vill behålla vår ursprungliga vektor obefläckad behöver vi en andra kopia. Men vad händer när vi slår till på return-anvisningen?
Hela innehållet i new_values måste kopieras! I princip kan det finnas upp till två kopior här: en till ett tillfälligt objekt som ska returneras och en andra när vektortilldelningsoperatorn körs på raden v = doubleValues( v );. Den första kopian kan optimeras bort av kompilatorn automatiskt, men det går inte att undvika att tilldelningen till v måste kopiera alla värden igen,vilket kräver en ny minnesallokering och en ny iteration över helavektorn.
Detta exempel kan vara lite konstruerat – och det går naturligtvis att hitta sätt att undvika den här typen av problem – t.ex. genom att lagra och returneravektorn med hjälp av en pekare, eller genom att överlämna en vektor som skall fyllas på. Saken är den att ingen av dessa programmeringsstilar är särskilt naturliga. Dessutom har ett tillvägagångssätt som kräver att man returnerar en pekare infört minst en ytterligare minnesallokering, och ett av designmålen för C++ är att undvika minnesallokeringar.
Det värsta med hela den här historien är att det objekt som returneras fråndoubleValues är ett tillfälligt värde som inte längre behövs. När du har raden v = doubleValues( v ) kommer resultatet av doubleValues( v ) bara att slängas när det har kopierats! I teorin borde det vara möjligt att hoppa över hela kopieringen och bara stjäla pekaren i den tillfälliga vektorn och behålla den i v. Varför kan vi inte flytta objektet? I C++03 är svaret att det inte fanns något sätt att avgöra om ett objekt var temporärt eller inte, du var tvungen att köra samma kod i tilldelningsoperatorn eller kopieringskonstruktören, oavsett varifrån värdet kom, så det var inte möjligt att stjäla. I C++11 är svaret – du kan!
Det är vad rvalue-referenser och move-semantik är till för! Move semantics låter dig undvika onödiga kopior när du arbetar med tillfälliga objekt som är på väg att förångas och vars resurser säkert kan tas från det tillfälliga objektet och användas av ett annat.
Move semantics är beroende av en ny funktion i C++11, kallad rvalue references, som du vill förstå för att verkligen förstå vad som händer. Så först pratar vi om vad en rvalue är och sedan vad en rvalue-referens är.Slutligen återkommer vi till flyttsemantiken och hur den kan implementeras med rvalue-referenser.
Rvalues och lvalues – bittra rivaler eller bästa vänner?
I C++ finns det rvalues och lvalues. Ett lvärde är ett uttryck varsadress kan tas, ett lokaliseringsvärde – i princip ger ett lvärde en (halv)permanent bit av minnet. Du kan göra tilldelningar till lvalues. Forexample:
int a;a = 1; // here, a is an lvalue
Du kan också ha lvalues som inte är variabler:
int x;int& getRef () { return x;}getRef() = 4;
Här returnerar getRef en referens till en global variabel, så den returnerar ett värde som lagras på en permanent plats. (Du skulle bokstavligen kunna skriva & getRef() om du ville, och det skulle ge dig adressen till x.)
Rvalues är – ja, rvalues är inte lvalues. Ett uttryck är ett r-värde om det resulterar i ett tillfälligt objekt. Till exempel:
int x;int getVal (){ return x;}getVal();
Här är getVal() ett r-värde – det värde som returneras är inte en referens till x, det är bara ett tillfälligt värde. Detta blir lite mer intressant om vi använder riktiga objekt i stället för siffror:
string getName (){ return "Alex";}getName();
Här returnerar getName en sträng som konstrueras inne i funktionen. Du kan tilldela resultatet av getName till en variabel:
string name = getName();
Men du tilldelar från ett tillfälligt objekt, inte från något värde som har en fast plats. getName() är ett rvalue.
Detektering av tillfälliga objekt med rvalue-referenser
Det viktiga är att rvalues hänvisar till tillfälliga objekt – precis som värdet som returneras från doubleValues. Skulle det inte vara fantastiskt om vi utan minsta tvivel kunde veta att ett värde som returneras från ett uttryck är tillfälligt, och på något sätt skriva kod som är överbelastad för att bete sig annorlunda för tillfälliga objekt? Varför, ja, ja det skulle det verkligen vara. Och det är detta som rvalue-referenser är till för. En rvalue-referens är en referens som endast binder till ett tillfälligt objekt. Vad menar jag?
För C++11 kunde man, om man hade ett tillfälligt objekt, använda en ”vanlig” eller ”lvalue-referens” för att binda det, men bara om den var const:
const string& name = getName(); // okstring& name = getName(); // NOT ok
Intuitionen här är att man inte kan använda en ”mutable”-referens, eftersom man i så fall skulle kunna ändra ett objekt som är på väg att försvinna, och det skulle vara farligt. Observera förresten att om man håller fast vid en konstreferens till ett temporärt objekt säkerställer man att det temporära objektet inte förstörs omedelbart. Detta är en trevlig garanti i C++, men det är fortfarande ett tillfälligt objekt, så du vill inte ändra det.
I C++11 finns det dock en ny typ av referens, en ”rvalue-referens”, som gör att du kan binda en föränderlig referens till ett r-värde, men inte till ett l-värde. Med andra ord är rvalue-referenser perfekta för att upptäcka om ett värde är ett tillfälligt objekt eller inte. Rvalue-referenser använder &&-syntaxen istället för bara &, och kan vara const och non-const, precis som lvalue-referenser, även om du sällan kommer att se en const rvalue-referens (som vi kommer att se är mutablereferenser typiskt sett poängen):
const string&& name = getName(); // okstring&& name = getName(); // also ok - praise be!
Så långt är allt det här gott och väl, men hur hjälper det? Det viktigaste med lvalue-referenser kontra rvalue-referenser är vad som händer när du skriver funktioner som tar lvalue- eller rvalue-referenser som argument. Låt oss säga att vi har två funktioner:
printReference (const String& str){ cout << str;}printReference (String&& str){ cout << str;}
Nu blir beteendet intressant – printReference-funktionen som tar en lvalue-referens kommer att acceptera vilket argument som helst, oavsett om det är ett lvalue eller ett rvalue, och oavsett om lvalue eller rvalue är föränderligt eller inte. Men i närvaro av den andra överladdningen, printReference som tar en r-värde-referens, kommer den att ta emot alla värden utom mutablaervärde-referenser. Med andra ord, om du skriver:
string me( "alex" );printReference( me ); // calls the first printReference function, taking an lvalue referenceprintReference( getName() ); // calls the second printReference function, taking a mutable rvalue reference
Nu har vi ett sätt att avgöra om en referensvariabel hänvisar till ett temporärtobjekt eller till ett permanent objekt. Rvalue-referensversionen av metoden är som den hemliga bakdörrens ingång till klubben som man bara kan komma in i om man är ett tillfälligt objekt (tråkig klubb, antar jag). Nu när vi har vår metod för att avgöra om ett objekt var tillfälligt eller permanent, hur kan vi använda den?
Förflyttningskonstruktör och flyttningstilldelningsoperatör
Det vanligaste mönstret som du kommer att se när du arbetar med r-värdereferenser är att skapa en flyttningskonstruktör och en flyttningstilldelningsoperatör (som följer samma principer). En flyttkonstruktör, liksom en kopieringskonstruktör, tar en instans av ett objekt som argument och skapar en ny instans baserad på det ursprungliga objektet. Move-konstruktören kan dock undvika omallokering av minnet eftersom vi vet att den har fått ett tillfälligt objekt, så istället för att kopiera objektets fält flyttar vi dem.
Vad innebär det att flytta ett fält i objektet? Om fältet är en primitiv typ, som int, kopierar vi det bara. Det blir mer intressant om fältet är en pekare: här kan vi i stället för att allokera och initialisera nytt minne helt enkelt stjäla pekaren och nollställa pekaren i det tillfälliga objektet! Vi vet att det tillfälliga objektet inte längre kommer att behövas, så vi kan ta dess pekare underifrån.
Föreställ dig att vi har en enkel ArrayWrapper-klass, så här:
class ArrayWrapper{ public: ArrayWrapper (int n) : _p_vals( new int ) , _size( n ) {} // copy constructor ArrayWrapper (const ArrayWrapper& other) : _p_vals( new int ) , _size( other._size ) { for ( int i = 0; i < _size; ++i ) { _p_vals = other._p_vals; } } ~ArrayWrapper () { delete _p_vals; } private: int *_p_vals; int _size;};
Bemärk att kopieringskonstruktören både måste allokera minne och kopiera varje värde från arrayen, ett i taget! Det är mycket arbete för en kopia. Låt oss lägga till en move-konstruktör och få en enorm effektivitet.
class ArrayWrapper{public: // default constructor produces a moderately sized array ArrayWrapper () : _p_vals( new int ) , _size( 64 ) {} ArrayWrapper (int n) : _p_vals( new int ) , _size( n ) {} // move constructor ArrayWrapper (ArrayWrapper&& other) : _p_vals( other._p_vals ) , _size( other._size ) { other._p_vals = NULL; other._size = 0; } // copy constructor ArrayWrapper (const ArrayWrapper& other) : _p_vals( new int ) , _size( other._size ) { for ( int i = 0; i < _size; ++i ) { _p_vals = other._p_vals; } } ~ArrayWrapper () { delete _p_vals; }private: int *_p_vals; int _size;};
Wow, move-konstruktören är faktiskt enklare än copy-konstruktören!Det är en stor bedrift. De viktigaste sakerna att lägga märke till är:
- Parametern är en non-const rvalue-referens
- other._p_vals sätts till NULL
Den andra observationen förklarar den första – vi kunde inte sätta other._p_vals tillNULL om vi hade tagit en const rvalue-referens. Men varför måste vi sättaother._p_vals = NULL? Anledningen är destruktorn – när det tillfälliga objektet går utanför räckvidden, precis som alla andra C++-objekt, kommer dess destruktor att köras.När dess destruktor körs kommer den att frigöra _p_vals. Samma _p_vals som vi just kopierade! Om vi inte sätter other._p_vals till NULL skulle flytten egentligen inte vara en flytt – det skulle bara vara en kopia som leder till en krasch senare när vi börjar använda frigjort minne. Detta är hela poängen med en move-konstruktör: att undvika en kopia genom att ändra det ursprungliga, tillfälliga objektet!
Också här fungerar överbelastningsreglerna så att move-konstruktören endast anropas för ett tillfälligt objekt – och endast ett tillfälligt objekt som kan ändras. Detta innebär att om du har en funktion som returnerar ett const-objekt kommer det att leda till att copy-konstruktorn körs i stället för move-konstruktorn – skriv inte sådan här kod:
const ArrayWrapper getArrayWrapper (); // makes the move constructor useless, the temporary is const!
Det finns fortfarande ytterligare en situation som vi inte har diskuterat hur man hanterar i en move-konstruktör – när vi har ett fält som är ett objekt. Tänk dig till exempel att vi i stället för att ha ett fält för storlek har ett metadatafält som ser ut så här:
class MetaData{public: MetaData (int size, const std::string& name) : _name( name ) , _size( size ) {} // copy constructor MetaData (const MetaData& other) : _name( other._name ) , _size( other._size ) {} // move constructor MetaData (MetaData&& other) : _name( other._name ) , _size( other._size ) {} std::string getName () const { return _name; } int getSize () const { return _size; } private: std::string _name; int _size;};
Nu kan vår array ha ett namn och en storlek, så vi kanske måste ändra definitionen av ArrayWrapper så här:
class ArrayWrapper{public: // default constructor produces a moderately sized array ArrayWrapper () : _p_vals( new int ) , _metadata( 64, "ArrayWrapper" ) {} ArrayWrapper (int n) : _p_vals( new int ) , _metadata( n, "ArrayWrapper" ) {} // move constructor ArrayWrapper (ArrayWrapper&& other) : _p_vals( other._p_vals ) , _metadata( other._metadata ) { other._p_vals = NULL; } // copy constructor ArrayWrapper (const ArrayWrapper& other) : _p_vals( new int ) , _metadata( other._metadata ) { for ( int i = 0; i < _metadata.getSize(); ++i ) { _p_vals = other._p_vals; } } ~ArrayWrapper () { delete _p_vals; }private: int *_p_vals; MetaData _metadata;};
Fungerar detta? Det verkar väldigt naturligt, eller hur, att bara anropa MetaDatamove-konstruktören från flyttkonstruktören för ArrayWrapper? Problemet är att detta helt enkelt inte fungerar. Anledningen är enkel: värdet av other imove-konstruktören – det är en r-värde-referens. Men en rvalue-referens är i själva verket inte ett rvalue. Det är ett l-värde, och därför anropas copy-konstruktören, inte move-konstruktören. Detta är konstigt. Jag vet – det är förvirrande. Här är ett sätt att tänka på det. Ett rvalue är ett uttryck som skapar ett objekt som är på väg att avdunsta i luften. Det är på sina sista ben i livet – eller på väg att uppfylla sitt livs syfte. Plötsligt skickar vi det tillfälliga till en move-konstruktör, och det får ett nytt liv i det nya scope. I den kontext där rvalue-uttrycket utvärderades är det tillfälliga objektet verkligen slut. Men i vår konstruktör har objektet ett namn; det kommer att leva under hela funktionens varaktighet. med andra ord kan vi använda variabeln other mer än en gång i funktionen, och det tillfälliga objektet har en definierad plats som verkligen består under hela funktionen. Det är ett lvärde i ordets rätta bemärkelse, vi kan lokalisera objektet på en viss adress som är stabil under hela funktionsanropets varaktighet. Vi kanske faktiskt vill använda den senare i funktionen. Om en move-konstruktör anropas varje gång vi har ett objekt i en rvaluereferens kan vi av misstag använda ett flyttat objekt!
// move constructor ArrayWrapper (ArrayWrapper&& other) : _p_vals( other._p_vals ) , _metadata( other._metadata ) { // if _metadata( other._metadata ) calls the move constructor, using // other._metadata here would be extremely dangerous! other._p_vals = NULL; }
En sista sak: både lvalue- och rvalue-referenser är lvalue-uttryck. Skillnaden är att en lvalue-referens måste vara const för att hålla en referens till ett r-värde, medan en rvalue-referens alltid kan hålla en referens till ett r-värde.Det är som skillnaden mellan en pekare och det som pekas på. Det som det pekas på kom från ett rvalue, men när vi använder själva rvalue-referensen resulterar det i ett lvalue.
std::move
Så vad är tricket för att hantera det här fallet? Vi måste använda std::move, från<utility>–std::move är ett sätt att säga, ”ok, ärligt talat så vet jag att jag har ett l-värde, men jag vill att det ska vara ett r-värde.” std::move flyttar inte i sig självt något; det förvandlar bara ett l-värde till ett r-värde, så att du kan anropa move-konstruktören. Vår kod borde se ut så här:
#include <utility> // for std::move // move constructor ArrayWrapper (ArrayWrapper&& other) : _p_vals( other._p_vals ) , _metadata( std::move( other._metadata ) ) { other._p_vals = NULL; }
Och naturligtvis borde vi verkligen gå tillbaka till MetaData och fixa dess egen move-konstruktör så att den använder std::move på strängen som den innehåller:
MetaData (MetaData&& other) : _name( std::move( other._name ) ) // oh, blissful efficiency : _size( other._size ) {}
Move assignment operator
Såväl som vi har en move-konstruktör, borde vi också ha en move assignment operator. Du kan enkelt skriva en sådan genom att använda samma tekniker som för att skapa en flyttkonstruktör.
Flyttkonstruktörer och implicit genererade konstruktörer
Som du vet kommer kompilatorn i C++, när du deklarerar någon konstruktör, inte längre att generera standardkonstruktören åt dig. Samma sak gäller här: om du lägger till en flyttkonstruktör till en klass måste du deklarera och definiera din egen standardkonstruktör. Å andra sidan hindrar inte deklarationen av en move-konstruktör kompilatorn från att tillhandahålla en implicit genererad kopieringskonstruktör, och deklarationen av en move-tilldelningsoperator hindrar inte skapandet av en standardtilldelningsoperator.
Hur fungerar std::move
Du kanske undrar, hur skriver man en funktion som std::move? Hur får man denna magiska egenskap att omvandla ett lvärde till en referens till ettrvärde? Svaret är, som du kanske kan gissa, typecasting. Den faktiska deklarationen för std::move är något mer komplicerad, men i grund och botten är det bara en static_cast till en r-värdereferens. Detta innebär faktiskt att du egentligen inte behöver använda move – men det borde du göra, eftersom det är mycket tydligare vad du menar. Att en cast krävs är förresten en mycket bra sak! Det betyder att du inte av misstag kan omvandla ett l-värde till ett r-värde, vilket skulle vara farligt eftersom det skulle kunna möjliggöra en oavsiktlig förflyttning. Du måste uttryckligen använda std::move (eller en cast) för att konvertera ett l-värde till en r-värdereferens, och en r-värdereferens kommer aldrig att binda till ett l-värde på egen hand.
Returnering av en explicit r-värdereferens från en funktion
Är det någonsin tillfällen då du bör skriva en funktion som returnerar en r-värdereferens? Vad innebär det egentligen att returnera en rvalue-referens? Är inte funktioner som returnerar objekt efter värde redan rvalues?
Låt oss svara på den andra frågan först: att returnera en explicit rvalue-referens är något annat än att returnera ett objekt efter värde. Ta följande enkla exempel:
int x;int getInt (){ return x;}int && getRvalueInt (){ // notice that it's fine to move a primitive type--remember, std::move is just a cast return std::move( x );}
Det är tydligt att det i det första fallet, trots att getInt() är ett r-värde, görs en kopia av variabeln x. Vi kan till och med se detta genom att skriva en liten hjälpfunktion:
void printAddress (const int& v) // const ref to allow binding to rvalues{ cout << reinterpret_cast<const void*>( & v ) << endl;}printAddress( getInt() ); printAddress( x );
När du kör det här programmet ser du att det finns två separata värden som skrivs ut.
Å andra sidan skriver
printAddress( getRvalueInt() ); printAddress( x );
ut samma värde eftersom vi uttryckligen returnerar ett r-värde här.
Att returnera en r-värdesreferens är alltså en annan sak än att inte returnera en r-värdesreferens, men denna skillnad visar sig tydligast om du har ett redan existerande objekt som du returnerar i stället för ett temporärt objekt som skapas i funktionen (där kompilatorn troligen eliminerar kopian åt dig).
Nu kommer vi till frågan om du vill göra detta. Svaret är:förmodligen inte. I de flesta fall gör det bara att det blir mer sannolikt att du hamnar med en hängande referens (ett fall där referensen existerar, men det tillfälligaobjekt som den hänvisar till har förstörts). Problemet är ganska likt faran med att returnera en lvalue-referens – det objekt som refereras till kanske inte längre existerar. R-värdereferenser kan inte magiskt hålla ett objekt vid liv åt dig.Att returnera en r-värdereferens skulle främst vara meningsfullt i mycket sällsynta fall där du har en medlemsfunktion och behöver returnera resultatet av att kallastd::move på ett fält i klassen från den funktionen – och hur ofta kommer du att göra det?
Förflyttningssemantik och standardbiblioteket
Vi återgår till vårt ursprungliga exempel – vi använde en vektor, och vi har ingen kontroll över vektorklassen och om den har en move-konstruktör move-tilldelningsoperatör eller inte. Lyckligtvis är standardiseringskommittén klok, ochmove-semantik har lagts till i standardbiblioteket. Detta innebär att du nu effektivt kan returnera vektorer, kartor, strängar och vilka andra standardbiblioteksobjekt du vill, och dra full nytta av move-semantiken.
Flyttbara objekt i STL-containrar
Standardbiblioteket går till och med ett steg längre. Om du aktiverar movesemantik i dina egna objekt genom att skapa move-tilldelningsoperatörer ochmove-konstruktörer, när du lagrar dessa objekt i en container, kommer STL automatiskt att använda std::move och automatiskt dra nytta av move-aktiverade klasser för att eliminera ineffektiva kopior.