Wanneer moet std::move worden gebruikt voor een functieretourwaarde?

In dit geval

struct Foo {};
Foo meh() {
  return std::move(Foo());
}

Ik ben er vrij zeker van dat de verplaatsing niet nodig is, omdat de nieuw gemaakte Fooeen x-waarde zal zijn.

Maar wat in dit soort gevallen?

struct Foo {};
Foo meh() {
  Foo foo;
  //do something, but knowing that foo can safely be disposed of
  //but does the compiler necessarily know it?
  //we may have references/pointers to foo. how could the compiler know?
  return std::move(foo); //so here the move is needed, right?
}

Daar is de verhuizing nodig, neem ik aan?


Antwoord 1, autoriteit 100%

In het geval van return std::move(foo);is de moveoverbodig vanwege 12.8/32:

Wanneer aan de criteria voor het weglaten van een kopieerbewerking wordt voldaan of zou zijn
met uitzondering van het feit dat het bronobject een functieparameter is,
en het te kopiëren object wordt aangeduid met een lvalue, overload
resolutie om de constructor voor de kopie te selecteren wordt eerst uitgevoerd als
als het object werd aangeduid met een rwaarde.

return foo;is een geval van NRVO, dus kopiëren is toegestaan. Foois een waarde. Dus de constructor die is geselecteerd voor de “kopie” van Foonaar de geretourneerde waarde van mehmoet de move-constructor zijn als die bestaat.

Het toevoegen van moveheeft echter wel een potentieel effect: het voorkomt dat de zet wordt weggelaten, omdat return std::move(foo);nietkomt in aanmerking voor NRVO.

Voor zover ik weet, bevat 12.8/32 de enigevoorwaarden waaronder een kopie van een lvalue kan worden vervangen door een zet. Het is de compiler in het algemeen niet toegestaan ​​om te detecteren dat een l-waarde ongebruikt is na de kopie (bijvoorbeeld met behulp van DFA), en de wijziging op eigen initiatief aan te brengen. Ik neem hier aan dat er een waarneembaar verschil is tussen de twee — als het waarneembare gedrag hetzelfde is, dan is de “als-als”-regel van toepassing.

Dus, om de vraag in de titel te beantwoorden, gebruik std::moveop een retourwaarde wanneer u wilt dat deze wordt verplaatst en deze toch niet wordt verplaatst. Dat is:

  • je wilt dat het wordt verplaatst, en
  • het is een waarde, en
  • het komt niet in aanmerking voor kopieerelisie, en
  • het is niet de naam van een functieparameter op basis van waarde.

Aangezien dit nogal onhandig is en zetten meestalgoedkoop zijn, zou je kunnen zeggen dat je dit in niet-sjablooncode kunt vereenvoudigen. Gebruik std::movewanneer:

  • je wilt dat het wordt verplaatst, en
  • het is een waarde, en
  • je kunt je er geen zorgen over maken.

Door de vereenvoudigde regels te volgen, offer je wat bewegingselisie op. Voor typen zoals std::vectordie goedkoop te verplaatsen zijn, zul je het waarschijnlijk nooit merken (en als je het wel merkt, kun je optimaliseren). Voor typen zoals std::arraydie duur zijn om te verplaatsen, of voor sjablonen waarvan u geen idee heeft of verhuizingen goedkoop zijn of niet, is de kans groter dat u zich er zorgen over maakt.


Antwoord 2, autoriteit 27%

De verplaatsing is in beide gevallen niet nodig. In het tweede geval is std::moveoverbodig omdat je een lokale variabele op waarde retourneert, en de compiler zal begrijpen dat, aangezien je die lokale variabele niet meer gaat gebruiken, het verplaatst van in plaats van gekopieerd.


Antwoord 3, autoriteit 20%

