C++
De regel van drie, vijf en nul
Zoeken…
Regel van vijf
C ++ 11 introduceert twee nieuwe speciale lidfuncties: de constructor van de beweging en de operator voor toewijzing van bewegingen. Om dezelfde redenen dat u de Regel van Drie in C ++ 03 wilt volgen, wilt u meestal de Regel van Vijf in C ++ 11 volgen: Als een klasse EEN van vijf speciale lidfuncties vereist, en als semantiek verplaatsen gewenst zijn, dan vereist het hoogstwaarschijnlijk ALLE VIJF van hen.
Merk echter op dat het niet volgen van de Regel van Vijf meestal niet als een fout wordt beschouwd, maar als een gemiste optimalisatiemogelijkheid, zolang de Regel van Drie nog steeds wordt gevolgd. Als er geen verplaatsingsconstructor of verplaatsingsopdrachtoperator beschikbaar is terwijl de compiler er normaal gesproken een gebruikt, gebruikt hij in plaats daarvan indien mogelijk kopie-semantiek, wat resulteert in een minder efficiënte bewerking vanwege onnodige kopieerbewerkingen. Als semantiek van verplaatsingen niet gewenst is voor een klasse, is het niet nodig om een constructor van bewegingen of een operator voor de toewijzing aan te geven.
Hetzelfde voorbeeld als voor de Regel van Drie:
class Person
{
char* name;
int age;
public:
// Destructor
~Person() { delete [] name; }
// Implement Copy Semantics
Person(Person const& other)
: name(new char[std::strlen(other.name) + 1])
, age(other.age)
{
std::strcpy(name, other.name);
}
Person &operator=(Person const& other)
{
// Use copy and swap idiom to implement assignment.
Person copy(other);
swap(*this, copy);
return *this;
}
// Implement Move Semantics
// Note: It is usually best to mark move operators as noexcept
// This allows certain optimizations in the standard library
// when the class is used in a container.
Person(Person&& that) noexcept
: name(nullptr) // Set the state so we know it is undefined
, age(0)
{
swap(*this, that);
}
Person& operator=(Person&& that) noexcept
{
swap(*this, that);
return *this;
}
friend void swap(Person& lhs, Person& rhs) noexcept
{
std::swap(lhs.name, rhs.name);
std::swap(lhs.age, rhs.age);
}
};
Als alternatief kunnen zowel de kopieer- als de verplaatsingsoperator worden vervangen door een enkele toewijzingsoperator, die een instantie op waarde neemt in plaats van op referentie of rvalue-referentie om het gebruik van het copy-and-swap-idioom te vergemakkelijken.
Person& operator=(Person copy)
{
swap(*this, copy);
return *this;
}
Uitbreiding van de Regel van Drie naar de Regel van Vijf is belangrijk om prestatieredenen, maar is in de meeste gevallen niet strikt noodzakelijk. Het toevoegen van de kopieerconstructor en de toewijzingsoperator zorgt ervoor dat het verplaatsen van het type geen geheugen lekt (verplaatsen construeren zal in dat geval gewoon terugvallen op kopiëren), maar kopieën uitvoeren die de beller waarschijnlijk niet had verwacht.
Regel van nul
We kunnen de principes van de Rule of Five en RAII combineren om een veel slankere interface te krijgen: de Rule of Zero: elke resource die moet worden beheerd, moet een eigen type hebben. Dat type zou hebben om de regel van Vijf volgen, maar alle gebruikers van die bron niet nodig om een van de vijf speciale lid functies te schrijven en kan gewoon default
allemaal.
Met behulp van de klasse Person
geïntroduceerd in het voorbeeld van Regel drie kunnen we een resource-managementobject maken voor cstrings
:
class cstring {
private:
char* p;
public:
~cstring() { delete [] p; }
cstring(cstring const& );
cstring(cstring&& );
cstring& operator=(cstring const& );
cstring& operator=(cstring&& );
/* other members as appropriate */
};
En zodra dit gescheiden is, wordt onze Person
klasse veel eenvoudiger:
class Person {
cstring name;
int arg;
public:
~Person() = default;
Person(Person const& ) = default;
Person(Person&& ) = default;
Person& operator=(Person const& ) = default;
Person& operator=(Person&& ) = default;
/* other members as appropriate */
};
De speciale leden in Person
hoeven niet eens expliciet te worden aangegeven; de compiler zal deze standaard instellen of verwijderen, op basis van de inhoud van Person
. Daarom is het volgende ook een voorbeeld van de nulregel.
struct Person {
cstring name;
int arg;
};
Als cstring
een alleen-verplaatsbaar type zou zijn, met een delete
kopie-constructor / toewijzingsoperator, zou Person
ook automatisch alleen verplaatsen zijn.
De term nulregel werd geïntroduceerd door R. Martinho Fernandes
Regel van drie
De Regel van Drie stelt dat als een type ooit een door de gebruiker gedefinieerde kopie-constructor, kopie-toewijzingsoperator of destructor moet hebben, het alle drie moet hebben.
De reden voor de regel is dat een klasse die een van de drie nodig heeft, een bepaalde bron beheert (filehandles, dynamisch toegewezen geheugen, enz.), En alle drie zijn nodig om die bron consistent te beheren. De kopieerfuncties behandelen hoe de bron wordt gekopieerd tussen objecten, en de vernietiger zou de bron vernietigen, in overeenstemming met RAII-principes .
Overweeg een type dat een stringresource beheert:
class Person
{
char* name;
int age;
public:
Person(char const* new_name, int new_age)
: name(new char[std::strlen(new_name) + 1])
, age(new_age)
{
std::strcpy(name, new_name);
}
~Person() {
delete [] name;
}
};
Omdat de name
in de constructor is toegewezen, maakt de destructor de toewijzing ongedaan om te voorkomen dat geheugen lekt. Maar wat gebeurt er als een dergelijk object wordt gekopieerd?
int main()
{
Person p1("foo", 11);
Person p2 = p1;
}
Eerst wordt p1
geconstrueerd. Vervolgens wordt p2
gekopieerd van p1
. De door C ++ gegenereerde kopieerconstructie kopieert echter elke component van het type zoals het is. Wat betekent dat p1.name
en p2.name
beide naar dezelfde string wijzen.
Wanneer main
einde eindigt, worden destructors opgeroepen. De destructor van eerste p2
zal worden genoemd; het zal de string verwijderen. Dan zal de destructor van p1
worden genoemd. De tekenreeks is echter al verwijderd . Het aanroepen van delete
in het geheugen dat al is verwijderd, levert ongedefinieerd gedrag op.
Om dit te voorkomen, is het noodzakelijk om een geschikte kopie-constructor te verschaffen. Een benadering is om een systeem met referentietelling te implementeren, waarbij verschillende Person
instanties dezelfde stringgegevens delen. Telkens wanneer een kopie wordt uitgevoerd, wordt de gedeelde referentietelling verhoogd. De destructor verlaagt dan de referentietelling, waarbij het geheugen alleen wordt vrijgegeven als de telling nul is.
Of we kunnen waarde-semantiek en diepkopieergedrag implementeren:
Person(Person const& other)
: name(new char[std::strlen(other.name) + 1])
, age(other.age)
{
std::strcpy(name, other.name);
}
Person &operator=(Person const& other)
{
// Use copy and swap idiom to implement assignment
Person copy(other);
swap(copy); // assume swap() exchanges contents of *this and copy
return *this;
}
De implementatie van de operator voor het toewijzen van kopieën wordt gecompliceerd door de noodzaak om een bestaande buffer vrij te geven. De kopieer- en wisseltechniek maakt een tijdelijk object met een nieuwe buffer. Het omwisselen van de inhoud van *this
and copy
geeft eigendom van de copy
van de originele buffer. Vernietiging van copy
, wanneer de functie terugkeert, geeft de buffer vrij die eerder *this
eigendom was.
Bescherming tegen zelf-toewijzing
Bij het schrijven van een operator voor het toewijzen van kopieën is het erg belangrijk dat deze kan werken in geval van zelftoekenning. Dat wil zeggen, het moet dit toestaan:
SomeType t = ...;
t = t;
Zelfopdracht gebeurt meestal niet op zo'n voor de hand liggende manier. Het gebeurt meestal via een omcirkelde route door verschillende codesystemen, waarbij de locatie van de toewijzing eenvoudig twee Person
of referenties heeft en geen idee heeft dat ze hetzelfde object zijn.
Elke exploitant van een kopieopdracht die u schrijft, moet hiermee rekening kunnen houden.
De typische manier om dit te doen is om alle toewijzingslogica in een toestand als deze te verpakken:
SomeType &operator=(const SomeType &other)
{
if(this != &other)
{
//Do assignment logic.
}
return *this;
}
Opmerking: het is belangrijk om na te denken over zelftoekenning en ervoor te zorgen dat uw code zich correct gedraagt wanneer deze gebeurt. Zelftoewijzing is echter een zeer zeldzame gebeurtenis en optimalisatie om te voorkomen, kan het normale geval zelfs pessimiseren. Aangezien het normale geval veel gebruikelijker is, kan pessimisatie voor zelftoewijzing uw code-efficiëntie verminderen (dus wees voorzichtig met het gebruik).
De normale techniek voor het implementeren van de toewijzingsoperator is bijvoorbeeld het copy and swap idiom
. De normale implementatie van deze techniek neemt niet de moeite om te testen op zelftoekenning (hoewel zelftoekenning duur is omdat een kopie wordt gemaakt). De reden is dat pessimisatie van het normale geval veel duurder is gebleken (omdat dit vaker gebeurt).
Operators voor verplaatsingstoewijzingen moeten ook worden beschermd tegen zelftoekenning. De logica voor veel van dergelijke operatoren is echter gebaseerd op std::swap
, die prima kan omgaan met wisselen van / naar hetzelfde geheugen. Dus als uw logica voor verplaatsingstoewijzing niets meer is dan een reeks swap-bewerkingen, hebt u geen bescherming tegen zelftoekenning nodig.
Als dit niet het geval is, moet u soortgelijke maatregelen nemen als hierboven.