Je travaille sur l’apprentissage du C++ avec le livre de Stroustrup (Principes de programmation et travaux pratiques utilisant le C++). Dans un exercice, nous définissons une structure simple:
template<typename T>
struct S {
explicit S(T v):val{v} { };
T& get();
const T& get() const;
void set(T v);
void read_val(T& v);
T& operator=(const T& t); // deep copy assignment
private:
T val;
};
On nous demande ensuite de définir une fonction membre const et non const pour obtenir val
.
Je me demandais: y a-t-il des cas où il est logique d'avoir une fonction non-constante get
qui retourne val
?
Il me semble beaucoup plus clair que nous ne pouvons pas changer la valeur indirectement dans de telles situations. Quels pourraient être les cas d'utilisation où vous avez besoin d'une fonction const et d'une fonction non-constante get
pour renvoyer une variable membre?
Objets non constants?
Getters et setters ne sont que convention. Au lieu de fournir un getter et un setter, un idiome parfois utilisé consiste à fournir quelque chose le long de la ligne
struct foo {
int val() const { return val_; }
int& val() { return val_; }
private:
int val_;
};
En fonction de la constance de l'instance, vous obtenez une référence ou une copie:
void bar(const foo& a, foo& b) {
auto x = a.val(); // calls the const method returning an int
b.val() = x; // calls the non-const method returning an int&
};
Que ce soit un bon style en général est une question d'opinion. Il y a des cas où cela crée de la confusion et d'autres cas où ce comportement correspond exactement à ce que vous attendez (voir ci-dessous).
Dans tous les cas, il est plus important de concevoir l'interface d'une classe en fonction de ce qu'elle est censée faire et de la manière dont vous voulez l'utiliser, plutôt que de suivre aveuglément les conventions concernant les setters et les getters (par exemple, vous devriez donner à la méthode un nom significatif. cela exprime ce qu'il fait, pas seulement en termes de "prétendre être encapsulé et me donner maintenant accès à tous vos composants internes via des accesseurs", ce que signifie réellement utiliser des accesseurs partout).
Exemple concret
Considérez que l’accès aux éléments dans les conteneurs est généralement implémenté comme ceci Comme exemple de jouet:
struct my_array {
int operator[](unsigned i) const { return data[i]; }
int& operator[](unsigned i) { return data[i]; }
private:
int data[10];
};
Ce n'est pas le travail de conteneur de cacher les éléments à l'utilisateur (même data
pourrait être public). Vous ne voulez pas que différentes méthodes accèdent à des éléments selon que vous voulez lire ou écrire l'élément, il est donc logique de fournir une surcharge const
et une surcharge non-const.
référence non-const de get vs encapsulation
Ce n’est peut-être pas si évident, mais il est un peu controversé de savoir si le fait de fournir des accesseurs et des setters prend en charge l’encapsulation ou l’inverse. Bien qu'en général, cette question repose dans une large mesure sur l'opinion, mais pour les accesseurs qui renvoient des références non constantes, il ne s'agit pas tellement d'opinions. Ils brisent l'encapsulation. Considérer
struct broken {
void set(int x) {
counter++;
val = x;
}
int& get() { return x; }
int get() const { return x; }
private:
int counter = 0;
int value = 0;
};
Cette classe est cassée comme son nom l'indique. Les clients peuvent simplement saisir une référence et la classe n'a aucune chance de compter le nombre de fois où la valeur est modifiée (comme le suggère set
). Une fois que vous avez renvoyé une référence non-const, alors en ce qui concerne l'encapsulation, il y a peu de différence par rapport au membre. Par conséquent, ceci n'est utilisé que dans les cas où un tel comportement est naturel (par exemple, un conteneur).
PS
Notez que votre exemple renvoie un const T&
plutôt qu'une valeur. C'est raisonnable pour un code de modèle, où vous ne savez pas combien coûte une copie, alors que pour un int
, vous ne gagnerez pas beaucoup en retournant un const int&
au lieu d'un int
. Par souci de clarté, j’ai utilisé des exemples non modèles, mais pour un code basé sur un modèle, vous préféreriez probablement renvoyer un const T&
.
Permettez-moi d'abord de reformuler votre question:
Pourquoi avoir un membre non-constant pour un membre plutôt que de le rendre public?
Plusieurs raisons possibles:
Qui a dit que le non-consommateur doit être juste:
T& get() { return val; }
? ça pourrait bien être quelque chose comme:
T& get() {
if (check_for_something_bad()) {
throw std::runtime_error{"Attempt to mutate val when bad things have happened");
}
return val;
}
However, as @BenVoigt suggests, it is more appropriate to wait until the caller actually tries to mutate the value through the reference before spewing an error.
Certaines organisations appliquent des normes de codage. Ces normes de codage sont parfois créées par des personnes trop défensives. Donc, vous pourriez voir quelque chose comme:
Sauf si votre classe est un type "données anciennes" , aucun membre de données ne peut être public. Vous pouvez utiliser des méthodes getter pour ces membres non publics, si nécessaire.
et puis, même s'il est logique qu'une classe spécifique n'autorise qu'un accès non-const, cela ne se produira pas.
val
n'y est tout simplement pas?Vous avez donné un exemple dans lequel val
est en fait existe dans une instance de la classe. Mais en fait, ça ne doit pas! La méthode get()
peut renvoyer une sorte d’objet proxy qui, lors de l’affectation, de la mutation, etc., effectue certains calculs (par exemple, le stockage ou la récupération de données dans une base de données).
Maintenant, en lisant les articles 1. ou 3 ci-dessus, vous pourriez demander "mais mon struct S
_ est-ce que a val
!" ou "par mon get()
ne fait rien d'intéressant!" - Bien, c'est vrai, ils ne le font pas. mais vous voudrez peut-être changer ce comportement à l'avenir. Sans get()
, tous les utilisateurs de votre classe devront modifier leur code. Avec get()
, il vous suffit de modifier l’implémentation de struct S
.
Maintenant, je ne préconise pas ce type d'approche de conception, mais certains programmeurs le font.
get()
est appelable par des objets non const autorisés à muter, vous pouvez faire:
S r(0);
r.get() = 1;
mais si vous définissez r
const en tant que const S r(0)
, la ligne r.get() = 1
ne compilera plus, pas même pour récupérer la valeur. C'est pourquoi vous avez besoin d'une version constante const T& get() const
permettant au moins de récupérer la valeur des objets const.
const S r(0)
int val = r.get()
La version const des fonctions membres tente d’être cohérente avec la propriété constness de l’objet sur lequel l’appel est effectué. En d’autres termes, si l’objet est immuable en étant const et que la fonction membre renvoie une référence, elle peut refléter le constness de l'appelant en renvoyant une référence const, préservant ainsi la propriété d'immutabilité de l'objet.
Cela dépend du but de S
. S'il s'agit d'une sorte de wrapper mince, il peut être approprié de permettre à l'utilisateur d'accéder directement à la valeur sous-jacente.
Un des exemples de la vie réelle est std::reference_wrapper
.
Si un getter renvoie simplement une référence non-const à un membre, comme ceci:
private:
Object m_member;
public:
Object &getMember() {
return m_member;
}
Alors m_member
devrait être public à la place, et l'accesseur n'est pas nécessaire. Il est absolument inutile de rendre ce membre privé, puis de créer un accesseur qui donne tout accès à celui-ci.
Si vous appelez getMember()
, vous pouvez stocker la référence résultante dans un pointeur/une référence, puis faire ce que vous voulez avec m_member
, la classe englobante n'en saura rien. C'est pareil, comme si m_member
avait été public.
Notez que si getMember()
effectue une tâche supplémentaire (par exemple, il ne renvoie pas simplement m_member
, mais la construit paresseusement), alors getMember()
pourrait être utile:
Object &getMember() {
if (!m_member) m_member = new Object;
return *m_member;
}
Je voudrais également souligner le côté pratique de la question. Par exemple, vous voulez agréger votre objet Bar
dans un autre objet Foo
et créer une méthode constante doSomething
qui utilise Bar
uniquement pour lire:
class Foo
{
Bar bar_;
public:
void doSomething() const
{
//bar_.get();
}
}
Vous avez un problème! Vous êtes absolument certain de ne pas modifier l'objet Foo
, vous souhaitez le marquer avec le modificateur const
, mais vous ne pouvez pas le faire car get
n'est pas const
. En d'autres termes, vous créez vos propres problèmes pour l'avenir et vous compliquez la rédaction d'un code clair et de haute qualité. Mais vous pouvez implémenter deux versions pour get
si vous en avez vraiment besoin:
class Bar
{
int data = 1;
public:
int& get() { return data; };
const int& get() const { return data; };
};
une telle situation peut se produire si vous avez déjà un code dans lequel les modificateurs constants sont ignorés. Par conséquent, une telle situation est courante:
foo(int* a); // Just get a, don`t modified
foo(&bar_.get()); // need non constant get()