Als ik een setter en/of getter voor een eigenschap moet schrijven, schrijf ik het als volgt:
struct X { /*...*/};
class Foo
{
private:
X x_;
public:
void set_x(X value)
{
x_ = value;
}
X get_x()
{
return x_;
}
};
Ik heb echter gehoord dat dit de Java-stijlis van het schrijven van setters en getters en dat ik het in C++-stijl moet schrijven. Bovendien kreeg ik te horen dat het inadequaat en zelfs onjuist is. Wat betekent dat? Hoe kan ik de setters en getters in C++ schrijven?
Neem aan dat de behoefte aan getters en/of setters gerechtvaardigd is. bijv. misschien doen we wat controles in de setter, of misschien schrijven we alleen de getter.
Antwoord 1, autoriteit 100%
Er zijn twee verschillende vormen van “eigenschappen” die voorkomen in de standaardbibliotheek, die ik zal categoriseren als “Identiteitsgericht” en “Waardegericht”. Welke je kiest, hangt af van hoe het systeem moet communiceren met foo
. Geen van beide is “juist”.
Identiteitsgericht
class Foo
{
X x_;
public:
X & x() { return x_; }
const X & x() const { return x_; }
}
Hier retourneren we een referentienaar het onderliggende X
-lid, waardoor beide zijden van de oproepsite wijzigingen kunnen waarnemen die door de ander zijn geïnitieerd. Het X
lid is zichtbaar voor de buitenwereld, vermoedelijk omdat zijn identiteit belangrijk is. Het kan op het eerste gezicht lijken alsof er alleen de “krijg”-kant van een eigenschap is, maar dit is niet het geval als X
toewijsbaar is.
Foo f;
f.x() = X { ... };
Waarde georiënteerd
class Foo
{
X x_;
public:
X x() const { return x_; }
void x(X x) { x_ = std::move(x); }
}
Hier retourneren we een van de van de X
-lid en accepteren een kopie om met te overschrijven. Latere veranderingen aan beide kanten verspreiden zich niet. Vermoedelijk geven we alleen om de -waarde van X
in dit geval.
Antwoord 2, Autoriteit 63%
In de loop der jaren ben ik geloven dat het hele idee van Getter / Setter meestal een vergissing is. Zoals in tegenspraak is, zoals het kan klinken, is een openbare variabele normaal gesproken het juiste antwoord.
De truc is dat de openbare variabele van het juiste type moet zijn. In de vraag die u hebt opgegeven dat we een setter hebben geschreven dat enige controle is van de waarde die wordt geschreven, of anders dat we alleen een Getter schrijven (dus we hebben een effectief const
Object ).
Ik zou zeggen dat allebei in feite iets zeggen als: “X is een int. Alleen is het niet echt een int – het is echt iets soort van als een int, maar met deze extra beperkingen …”
En dat brengt ons naar het echte punt: als er een zorgvuldige blik op x laat zien dat het echt een ander type is, definieer het type dat het echt is, en maak het dan als een openbaar lid van dat type. De blote botten ervan kunnen er zoiets uitzien:
template <class T>
class checked {
T value;
std::function<T(T const &)> check;
public:
template <class checker>
checked(checker check)
: check(check)
, value(check(T()))
{ }
checked &operator=(T const &in) { value = check(in); return *this; }
operator T() const { return value; }
friend std::ostream &operator<<(std::ostream &os, checked const &c) {
return os << c.value;
}
friend std::istream &operator>>(std::istream &is, checked &c) {
try {
T input;
is >> input;
c = input;
}
catch (...) {
is.setstate(std::ios::failbit);
}
return is;
}
};
Dit is generiek, dus de gebruiker kan iets functioneels specificeren (bijv. een lambda) dat ervoor zorgt dat de waarde correct is – het kan de waarde ongewijzigd doorgeven, of het kan het wijzigen (bijv. voor een verzadigend type ) of het kan een uitzondering genereren – maar als het niet wordt gegenereerd, moet wat het retourneert een waarde zijn die acceptabel is voor het type dat wordt opgegeven.
Dus om bijvoorbeeld een geheel getal te krijgen dat alleen waarden van 0 tot 10 toestaat en verzadigt bij 0 en 10 (dwz elk negatief getal wordt 0 en elk getal groter dan 10 wordt 10, we kunnen code schrijven op deze algemene bestelling:
checked<int> foo([](auto i) { return std::min(std::max(i, 0), 10); });
Dan kunnen we min of meer de gebruikelijke dingen doen met een foo
, met de zekerheid dat deze altijd in het bereik 0..10 zal liggen:
std::cout << "Please enter a number from 0 to 10: ";
std::cin >> foo; // inputs will be clamped to range
std::cout << "You might have entered: " << foo << "\n";
foo = foo - 20; // result will be clamped to range
std::cout << "After subtracting 20: " << foo;
Hiermee kunnen we het lid veilig openbaar maken, omdat het type dat we het hebben gedefinieerd, echt het type is dat we willen dat het is – de voorwaarden die we eraan willen stellen zijn inherent aan het type, niet iets dat achteraf (om zo te zeggen) door de getter/setter is geplakt.
Dat is natuurlijk voor het geval dat we de waarden op de een of andere manier willen beperken. Als we alleen een type willen dat in feite alleen-lezen is, is dat veel eenvoudiger – alleen een sjabloon die een constructor en een operator T
definieert, maar geen toewijzingsoperator die een T als parameter neemt.
Natuurlijk kunnen sommige gevallen van beperkte invoer ingewikkelder zijn. In sommige gevallen wil je zoiets als een relatie tussen twee dingen, dus (bijvoorbeeld) foo
moet in het bereik 0..1000 liggen en bar
moet tussen 2x liggen en 3x foo
. Er zijn twee manieren om dat soort dingen aan te pakken. Een daarvan is om dezelfde sjabloon als hierboven te gebruiken, maar met het onderliggende type std::tuple<int, int>
, en vanaf daar verder te gaan. Als je relaties echt complex zijn, wil je misschien een aparte klasse definiëren om de objecten in die complexe relatie te definiëren.
Samenvatting
Definieer uw lid als van het type dat u echt wilt, en alle nuttige dingen die de getter/setter zou kunnen/zou kunnen doen worden ondergebracht in de eigenschappen van dat type.
Antwoord 3, autoriteit 35%
Zo zou ik een generieke setter/getter schrijven:
class Foo
{
private:
X x_;
public:
X& x() { return x_; }
const X& x() const { return x_; }
};
Ik zal proberen de redenering achter elke transformatie uit te leggen:
Het eerste probleem met uw versie is dat in plaats van waarden door te geven, u const-referenties moet doorgeven. Dit voorkomt onnodig kopiëren. Toegegeven, sinds C++11
kan de waarde worden verplaatst, maar dat is niet altijd mogelijk. Voor basisgegevenstypen (bijv. int
) is het gebruik van waarden in plaats van verwijzingen OK.
Dus daar corrigeren we eerst voor.
class Foo1
{
private:
X x_;
public:
void set_x(const X& value)
// ^~~~~ ^
{
x_ = value;
}
const X& get_x()
// ^~~~~ ^
{
return x_;
}
};
Toch is er een probleem met de bovenstaande oplossing. Aangezien get_x
het object niet wijzigt, moet het worden gemarkeerd als const
. Dit maakt deel uit van een C++-principe genaamd const correctness.
Met de bovenstaande oplossing kunt u de eigenschap niet verkrijgen van een const
-object:
const Foo1 f;
X x = f.get_x(); // Compiler error, but it should be possible
Dit komt omdat get_x
die geen const-methode is, niet kan worden aangeroepen op een const-object. De reden hiervoor is dat een niet-const-methode het object kan wijzigen, dus het is illegaal om het op een const-object aan te roepen.
Dus we maken de nodige aanpassingen:
class Foo2
{
private:
X x_;
public:
void set_x(const X& value)
{
x_ = value;
}
const X& get_x() const
// ^~~~~
{
return x_;
}
};
De bovenstaande variant is correct. In C++ is er echter een andere manier om het te schrijven, die meer C++-achtig is en minder Java-achtig.
Er zijn twee dingen om te overwegen:
- we kunnen een verwijzing naar het gegevenslid retourneren en als we die verwijzing wijzigen, wijzigen we het gegevenslid zelf. We kunnen dit gebruiken om onze setter te schrijven.
- in C++ kunnen methoden alleen al worden overbelast door vastberadenheid.
Dus met de bovenstaande kennis kunnen we onze laatste elegante C++-versie schrijven:
Definitieve versie
class Foo
{
private:
X x_;
public:
X& x() { return x_; }
const X& x() const { return x_; }
};
Als persoonlijke voorkeur gebruik ik de nieuwe trailing return-functiestijl. (bv. in plaats van int foo()
schrijf ik auto foo() -> int
.
class Foo
{
private:
X x_;
public:
auto x() -> X& { return x_; }
auto x() const -> const X& { return x_; }
};
En nu veranderen we de aanroepsyntaxis van:
Foo2 f;
X x1;
f.set_x(x1);
X x2 = f.get_x();
naar:
Foo f;
X x1;
f.x() = x1;
X x2 = f.x();
const Foo cf;
X x1;
//cf.x() = x1; // error as expected. We cannot modify a const object
X x2 = cf.x();
Voorbij de definitieve versie
Om prestatieredenen kunnen we een stap verder gaan en &&
overbelasten en een rvalue-referentie retourneren naar x_
, zodat we er indien nodig van kunnen afwijken.
class Foo
{
private:
X x_;
public:
auto x() const& -> const X& { return x_; }
auto x() & -> X& { return x_; }
auto x() && -> X&& { return std::move(x_); }
};
Hartelijk dank voor de feedback die is ontvangen in opmerkingen en in het bijzonder aan StorryTeller voor zijn geweldige suggesties om dit bericht te verbeteren.
Antwoord 4
Uw belangrijkste fout is dat als u geen verwijzingen gebruikt in de API-parameters en retourwaarde, u mogelijkhet risico loopt onnodige kopieën uit te voeren in beide get/set-bewerkingen (“MAY” want als u gebruik de optimizer die je compileert waarschijnlijk in staat zal zijn om deze kopieën te vermijden).
Ik zal het schrijven als:
class Foo
{
private:
X x_;
public:
void x(const X &value) { x_ = value; }
const X &x() const { return x_; }
};
Hierdoor blijft de const correctheidbehouden, dat is een zeer belangrijk kenmerk van C++, en het is compatibel met oudere C++-versies (het andere antwoord vereist c++11).
Je kunt deze les gebruiken met:
Foo f;
X obj;
f.x(obj);
X objcopy = f.x(); // get a copy of f::x_
const X &objref = f.x(); // get a reference to f::x_
Ik vind het gebruik van get/set overbodig zowel met _ als met camel case (dwz getX(), setX()), als je iets verkeerd doet, zal de compiler je helpen het op te lossen.
Als u het innerlijke Foo::X-object wilt wijzigen, kunt u ook een derde overbelasting van x():
toevoegen
X &x() { return x_; }
.. op deze manier kun je iets schrijven als:
Foo f;
X obj;
f.x() = obj; // replace inner object
f.x().int_member = 1; // replace a single value inside f::x_
maar ik raad je aan dit te vermijden, behalve als je echt heel vaak de innerlijke structuur (X) moet wijzigen.
Antwoord 5
Gebruik een IDE voor het genereren. CLion biedt de mogelijkheid om getters en setters in te voegen op basis van een klassenlid. Van daaruit kunt u het gegenereerde resultaat zien en dezelfde oefening volgen.