C++
Const Correctheid
Zoeken…
Syntaxis
- class ClassOne {public: bool non_modifying_member_function () const {/ * ... * /}};
- int ClassTwo :: non_modifying_member_function () const {/ * ... * /}
- void ClassTwo :: modifying_member_function () {/ * ... * /}
- char non_param_modding_func (const ClassOne & one, const ClassTwo * two) {/ * ... * /}
- float parameter_modifying_function (ClassTwo & one, ClassOne * two) {/ * ... * /}
- korte ClassThree :: non_modding_non_param_modding_f (const ClassOne &) const {/ * ... * /}
Opmerkingen
const
correctheid is een zeer nuttig hulpmiddel voor het oplossen van problemen, omdat het de programmeur in staat stelt snel te bepalen welke functies onbedoeld code kunnen wijzigen. Het voorkomt ook dat onbedoelde fouten, zoals die in Const Correct Function Parameters
, worden weergegeven, correct compileren en onopgemerkt blijven.
Het is veel eenvoudiger om een klasse voor const
correctheid te ontwerpen, dan om later const
correctheid aan een reeds bestaande klasse toe te voegen. Indien mogelijk, het ontwerpen van een klasse die kan worden const
corrigeren zodat het const
correct, om jezelf en anderen te redden de moeite van het later te modificeren.
Merk op dat dit indien nodig ook kan worden toegepast op volatile
correctheid, met dezelfde regels als voor const
correctheid, maar dit wordt veel minder vaak gebruikt.
Referenties:
De basis
const
correctheid is de praktijk van het ontwerpen van code, zodat alleen code die een instantie moet wijzigen, een instantie kan wijzigen (dat wil zeggen schrijftoegang heeft), en omgekeerd, dat elke code die een instantie niet hoeft te wijzigen, niet kan doen dus (dwz heeft alleen leestoegang). Dit voorkomt dat de instantie onbedoeld wordt gewijzigd, waardoor code minder foutgevoelig wordt en wordt vastgelegd of de code bedoeld is om de status van de instantie te wijzigen of niet. Ook kunnen instanties worden behandeld als const
wanneer ze niet hoeven te worden gewijzigd, of als const
gedefinieerd als ze na initialisatie niet hoeven te worden gewijzigd, zonder functionaliteit te verliezen.
Dit wordt gedaan door const
CV-kwalificaties te geven en door pointer / referentieparameters const
, behalve in het geval dat ze schrijftoegang nodig hebben.
class ConstCorrectClass {
int x;
public:
int getX() const { return x; } // Function is const: Doesn't modify instance.
void setX(int i) { x = i; } // Not const: Modifies instance.
};
// Parameter is const: Doesn't modify parameter.
int const_correct_reader(const ConstCorrectClass& c) {
return c.getX();
}
// Parameter isn't const: Modifies parameter.
void const_correct_writer(ConstCorrectClass& c) {
c.setX(42);
}
const ConstCorrectClass invariant; // Instance is const: Can't be modified.
ConstCorrectClass variant; // Instance isn't const: Can be modified.
// ...
const_correct_reader(invariant); // Good. Calling non-modifying function on const instance.
const_correct_reader(variant); // Good. Calling non-modifying function on modifiable instance.
const_correct_writer(variant); // Good. Calling modifying function on modifiable instance.
const_correct_writer(invariant); // Error. Calling modifying function on const instance.
Vanwege de aard van const-correctheid begint dit met de functies van de klasse en gaat het naar buiten; als u probeert een niet- const
aan te roepen vanuit een const
instantie, of vanuit een niet- const
instantie die als const
wordt behandeld, geeft de compiler een foutmelding over het verliezen van cv-kwalificaties.
Const Correct Klasseontwerp
In een const
-correcte klasse hebben alle lidfuncties die de logische status niet wijzigen this
cv-kwalificatie als const
, wat aangeeft dat ze het object niet wijzigen (afgezien van eventuele mutable
velden, die vrij kunnen worden gewijzigd, zelfs in const
instanties ); als een const
cv-gekwalificeerde functie een referentie retourneert, moet die referentie ook const
. Hierdoor kunnen ze worden aangeroepen op zowel constante als niet-cv-gekwalificeerde instanties, omdat een const T*
in staat is om te binden aan een T*
of een const T*
. Dit laat op zijn beurt functies toe om hun doorgegeven referentieparameters als const
te declareren wanneer ze niet hoeven te worden gewijzigd, zonder enige functionaliteit te verliezen.
Bovendien zijn in een const
correct-klasse alle doorgegeven referentieparameters const
correct, zoals besproken in Const Correct Function Parameters
, zodat ze alleen kunnen worden gewijzigd wanneer de functie ze expliciet moet wijzigen.
Laten we eerst eens kijken naar this
cv-kwalificaties:
// Assume class Field, with member function "void insert_value(int);".
class ConstIncorrect {
Field fld;
public:
ConstIncorrect(Field& f); // Modifies.
Field& getField(); // Might modify. Also exposes member as non-const reference,
// allowing indirect modification.
void setField(Field& f); // Modifies.
void doSomething(int i); // Might modify.
void doNothing(); // Might modify.
};
ConstIncorrect::ConstIncorrect(Field& f) : fld(f) {} // Modifies.
Field& ConstIncorrect::getField() { return fld; } // Doesn't modify.
void ConstIncorrect::setField(Field& f) { fld = f; } // Modifies.
void ConstIncorrect::doSomething(int i) { // Modifies.
fld.insert_value(i);
}
void ConstIncorrect::doNothing() {} // Doesn't modify.
class ConstCorrectCVQ {
Field fld;
public:
ConstCorrectCVQ(Field& f); // Modifies.
const Field& getField() const; // Doesn't modify. Exposes member as const reference,
// preventing indirect modification.
void setField(Field& f); // Modifies.
void doSomething(int i); // Modifies.
void doNothing() const; // Doesn't modify.
};
ConstCorrectCVQ::ConstCorrectCVQ(Field& f) : fld(f) {}
Field& ConstCorrectCVQ::getField() const { return fld; }
void ConstCorrectCVQ::setField(Field& f) { fld = f; }
void ConstCorrectCVQ::doSomething(int i) {
fld.insert_value(i);
}
void ConstCorrectCVQ::doNothing() const {}
// This won't work.
// No member functions can be called on const ConstIncorrect instances.
void const_correct_func(const ConstIncorrect& c) {
Field f = c.getField();
c.do_nothing();
}
// But this will.
// getField() and doNothing() can be called on const ConstCorrectCVQ instances.
void const_correct_func(const ConstCorrectCVQ& c) {
Field f = c.getField();
c.do_nothing();
}
We kunnen dit vervolgens combineren met Const Correct Function Parameters
, waardoor de klasse volledig const
-correct is.
class ConstCorrect {
Field fld;
public:
ConstCorrect(const Field& f); // Modifies instance. Doesn't modify parameter.
const Field& getField() const; // Doesn't modify. Exposes member as const reference,
// preventing indirect modification.
void setField(const Field& f); // Modifies instance. Doesn't modify parameter.
void doSomething(int i); // Modifies. Doesn't modify parameter (passed by value).
void doNothing() const; // Doesn't modify.
};
ConstCorrect::ConstCorrect(const Field& f) : fld(f) {}
Field& ConstCorrect::getField() const { return fld; }
void ConstCorrect::setField(const Field& f) { fld = f; }
void ConstCorrect::doSomething(int i) {
fld.insert_value(i);
}
void ConstCorrect::doNothing() const {}
Dit kan ook worden gecombineerd met overbelasting op basis van const
, in het geval dat we één gedrag willen als de instantie const
, en een ander gedrag als dat niet het geval is; een algemeen gebruik hiervoor zijn constainers die accessors bieden die alleen modificatie toestaan als de container zelf niet- const
.
class ConstCorrectContainer {
int arr[5];
public:
// Subscript operator provides read access if instance is const, or read/write access
// otherwise.
int& operator[](size_t index) { return arr[index]; }
const int& operator[](size_t index) const { return arr[index]; }
// ...
};
Dit wordt vaak gebruikt in de standaardbibliotheek, waarbij de meeste containers overbelastingen bieden om rekening te houden met de const
.
Const Correcte functieparameters
In een const
-correcte functie worden alle doorgegeven referentieparameters gemarkeerd als const
tenzij de functie ze direct of indirect wijzigt, waardoor wordt voorkomen dat de programmeur onbedoeld iets wijzigt dat ze niet wilden veranderen. Hierdoor kan de functie om zowel te nemen const
en niet-cv-gekwalificeerde instanties, en op zijn beurt, zorgt ervoor dat de instantie is this
type te zijn const T*
wanneer een lid functie wordt aangeroepen, waarbij T
is het type class'.
struct Example {
void func() { std::cout << 3 << std::endl; }
void func() const { std::cout << 5 << std::endl; }
};
void const_incorrect_function(Example& one, Example* two) {
one.func();
two->func();
}
void const_correct_function(const Example& one, const Example* two) {
one.func();
two->func();
}
int main() {
Example a, b;
const_incorrect_function(a, &b);
const_correct_function(a, &b);
}
// Output:
3
3
5
5
Hoewel de effecten hiervan minder direct zichtbaar zijn dan die van const
correct klasseontwerp (in dat const
-correct functies en const
-incorrecte klassen compilatiefouten veroorzaken, terwijl const
-correct klassen en const
incorrecte functies correct compileren), const
correct functies zullen veel fouten opvangen die const
onjuiste functies zouden laten doorglippen, zoals die hieronder. [Merk echter op dat een const
incorrecte functie compilatiefouten zal veroorzaken als een const
instantie wordt doorgegeven terwijl deze een niet- const
instantie verwachtte.]
// Read value from vector, then compute & return a value.
// Caches return values for speed.
template<typename T>
const T& bad_func(std::vector<T>& v, Helper<T>& h) {
// Cache values, for future use.
// Once a return value has been calculated, it's cached & its index is registered.
static std::vector<T> vals = {};
int v_ind = h.get_index(); // Current working index for v.
int vals_ind = h.get_cache_index(v_ind); // Will be -1 if cache index isn't registered.
if (vals.size() && (vals_ind != -1) && (vals_ind < vals.size()) && !(h.needs_recalc())) {
return vals[h.get_cache_index(v_ind)];
}
T temp = v[v_ind];
temp -= h.poll_device();
temp *= h.obtain_random();
temp += h.do_tedious_calculation(temp, v[h.get_last_handled_index()]);
// We're feeling tired all of a sudden, and this happens.
if (vals_ind != -1) {
vals[vals_ind] = temp;
} else {
v.push_back(temp); // Oops. Should've been accessing vals.
vals_ind = vals.size() - 1;
h.register_index(v_ind, vals_ind);
}
return vals[vals_ind];
}
// Const correct version. Is identical to above version, so most of it shall be skipped.
template<typename T>
const T& good_func(const std::vector<T>& v, Helper<T>& h) {
// ...
// We're feeling tired all of a sudden, and this happens.
if (vals_ind != -1) {
vals[vals_ind] = temp;
} else {
v.push_back(temp); // Error: discards qualifiers.
vals_ind = vals.size() - 1;
h.register_index(v_ind, vals_ind);
}
return vals[vals_ind];
}
Const Correctness as Documentation
Een van de meer nuttige dingen over const
correctheid is dat het dient als een manier om code te documenteren en bepaalde garanties biedt aan de programmeur en andere gebruikers. Deze garanties worden door de compiler afgedwongen vanwege const
, waarbij een gebrek aan const
op zijn beurt aangeeft dat de code deze niet biedt.
const
CV-gekwalificeerde ledenfuncties:
- Elke
const
dieconst
kan worden verondersteld de intentie te hebben de instantie te lezen, en:- Wijzigt de logische status van de instantie waarop ze worden ingeschakeld. Daarom zullen ze geen lidvariabelen wijzigen van de instantie waarop ze worden aangeroepen, behalve
mutable
variabelen. - Roept geen andere functies aan die ledenvariabelen van de instantie zouden wijzigen, behalve
mutable
variabelen.
- Wijzigt de logische status van de instantie waarop ze worden ingeschakeld. Daarom zullen ze geen lidvariabelen wijzigen van de instantie waarop ze worden aangeroepen, behalve
- Omgekeerd kan van elke lidfunctie die geen
const
is de intentie hebben om de instantie te wijzigen, en:- Kan de logische status al dan niet wijzigen.
- Kan al dan niet andere functies aanroepen die de logische status wijzigen.
Dit kan worden gebruikt om aannames te doen over de status van het object nadat een bepaalde lidfunctie is aangeroepen, zelfs zonder de definitie van die functie te zien:
// ConstMemberFunctions.h
class ConstMemberFunctions {
int val;
mutable int cache;
mutable bool state_changed;
public:
// Constructor clearly changes logical state. No assumptions necessary.
ConstMemberFunctions(int v = 0);
// We can assume this function doesn't change logical state, and doesn't call
// set_val(). It may or may not call squared_calc() or bad_func().
int calc() const;
// We can assume this function doesn't change logical state, and doesn't call
// set_val(). It may or may not call calc() or bad_func().
int squared_calc() const;
// We can assume this function doesn't change logical state, and doesn't call
// set_val(). It may or may not call calc() or squared_calc().
void bad_func() const;
// We can assume this function changes logical state, and may or may not call
// calc(), squared_calc(), or bad_func().
void set_val(int v);
};
Vanwege const
regels zullen deze veronderstellingen door de compiler worden afgedwongen.
// ConstMemberFunctions.cpp
ConstMemberFunctions::ConstMemberFunctions(int v /* = 0*/)
: cache(0), val(v), state_changed(true) {}
// Our assumption was correct.
int ConstMemberFunctions::calc() const {
if (state_changed) {
cache = 3 * val;
state_changed = false;
}
return cache;
}
// Our assumption was correct.
int ConstMemberFunctions::squared_calc() const {
return calc() * calc();
}
// Our assumption was incorrect.
// Function fails to compile, due to `this` losing qualifiers.
void ConstMemberFunctions::bad_func() const {
set_val(863);
}
// Our assumption was correct.
void ConstMemberFunctions::set_val(int v) {
if (v != val) {
val = v;
state_changed = true;
}
}
const
Functie Parameters:
- Van elke functie met een of meer parameters die
const
kan worden aangenomen dat deze de bedoeling heeft om die parameters te lezen, en:- Wijzigt die parameters niet en roept geen ledenfuncties aan die ze zouden wijzigen.
- Zal deze parameters niet doorgeven aan een andere functie die ze zou wijzigen en / of ledenfuncties aanroepen die ze zouden wijzigen.
- Omgekeerd kan van elke functie met een of meer parameters die geen
const
zijn, worden aangenomen dat ze de bedoeling hebben om die parameters te wijzigen, en:- Kan deze parameters al dan niet wijzigen of ledenfuncties aanroepen die ze zouden kunnen wijzigen.
- Kan deze parameters al dan niet doorgeven aan andere functies die ze zouden wijzigen en / of ledenfuncties aanroepen die ze zouden wijzigen.
Dit kan worden gebruikt om aannames te doen over de status van de parameters nadat ze zijn doorgegeven aan een bepaalde functie, zelfs zonder de definitie van die functie te zien.
// function_parameter.h
// We can assume that c isn't modified (and c.set_val() isn't called), and isn't passed
// to non_qualified_function_parameter(). If passed to one_const_one_not(), it is the first
// parameter.
void const_function_parameter(const ConstMemberFunctions& c);
// We can assume that c is modified and/or c.set_val() is called, and may or may not be passed
// to any of these functions. If passed to one_const_one_not, it may be either parameter.
void non_qualified_function_parameter(ConstMemberFunctions& c);
// We can assume that:
// l is not modified, and l.set_val() won't be called.
// l may or may not be passed to const_function_parameter().
// r is modified, and/or r.set_val() may be called.
// r may or may not be passed to either of the preceding functions.
void one_const_one_not(const ConstMemberFunctions& l, ConstMemberFunctions& r);
// We can assume that c isn't modified (and c.set_val() isn't called), and isn't passed
// to non_qualified_function_parameter(). If passed to one_const_one_not(), it is the first
// parameter.
void bad_parameter(const ConstMemberFunctions& c);
Vanwege const
regels zullen deze veronderstellingen door de compiler worden afgedwongen.
// function_parameter.cpp
// Our assumption was correct.
void const_function_parameter(const ConstMemberFunctions& c) {
std::cout << "With the current value, the output is: " << c.calc() << '\n'
<< "If squared, it's: " << c.squared_calc()
<< std::endl;
}
// Our assumption was correct.
void non_qualified_function_parameter(ConstMemberFunctions& c) {
c.set_val(42);
std::cout << "For the value 42, the output is: " << c.calc() << '\n'
<< "If squared, it's: " << c.squared_calc()
<< std::endl;
}
// Our assumption was correct, in the ugliest possible way.
// Note that const correctness doesn't prevent encapsulation from intentionally being broken,
// it merely prevents code from having write access when it doesn't need it.
void one_const_one_not(const ConstMemberFunctions& l, ConstMemberFunctions& r) {
// Let's just punch access modifiers and common sense in the face here.
struct Machiavelli {
int val;
int unimportant;
bool state_changed;
};
reinterpret_cast<Machiavelli&>(r).val = l.calc();
reinterpret_cast<Machiavelli&>(r).state_changed = true;
const_function_parameter(l);
const_function_parameter(r);
}
// Our assumption was incorrect.
// Function fails to compile, due to `this` losing qualifiers in c.set_val().
void bad_parameter(const ConstMemberFunctions& c) {
c.set_val(18);
}
Hoewel het mogelijk is te omzeilen const
correctheid , en in het verlengde van deze garanties te breken, moet dit met opzet gedaan worden door de programmeur (net als het breken van inkapseling met Machiavelli
, boven), en is waarschijnlijk onbepaald gedrag veroorzaken.
class DealBreaker : public ConstMemberFunctions {
public:
DealBreaker(int v = 0);
// A foreboding name, but it's const...
void no_guarantees() const;
}
DealBreaker::DealBreaker(int v /* = 0 */) : ConstMemberFunctions(v) {}
// Our assumption was incorrect.
// const_cast removes const-ness, making the compiler think we know what we're doing.
void DealBreaker::no_guarantees() const {
const_cast<DealBreaker*>(this)->set_val(823);
}
// ...
const DealBreaker d(50);
d.no_guarantees(); // Undefined behaviour: d really IS const, it may or may not be modified.
Echter, als gevolg van deze eisen de programmeur om heel specifiek de compiler te vertellen dat ze van plan zijn om te negeren const
ness, en niet consequent over compilers, het is over het algemeen veilig wordt aangenomen dat de const
juiste code onthoudt zich van het doen, tenzij anders aangegeven.