C++
Speciale ledenfuncties
Zoeken…
Virtuele en beschermde destructors
Een klasse die is ontworpen om te worden geërfd, wordt een basisklasse genoemd. Wees voorzichtig met de speciale ledenfuncties van een dergelijke klasse.
Een klasse die is ontworpen om tijdens de uitvoering polymorf te worden gebruikt (via een wijzer naar de basisklasse) moet de destructor virtual
verklaren. Hiermee kunnen de afgeleide delen van het object op de juiste manier worden vernietigd, zelfs wanneer het object via een aanwijzer naar de basisklasse wordt vernietigd.
class Base {
public:
virtual ~Base() = default;
private:
// data members etc.
};
class Derived : public Base { // models Is-A relationship
public:
// some methods
private:
// more data members
};
// virtual destructor in Base ensures that derived destructors
// are also called when the object is destroyed
std::unique_ptr<Base> base = std::make_unique<Derived>();
base = nullptr; // safe, doesn't leak Derived's members
Als de klasse niet polymorf hoeft te zijn, maar toch moet toestaan dat de interface wordt overgenomen, gebruik dan een niet-virtuele protected
destructor.
class NonPolymorphicBase {
public:
// some methods
protected:
~NonPolymorphicBase() = default; // note: non-virtual
private:
// etc.
};
Zo'n klasse kan nooit door een wijzer worden vernietigd, waardoor stille lekken door snijden worden vermeden.
Deze techniek is met name van toepassing op klassen die zijn ontworpen als private
basisklassen. Een dergelijke klasse kan worden gebruikt om enkele veelvoorkomende implementatiedetails in te kapselen, terwijl virtual
methoden worden geboden als aanpassingspunten. Dit soort klasse mag nooit polymorf worden gebruikt, en een protected
destructor helpt deze eis direct in de code te documenteren.
Ten slotte kunnen sommige klassen vereisen dat ze nooit als basisklasse worden gebruikt. In dit geval kan de klas als final
worden gemarkeerd. Een normale niet-virtuele openbare destructor is in dit geval prima.
class FinalClass final { // marked final here
public:
~FinalClass() = default;
private:
// etc.
};
Impliciete verplaatsing en kopie
Houd er rekening mee dat het declareren van een destructor de compiler verhindert impliciete verplaatsingsconstructors en verplaatsingstoewijzers te genereren. Als u een destructor aangeeft, moet u ook de juiste definities toevoegen voor de verplaatsingshandelingen.
Bovendien zal het melden van verplaatsingshandelingen het genereren van kopieerbewerkingen onderdrukken, dus deze moeten ook worden toegevoegd (als de objecten van deze klasse een kopie-semantiek moeten hebben).
class Movable {
public:
virtual ~Movable() noexcept = default;
// compiler won't generate these unless we tell it to
// because we declared a destructor
Movable(Movable&&) noexcept = default;
Movable& operator=(Movable&&) noexcept = default;
// declaring move operations will suppress generation
// of copy operations unless we explicitly re-enable them
Movable(const Movable&) = default;
Movable& operator=(const Movable&) = default;
};
Kopiëren en ruilen
Als u een klasse schrijft die bronnen beheert, moet u alle speciale lidfuncties implementeren (zie Regel van Drie / Vijf / Nul ). De meest directe benadering voor het schrijven van de copyconstructor en de toewijzingsoperator is:
person(const person &other)
: name(new char[std::strlen(other.name) + 1])
, age(other.age)
{
std::strcpy(name, other.name);
}
person& operator=(person const& rhs) {
if (this != &other) {
delete [] name;
name = new char[std::strlen(other.name) + 1];
std::strcpy(name, other.name);
age = other.age;
}
return *this;
}
Maar deze aanpak heeft enkele problemen. Het ontbreekt de sterke uitzondering garantie - als new[]
worpen, hebben we al het eigendom zijn van middelen vrijgemaakt this
en kan niet herstellen. We dupliceren veel van de logica van het maken van kopieën bij het toewijzen van kopieën. En we moeten de zelftoewijzingscontrole onthouden, die meestal alleen maar overhead toevoegt aan de kopieerbewerking, maar nog steeds van cruciaal belang is.
Om aan de sterke uitzonderingsgarantie te voldoen en codeduplicatie te voorkomen (dubbel dus met de operator voor de volgende verplaatsingstoewijzing), kunnen we het idioom kopiëren en uitwisselen gebruiken:
class person {
char* name;
int age;
public:
/* all the other functions ... */
friend void swap(person& lhs, person& rhs) {
using std::swap; // enable ADL
swap(lhs.name, rhs.name);
swap(lhs.age, rhs.age);
}
person& operator=(person rhs) {
swap(*this, rhs);
return *this;
}
};
Waarom werkt dit Bedenk wat er gebeurt als we dat hebben
person p1 = ...;
person p2 = ...;
p1 = p2;
Eerst kopiëren en construeren we rhs
van p2
(die we hier niet hoefden te dupliceren). Als die bewerking gooit, doen we niets in operator=
en blijft p1
onaangeroerd. Vervolgens wisselen we de leden tussen *this
en rhs
, en dan gaat rhs
buiten bereik. Wanneer operator=
, reinigt dit impliciet de oorspronkelijke bronnen this
(via de destructor, die we niet hoefden te dupliceren). Zelftoewijzing werkt ook - het is minder efficiënt met copy-and-swap (vereist een extra toewijzing en deallocatie), maar als dat het onwaarschijnlijke scenario is, vertragen we de typische use case niet om het te verklaren.
De bovenstaande formulering werkt zoals het al is voor toewijzing van verplaatsingen.
p1 = std::move(p2);
Hier, move-construct we rhs
uit p2
, en al de rest is net zo geldig is. Als een klasse verplaatsbaar is maar niet kan worden gekopieerd, hoeft de kopie-toewijzing niet te worden verwijderd, omdat deze toewijzingsoperator eenvoudigweg slecht wordt gevormd vanwege de verwijderde kopieermaker.
Standaard Constructor
Een standaardconstructor is een type constructor die geen parameters vereist wanneer deze wordt aangeroepen. Het is vernoemd naar het type dat het construeert en is een lidfunctie ervan (zoals alle constructors zijn).
class C{
int i;
public:
// the default constructor definition
C()
: i(0){ // member initializer list -- initialize i to 0
// constructor function body -- can do more complex things here
}
};
C c1; // calls default constructor of C to create object c1
C c2 = C(); // calls default constructor explicitly
C c3(); // ERROR: this intuitive version is not possible due to "most vexing parse"
C c4{}; // but in C++11 {} CAN be used in a similar way
C c5[2]; // calls default constructor for both array elements
C* c6 = new C[2]; // calls default constructor for both array elements
Een andere manier om aan de vereiste "geen parameters" te voldoen, is dat de ontwikkelaar standaardwaarden voor alle parameters levert:
class D{
int i;
int j;
public:
// also a default constructor (can be called with no parameters)
D( int i = 0, int j = 42 )
: i(i), j(j){
}
};
D d; // calls constructor of D with the provided default values for the parameters
Onder sommige omstandigheden (dat wil zeggen, de ontwikkelaar biedt geen constructors en er zijn geen andere diskwalificerende voorwaarden), biedt de compiler impliciet een lege standaardconstructor:
class C{
std::string s; // note: members need to be default constructible themselves
};
C c1; // will succeed -- C has an implicitly defined default constructor
Het hebben van een ander type constructor is een van de eerder genoemde diskwalificerende voorwaarden:
class C{
int i;
public:
C( int i ) : i(i){}
};
C c1; // Compile ERROR: C has no (implicitly defined) default constructor
Om impliciete standaardconstructorcreatie te voorkomen, is een veelgebruikte techniek om deze als private
aan te duiden (zonder definitie). De bedoeling is om een compileerfout te veroorzaken wanneer iemand de constructor probeert te gebruiken (dit resulteert in een Access to private- fout of een linkerfout, afhankelijk van de compiler).
Om er zeker van te zijn dat een standaardconstructor (functioneel vergelijkbaar met de impliciete) is gedefinieerd, kan een ontwikkelaar een lege expliciet schrijven.
In C ++ 11 kan een ontwikkelaar ook het sleutelwoord delete
om te voorkomen dat de compiler een standaardconstructor levert.
class C{
int i;
public:
// default constructor is explicitly deleted
C() = delete;
};
C c1; // Compile ERROR: C has its default constructor deleted
Verder kan een ontwikkelaar ook expliciet zijn over het willen dat de compiler een standaardconstructor levert.
class C{
int i;
public:
// does have automatically generated default constructor (same as implicit one)
C() = default;
C( int i ) : i(i){}
};
C c1; // default constructed
C c2( 1 ); // constructed with the int taking constructor
U kunt bepalen of een type een standaardconstructor heeft (of een primitief type is) met std::is_default_constructible
uit <type_traits>
:
class C1{ };
class C2{ public: C2(){} };
class C3{ public: C3(int){} };
using std::cout; using std::boolalpha; using std::endl;
using std::is_default_constructible;
cout << boolalpha << is_default_constructible<int>() << endl; // prints true
cout << boolalpha << is_default_constructible<C1>() << endl; // prints true
cout << boolalpha << is_default_constructible<C2>() << endl; // prints true
cout << boolalpha << is_default_constructible<C3>() << endl; // prints false
In C ++ 11 is het nog steeds mogelijk om de niet-functor-versie van std::is_default_constructible
:
cout << boolalpha << is_default_constructible<C1>::value << endl; // prints true
destructor
Een destructor is een functie zonder argumenten die wordt opgeroepen wanneer een door de gebruiker gedefinieerd object wordt vernietigd. Het is vernoemd naar het type dat het vernietigt met een voorvoegsel ~
.
class C{
int* is;
string s;
public:
C()
: is( new int[10] ){
}
~C(){ // destructor definition
delete[] is;
}
};
class C_child : public C{
string s_ch;
public:
C_child(){}
~C_child(){} // child destructor
};
void f(){
C c1; // calls default constructor
C c2[2]; // calls default constructor for both elements
C* c3 = new C[2]; // calls default constructor for both array elements
C_child c_ch; // when destructed calls destructor of s_ch and of C base (and in turn s)
delete[] c3; // calls destructors on c3[0] and c3[1]
} // automatic variables are destroyed here -- i.e. c1, c2 and c_ch
Onder de meeste omstandigheden (dwz een gebruiker biedt geen destructor en er zijn geen andere diskwalificerende voorwaarden), biedt de compiler impliciet een standaard destructor:
class C{
int i;
string s;
};
void f(){
C* c1 = new C;
delete c1; // C has a destructor
}
class C{
int m;
private:
~C(){} // not public destructor!
};
class C_container{
C c;
};
void f(){
C_container* c_cont = new C_container;
delete c_cont; // Compile ERROR: C has no accessible destructor
}
In C ++ 11 kan een ontwikkelaar dit gedrag negeren door te voorkomen dat de compiler een standaardvernietiger levert.
class C{
int m;
public:
~C() = delete; // does NOT have implicit destructor
};
void f{
C c1;
} // Compile ERROR: C has no destructor
Verder kan een ontwikkelaar ook expliciet zijn over het willen dat de compiler een standaard destructor levert.
class C{
int m;
public:
~C() = default; // saying explicitly it does have implicit/empty destructor
};
void f(){
C c1;
} // C has a destructor -- c1 properly destroyed
U kunt bepalen of een type een destructor heeft (of een primitief type is) met std::is_destructible
van <type_traits>
:
class C1{ };
class C2{ public: ~C2() = delete };
class C3 : public C2{ };
using std::cout; using std::boolalpha; using std::endl;
using std::is_destructible;
cout << boolalpha << is_destructible<int>() << endl; // prints true
cout << boolalpha << is_destructible<C1>() << endl; // prints true
cout << boolalpha << is_destructible<C2>() << endl; // prints false
cout << boolalpha << is_destructible<C3>() << endl; // prints false