Als bij een retourwaarde de retourexpressie rechtstreeks verwijst naar de naam van een lokale lvalue (d.w.z. op dit punt een xwaarde), is de std::moveniet nodig. Aan de andere kant, als de return-expressie nietde identifier is, wordt deze niet automatisch verplaatst, dus je zou bijvoorbeeld de expliciete std::movehierin nodig hebben geval:

T foo(bool which) {
   T a = ..., b = ...;
   return std::move(which? a : b);
   // alternatively: return which? std::move(a), std::move(b);
}

Als je een benoemde lokale variabele of een tijdelijke expressie rechtstreeks retourneert, moet je de expliciete std::movevermijden. De compiler moet(en zal in de toekomst) in die gevallen automatisch worden verplaatst, en het toevoegen van std::movekan andere optimalisaties beïnvloeden.


Antwoord 4, autoriteit 15%

Er zijn veel antwoorden over wanneer het niet moet worden verplaatst, maar de vraag is “wanneer moet het worden verplaatst?”

Hier is een gekunsteld voorbeeld van wanneer het moet worden gebruikt:

std::vector<int> append(std::vector<int>&& v, int x) {
  v.push_back(x);
  return std::move(v);
}

dat wil zeggen, als je een functie hebt die een rvalue-referentie nodig heeft, deze wijzigt en er vervolgens een kopie van teruggeeft. (In c++20gedrag verandert hier) In de praktijk is dit ontwerp bijna altijd beter:

std::vector<int> append(std::vector<int> v, int x) {
  v.push_back(x);
  return v;
}

waarmee u ook niet-rvalue parameters kunt gebruiken.

Kortom, als je een rvalue-referentie hebt binnen een functie die je wilt retourneren door te verplaatsen, moet je std::moveaanroepen. Als je een lokale variabele hebt (of het nu een parameter is of niet), retourneer je deze impliciet moves (en deze impliciete zet kan worden weggelaten, terwijl een expliciete zet dat niet kan). Als je een functie of bewerking hebt die lokale variabelen nodig heeft en een verwijzing naar die lokale variabele retourneert, moet je std::movegebruiken om de verplaatsing te laten plaatsvinden (bijvoorbeeld de trinaire ?:operator).


Antwoord 5

Een C++-compiler is gratis te gebruiken std::move(foo):

  • als bekend is dat Fooaan het einde van zijn levensduur is, en
  • het impliciete gebruik van std::moveheeft geen enkel effect op de semantiek van de C++-code, behalve de semantische effecten die zijn toegestaan ​​door de C++-specificatie.

Het hangt af van de optimalisatiemogelijkheden van de C++-compiler of deze in staat is om te berekenen welke transformaties van f(foo); foo.~Foo();naar f(std::move(foo)); foo.~Foo();zijn winstgevend in termen van prestaties of geheugenverbruik, terwijl ze zich houden aan de C++-specificatieregels.


Conceptueelgesproken, zijn C++-compilers voor het jaar 2017, zoals GCC 6.3.0, in staat om deze code te optimaliseren:

Foo meh() {
    Foo foo(args);
    foo.method(xyz);
    bar();
    return foo;
}

in deze code:

void meh(Foo *retval) {
   new (retval) Foo(arg);
   retval->method(xyz);
   bar();
}

die het aanroepen van de copy-constructor en de destructor van Foovermijdt.


C++-compilers van het jaar 2017, zoals GCC 6.3.0, kunnen niet in staat zijn omdeze codes te optimaliseren:

Foo meh_value() {
    Foo foo(args);
    Foo retval(foo);
    return retval;
}
Foo meh_pointer() {
    Foo *foo = get_foo();
    Foo retval(*foo);
    delete foo;
    return retval;
}

in deze codes:

Foo meh_value() {
    Foo foo(args);
    Foo retval(std::move(foo));
    return retval;
}
Foo meh_pointer() {
    Foo *foo = get_foo();
    Foo retval(std::move(*foo));
    delete foo;
    return retval;
}

wat betekent dat een programmeur voor het jaar 2017 dergelijke optimalisaties expliciet moet specificeren.

Other episodes