Het artikel bespreekt hoe onnodige kopieeroperaties in C++ kunnen worden vermeden om betere prestaties te behalen. De kernpunten zijn:
- Return Value Optimization (RVO): Sinds C++17 is copy elision gegarandeerd in bepaalde gevallen, waarbij objecten direct op de aanroepzijde worden gecreëerd in plaats van op de stack van de functie.
- Named Return Value Optimization (NRVO): Een vergelijkbare optimalisatie voor lokale objecten met een naam.
- Impliciete verplaatsingen: In veel scenario's, zoals bij het retourneren van parameters, voert de taal automatisch een verplaatsing uit.
- C++23: De auteur merkt op dat met C++23 zelfs complexe gevallen met rvalue-referenties nu impliciet worden verplaatst, waardoor het handmatig gebruiken van
std::move nog minder noodzakelijk is.
Verplaatsen in C++ zonder std::move
Return value optimization (RVO)
Een van de grootste vijanden van prestaties zijn onnodige kopieeroperaties.
Je bent waarschijnlijk bekend met return value optimization (RVO). Dit is waar je, indien mogelijk, altijd naar moet streven. RVO houdt in dat het geretourneerde object niet op de stack binnen de functie wordt gemaakt, maar aan de aanroepzijde, waar de waarde uiteindelijk terechtkomt. Dit bespaart zowel kopieer- als verplaatsacties. De standaard verwijst hiernaar als copy elision, aangezien de standaard nooit spreekt over compiler-optimalisaties.
Sinds C++17 krijg je gegarandeerde copy elision in het volgende geval:
Apple RVO()
{
return {};
}
Dit is pure RVO. Daarnaast is er named return value optimization (NRVO):
Apple NRVO()
{
Apple res{};
return res;
}
Dat laatste is niet onderhevig aan gegarandeerde copy elision. Waarschijnlijk betaal je hier echter ook niet voor een kopieer- of verplaatsactie.
Verplaatsen in plaats van kopiëren
Het volgende beste alternatief na copy elision is het verplaatsen (moving) van een object. Sinds C++11 zijn er in de taal een aantal plaatsen waar impliciete verplaatsingen plaatsvinden:
Apple Fun(Apple val)
{
return val;
}
In deze code wordt het resulterende object verplaatst vanuit de parameter.
Er zijn echter gevallen in de taal die iets ingewikkelder liggen. Het eerste voorbeeld is het volgende:
Apple Cat(Apple&& val)
{
return val;
}
Je hebt hier een functie die een rvalue-referentie als parameter neemt en het zojuist ontvangen object retourneert. Hoewel deze code compileert, krijg je vóór C++20 een kopieerconstructie van de retourwaarde (bij een conforme compiler zoals GCC). De minder conforme compiler Clang geeft je een verplaatsconstructie. Soms kan het dus beter om je niet strikt aan het boek te houden.
Het andere voorbeeld, dat ik minder prettig vind, is het volgende:
Apple&& Cat(Apple&& val)
{
return val;
}
Je hebt hier opnieuw een functie die een rvalue-referentie als parameter neemt, maar ditmaal wordt er een rvalue-referentie geretourneerd. In dit geval zou de code niet compileren zonder de retourwaarde handmatig te verplaatsen, wat ingaat tegen mijn advies.
Om eerlijk te zijn is in beide gevallen het verplaatsen van de retourwaarde vereist om het beste resultaat te behalen. Beide gevallen gaan dus in tegen mijn advies.
Zodra je echter overschakelt naar de C++23-modus, voeren beide gevallen een impliciete verplaatsing uit. Er is geen std::move vereist. Je kunt mijn regel om std::move zelden te gebruiken nu dus nog strikter volgen!
Verplaatsen in C++ zonder std::move
Return value optimization (RVO)
Een van de grootste vijanden van prestaties zijn onnodige kopieeroperaties.
Je bent waarschijnlijk bekend met return value optimization (RVO). Dit is waar je, indien mogelijk, altijd naar moet streven. RVO houdt in dat het geretourneerde object niet op de stack binnen de functie wordt gemaakt, maar aan de aanroepzijde, waar de waarde uiteindelijk terechtkomt. Dit bespaart zowel kopieer- als verplaatsacties. De standaard verwijst hiernaar als copy elision, aangezien de standaard nooit spreekt over compiler-optimalisaties.
Sinds C++17 krijg je gegarandeerde copy elision in het volgende geval:
Apple RVO()
{
return {};
}
Dit is pure RVO. Daarnaast is er named return value optimization (NRVO):
Apple NRVO()
{
Apple res{};
return res;
}
Dat laatste is niet onderhevig aan gegarandeerde copy elision. Waarschijnlijk betaal je hier echter ook niet voor een kopieer- of verplaatsactie.
Verplaatsen in plaats van kopiëren
Het volgende beste alternatief na copy elision is het verplaatsen (moving) van een object. Sinds C++11 zijn er in de taal een aantal plaatsen waar impliciete verplaatsingen plaatsvinden:
Apple Fun(Apple val)
{
return val;
}
In deze code wordt het resulterende object verplaatst vanuit de parameter.
Er zijn echter gevallen in de taal die iets ingewikkelder liggen. Het eerste voorbeeld is het volgende:
Apple Cat(Apple&& val)
{
return val;
}
Je hebt hier een functie die een rvalue-referentie als parameter neemt en het zojuist ontvangen object retourneert. Hoewel deze code compileert, krijg je vóór C++20 een kopieerconstructie van de retourwaarde (bij een conforme compiler zoals GCC). De minder conforme compiler Clang geeft je een verplaatsconstructie. Soms kan het dus beter om je niet strikt aan het boek te houden.
Het andere voorbeeld, dat ik minder prettig vind, is het volgende:
Apple&& Cat(Apple&& val)
{
return val;
}
Je hebt hier opnieuw een functie die een rvalue-referentie als parameter neemt, maar ditmaal wordt er een rvalue-referentie geretourneerd. In dit geval zou de code niet compileren zonder de retourwaarde handmatig te verplaatsen, wat ingaat tegen mijn advies.
Om eerlijk te zijn is in beide gevallen het verplaatsen van de retourwaarde vereist om het beste resultaat te behalen. Beide gevallen gaan dus in tegen mijn advies.
Zodra je echter overschakelt naar de C++23-modus, voeren beide gevallen een impliciete verplaatsing uit. Er is geen std::move vereist. Je kunt mijn regel om std::move zelden te gebruiken nu dus nog strikter volgen!