C++
Regeln om tre, fem och noll
Sök…
Regel om fem
C ++ 11 introducerar två nya specialmedlemfunktioner: flyttkonstruktören och drifttilldelningsoperatören. Av alla samma skäl som du vill följa regeln om tre i C ++ 03, vill du vanligtvis följa regeln för fem i C ++ 11: Om en klass kräver EN av fem specialmedlemfunktioner, och om flytta semantik är önskvärda, då kräver det troligen ALLA FEM av dem.
Observera dock att om du inte följer regeln av fem vanligtvis inte betraktas som ett fel, men en missad optimeringsmöjlighet, så länge tre regeln fortfarande följs. Om ingen flyttkonstruktör eller flyttilldelningsoperatör är tillgänglig när kompilatorn normalt skulle använda en, kommer den istället att använda kopisemantik om möjligt, vilket resulterar i en mindre effektiv operation på grund av onödiga kopieringsoperationer. Om flyttande semantik inte önskas för en klass, har det inget behov att förklara en flyttkonstruktör eller uppdragsoperatör.
Samma exempel som för regeln om tre:
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);
}
};
Alternativt kan både kopierings- och flyttningsuppdragsoperatören ersättas med en enda tilldelningsoperatör, som tar en instans med ett värde istället för referens eller rvalue-referens för att underlätta användning av kopierings- och swap-formen.
Person& operator=(Person copy)
{
swap(*this, copy);
return *this;
}
Att utvidga från regeln tre till regeln fem är viktigt av prestandaskäl, men är inte strikt nödvändigt i de flesta fall. Att lägga till kopieringskonstruktören och tilldelningsoperatören säkerställer att flyttningen av typen inte läcker minne (flyttkonstruktion faller helt enkelt tillbaka till kopiering i så fall), men kommer att utföra kopior som den som ringde antagligen inte förutsåg.
Nollregeln
Vi kan kombinera principerna för Rule of Five och RAII för att få ett mycket smalare gränssnitt: Rule of Zero: alla resurser som behöver hanteras bör vara i sin egen typ. Den typen måste följa regeln för fem, men alla användare av den resursen behöver inte skriva någon av de fem specialmedlemfunktionerna och kan helt enkelt default
alla.
Med hjälp av klassen Person
introducerad i exemplet Rule of Three kan vi skapa ett resurshanterande objekt för 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 */
};
Och när detta är separat blir vår Person
mycket enklare:
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 Person
specialmedlemmarna behöver inte ens deklareras uttryckligen; kompilatorn kommer att standardera eller radera dem på lämpligt sätt, baserat på innehållet i Person
. Därför är följande också ett exempel på regeln om noll.
struct Person {
cstring name;
int arg;
};
Om cstring
skulle vara en rörelse-typ, med en delete
kopia-konstruktör / uppdragsoperatör, skulle Person
automatiskt också vara rörlig.
Begreppet noll infördes av R. Martinho Fernandes
Reguladetri
Regel om tre säger att om en typ någonsin behöver ha en användardefinierad kopieringskonstruktör, kopieringsuppdragsoperatör eller destruktor, måste den ha alla tre .
Anledningen till regeln är att en klass som behöver någon av de tre hanterar en resurs (filhandtag, dynamiskt tilldelat minne osv.), Och alla tre behövs för att hantera resursen konsekvent. Kopieringsfunktionerna handlar om hur resursen kopieras mellan objekt och förstöraren skulle förstöra resursen i enlighet med RAII-principerna .
Tänk på en typ som hanterar en strängresurs:
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;
}
};
Eftersom name
tilldelades i konstruktören omdelar förstöraren det för att undvika läckande minne. Men vad händer om ett sådant objekt kopieras?
int main()
{
Person p1("foo", 11);
Person p2 = p1;
}
Först kommer p1
att konstrueras. Sedan p2
från p1
. Emellertid kommer den C ++ - genererade kopieringskonstruktören att kopiera varje komponent av typen som den är. Vilket innebär att p1.name
och p2.name
båda pekar på samma sträng.
När main
slutar kommer destruktorer att kallas. Första p2
: s förstörare kommer att kallas; det kommer att radera strängen. Sedan kommer p1
: s förstörare att kallas. Strängen är dock redan raderad . Att delete
minnet som redan har tagits bort ger odefinierat beteende.
För att undvika detta är det nödvändigt att tillhandahålla en lämplig kopieringskonstruktör. En metod är att implementera ett referensräknat system, där olika Person
delar samma strängdata. Varje gång en kopia utförs ökas det delade referensräkningen. Destruktorn minskar sedan referensräkningen och släpper endast minnet om räkningen är noll.
Eller så kan vi implementera värdesemantik och djup kopieringsbeteende :
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;
}
Implementering av operatören för kopieringsuppdrag kompliceras av behovet att släppa en befintlig buffert. Kopierings- och byttekniken skapar ett tillfälligt objekt som innehåller en ny buffert. Att byta innehållet i *this
och copy
ger äganderätt till copy
av den ursprungliga bufferten. Förstörelse av copy
, när funktionen återgår, släpper bufferten som tidigare ägs av *this
.
Självuppdragsskydd
När man skriver en kopieringsoperatör är det mycket viktigt att den kan arbeta vid självuppdrag. Det vill säga det måste tillåta detta:
SomeType t = ...;
t = t;
Självtilldelning sker vanligtvis inte på ett så uppenbart sätt. Det händer oftast via en omväg genom olika kodsystem, där platsen för uppdraget har helt enkelt två Person
pekare eller referenser och har ingen aning om att de är samma objekt.
Alla operatörer för kopiering som du skriver måste kunna ta hänsyn till detta.
Det typiska sättet att göra det är att radera all uppdragslogik i ett tillstånd som detta:
SomeType &operator=(const SomeType &other)
{
if(this != &other)
{
//Do assignment logic.
}
return *this;
}
Obs: Det är viktigt att tänka på självtilldelning och se till att din kod fungerar korrekt när den händer. Men självtilldelning är en mycket sällsynt händelse och optimering för att förhindra att det faktiskt kan pessimisera det normala fallet. Eftersom det normala fallet är mycket vanligare kan det att pessimisera för självtilldelning mycket väl minska din kodeffektivitet (så var försiktig med att använda den).
Som ett exempel är den normala tekniken för att implementera uppdragsoperatören copy and swap idiom
. Den normala implementeringen av denna teknik bryr sig inte att testa för självuppdrag (även om självuppdraget är dyrt eftersom en kopia görs). Anledningen är att pessimisering av det normala fallet har visat sig vara mycket dyrare (eftersom det händer oftare).
Flytta tilldelningsoperatörer måste också skyddas mot självtilldelning. Emellertid är logiken för många sådana operatörer baserad på std::swap
, som kan hantera byte från / till samma minne helt fint. Så om din rörelsetilldelningslogik inte är mer än en serie byteoperationer, behöver du inte självtilldelningsskydd.
Om detta inte är fallet måste du vidta liknande åtgärder som ovan.