Je programme en C # depuis un moment et je souhaite maintenant parfaire mes compétences en C++.
Avoir la classe:
class Foo
{
const std::string& name_;
...
};
Quelle serait la meilleure approche (je veux seulement permettre un accès en lecture au champ name_):
inline const std::string& name() const { return name_; }
Merci.
Rendre les champs non const constants est généralement une mauvaise idée, car il devient alors difficile de forcer des contraintes de vérification des erreurs et/ou d'ajouter des effets secondaires aux modifications de valeur à l'avenir.
Dans votre cas, vous avez un champ const, les problèmes ci-dessus ne sont donc pas un problème. Le principal inconvénient de ce domaine public est que vous bloquez l'implémentation sous-jacente. Par exemple, si à l'avenir vous souhaitez modifier la représentation interne en une chaîne C, une chaîne Unicode ou quelque chose d'autre, vous supprimez tout le code client. Avec un getter, vous pouvez convertir la représentation héritée des clients existants tout en fournissant les nouvelles fonctionnalités aux nouveaux utilisateurs via un nouveau getter.
Je suggérerais quand même d'avoir une méthode de lecture comme celle que vous avez placée plus haut. Cela maximisera votre flexibilité future.
L'utilisation d'une méthode getter constitue un meilleur choix de conception pour une classe de longue durée, car elle vous permet de remplacer la méthode getter par quelque chose de plus compliqué à l'avenir. Bien que cela semble moins nécessaire pour une valeur constante, le coût est faible et les avantages possibles sont importants.
En passant, en C++, il est particulièrement judicieux de donner à getter et à setter un membre le même nom, car à l'avenir, vous pourrez alors changer la paire de méthodes:
class Foo {
public:
std::string const& name() const; // Getter
void name(std::string const& newName); // Setter
...
};
Dans une seule variable membre publique qui définit un operator()()
pour chaque:
// This class encapsulates a fancier type of name
class fancy_name {
public:
// Getter
std::string const& operator()() const {
return _compute_fancy_name(); // Does some internal work
}
// Setter
void operator()(std::string const& newName) {
_set_fancy_name(newName); // Does some internal work
}
...
};
class Foo {
public:
fancy_name name;
...
};
Le code client devra bien sûr être recompilé, mais aucune modification de syntaxe n'est requise! De toute évidence, cette transformation fonctionne tout aussi bien pour les valeurs const, dans lesquelles seul un getter est nécessaire.
En passant, en C++, il est quelque peu étrange d'avoir un membre de référence const. Vous devez l'assigner dans la liste des constructeurs. À qui appartient la mémoire de cet objet et quelle est sa durée de vie?
En ce qui concerne le style, je suis d'accord avec les autres pour dire que vous ne voulez pas exposer vos proches. :-) J'aime ce modèle pour les setters/getters
class Foo
{
public:
const string& FirstName() const;
Foo& FirstName(const string& newFirstName);
const string& LastName() const;
Foo& LastName(const string& newLastName);
const string& Title() const;
Foo& Title(const string& newTitle);
};
De cette façon, vous pouvez faire quelque chose comme:
Foo f;
f.FirstName("Jim").LastName("Bob").Title("Programmer");
Je pense que l'approche C++ 11 serait plus comme ça maintenant.
#include <string>
#include <iostream>
#include <functional>
template<typename T>
class LambdaSetter {
public:
LambdaSetter() :
getter([&]() -> T { return m_value; }),
setter([&](T value) { m_value = value; }),
m_value()
{}
T operator()() { return getter(); }
void operator()(T value) { setter(value); }
LambdaSetter operator=(T rhs)
{
setter(rhs);
return *this;
}
T operator=(LambdaSetter rhs)
{
return rhs.getter();
}
operator T()
{
return getter();
}
void SetGetter(std::function<T()> func) { getter = func; }
void SetSetter(std::function<void(T)> func) { setter = func; }
T& GetRawData() { return m_value; }
private:
T m_value;
std::function<const T()> getter;
std::function<void(T)> setter;
template <typename TT>
friend std::ostream & operator<<(std::ostream &os, const LambdaSetter<TT>& p);
template <typename TT>
friend std::istream & operator>>(std::istream &is, const LambdaSetter<TT>& p);
};
template <typename T>
std::ostream & operator<<(std::ostream &os, const LambdaSetter<T>& p)
{
os << p.getter();
return os;
}
template <typename TT>
std::istream & operator>>(std::istream &is, const LambdaSetter<TT>& p)
{
TT value;
is >> value;
p.setter(value);
return is;
}
class foo {
public:
foo()
{
myString.SetGetter([&]() -> std::string {
myString.GetRawData() = "Hello";
return myString.GetRawData();
});
myString2.SetSetter([&](std::string value) -> void {
myString2.GetRawData() = (value + "!");
});
}
LambdaSetter<std::string> myString;
LambdaSetter<std::string> myString2;
};
int _tmain(int argc, _TCHAR* argv[])
{
foo f;
std::string hi = f.myString;
f.myString2 = "world";
std::cout << hi << " " << f.myString2 << std::endl;
std::cin >> f.myString2;
std::cout << hi << " " << f.myString2 << std::endl;
return 0;
}
J'ai testé cela dans Visual Studio 2013. Malheureusement, pour pouvoir utiliser le stockage sous-jacent à l'intérieur du LambdaSetter, je devais fournir un accesseur public "GetRawData" pouvant entraîner une cassure de l'encapsulation, mais vous pouvez le laisser de côté et fournir votre propre conteneur de stockage. Assurez-vous simplement que le seul moment où vous utilisez "GetRawData" est lorsque vous écrivez une méthode de lecture/paramétrage personnalisée.
Même si le nom est immuable, vous pouvez toujours avoir la possibilité de le calculer plutôt que de le stocker dans un champ. (Je réalise que c'est peu probable pour "name", mais visons le cas général.) Pour cette raison, même les champs constants sont mieux placés dans les getters:
class Foo {
public:
const std::string& getName() const {return name_;}
private:
const std::string& name_;
};
Notez que si vous deviez changer getName()
pour renvoyer une valeur calculée, il ne pourrait pas renvoyer const ref. C'est bon, car cela ne nécessite aucune modification des appelants (recompilation modulo.)
Évitez les variables publiques, à l'exception des classes qui sont essentiellement des structures de style C. Ce n’est tout simplement pas une bonne pratique.
Une fois que vous avez défini l'interface de classe, vous ne pourrez peut-être jamais la modifier (hormis l'ajout), car les utilisateurs pourront l'utiliser et s'en servir. Rendre une variable publique signifie que vous devez avoir cette variable, et vous devez vous assurer que celle-ci répond aux besoins de l'utilisateur.
Maintenant, si vous utilisez un getter, vous vous engagez à fournir certaines informations, qui sont actuellement conservées dans cette variable. Si la situation change et que vous préférez ne pas conserver cette variable tout le temps, vous pouvez modifier l'accès. Si les exigences changent (et que des modifications assez étranges ont été apportées) et que vous avez surtout besoin du nom qui se trouve dans cette variable, mais parfois celui de cette variable, vous pouvez simplement changer le getter. Si vous rendiez la variable publique, vous seriez coincé avec elle.
Cela n'arrivera pas toujours, mais je trouve qu'il est beaucoup plus facile d'écrire rapidement, plutôt que d'analyser la situation pour voir si je regretterais de rendre la variable publique (et de risquer de nous tromper plus tard).
Rendre les variables de membre privées est une bonne habitude à prendre. Tout magasin ayant des normes de code va probablement interdire de rendre public le membre variable occasionnel, et tout magasin avec des critiques de code est susceptible de vous en faire la critique.
Lorsque la facilité d’écriture importe peu, adoptez une habitude plus sûre.
Rassemblez des idées de plusieurs sources C++ et mettez-les dans un bel exemple simple et assez simple pour les getters/setters en C++:
class Canvas { public:
void resize() {
cout << "resize to " << width << " " << height << endl;
}
Canvas(int w, int h) : width(*this), height(*this) {
cout << "new canvas " << w << " " << h << endl;
width.value = w;
height.value = h;
}
class Width { public:
Canvas& canvas;
int value;
Width(Canvas& canvas): canvas(canvas) {}
int & operator = (const int &i) {
value = i;
canvas.resize();
return value;
}
operator int () const {
return value;
}
} width;
class Height { public:
Canvas& canvas;
int value;
Height(Canvas& canvas): canvas(canvas) {}
int & operator = (const int &i) {
value = i;
canvas.resize();
return value;
}
operator int () const {
return value;
}
} height;
};
int main() {
Canvas canvas(256, 256);
canvas.width = 128;
canvas.height = 64;
}
Sortie:
new canvas 256 256
resize to 128 256
resize to 128 64
Vous pouvez le tester en ligne ici: http://codepad.org/zosxqjTX
PS: FO Yvette <3
De la théorie des modèles de conception; "encapsule ce qui varie". En définissant un "getter", le principe ci-dessus est bien respecté. Ainsi, si la représentation de mise en œuvre du membre change à l'avenir, le membre peut être "massé" avant de revenir du "getter"; impliquant pas de refactoring de code côté client où l'appel 'getter' est effectué.
Cordialement,