web-dev-qa-db-fra.com

Raisons de la définition de fonctions membres non-const 'get'?

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?

21
Juri

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&.

15
user463035818

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:

1. Facile à instrumenter

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.

2. Convention culturelle/"le patron l'a dit"

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.

3. Peut-être que 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).

4. Permet de modifier les composants internes de la classe ultérieurement sans modifier le code utilisateur

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.

8
einpoklum

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.

5
Jans

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 .

3
Igor R.

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;
}
1
geza

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()
0
Dmytro Dadyka