web-dev-qa-db-fra.com

Comment écrire des getters et des setters C++

Si j'ai besoin d'écrire un setter et/ou un getter pour une propriété, je l'écris comme ceci:

struct X { /*...*/};

class Foo
{
private:
    X x_;

public:
    void set_x(X value)
    {
        x_ = value;
    }
    X get_x()
    {
        return x_;
    }
};

Cependant, j’ai entendu dire qu’il s’agissait du style Java d’écriture des paramètres et des accesseurs et que je devrais l’écrire en C++. De plus, on m'a dit que c'était inefficace et même incorrect. Qu'est-ce que ça veut dire? Comment puis-je écrire les setters et les getters en C++?


Supposons que le besoin d’obtenteurs et/ou de setters soit justifié . Par exemple. peut-être faisons-nous des vérifications dans le passeur, ou peut-être n’écrivons-nous que le getter.

3
bolov

Il existe deux formes distinctes de "propriétés" qui apparaissent dans la bibliothèque standard, que je qualifierai de "orientées sur l'identité" et "orientées sur la valeur". Le choix que vous choisissez dépend de la manière dont le système doit interagir avec Foo. Ni est plus "correct".

Identité orientée

class Foo
{
     X x_;
public:
          X & x()       { return x_; }
    const X & x() const { return x_; }
}

Nous renvoyons ici un référence au membre X sous-jacent, qui permet aux deux côtés du site d’appel d’observer les changements initiés par l’autre. Le membre X est visible du monde extérieur, probablement parce que son identité est importante. À première vue, il peut sembler qu’il n’ya que le côté "get" d’une propriété, mais ce n’est pas le cas si X est assignable.

 Foo f;
 f.x() = X { ... };

Axé sur la valeur

class Foo
{
     X x_;
public:
     X x() const { return x_; }
     void x(X x) { x_ = std::move(x); }
}

Nous renvoyons ici une copie du membre X et acceptons une copie pour écraser avec. Les modifications ultérieures des deux côtés ne se propagent pas. Vraisemblablement, nous ne nous soucions que de la valeur valeur de x dans ce cas.

8
Caleth

Au fil des ans, j'ai fini par croire que la notion de getter/setter était essentiellement toujours une erreur. Comme @Alf l'a suggéré dans les commentaires, contrairement à ce que cela puisse paraître, une variable publique est normalement la bonne réponse.

L'astuce est que la variable publique doit être du type correct. Dans la question, vous avez indiqué que nous avons écrit un objet setter qui vérifie la valeur en cours d'écriture ou que nous écrivons uniquement un objet getter (nous avons donc un objet const).

Je dirais que les deux disent en gros quelque chose comme: "X est un int. Seulement, ce n'est pas vraiment un int - c'est vraiment un peu comme un int, mais avec ces restrictions supplémentaires ..."

Et cela nous amène au point réel: si un examen attentif de X montre qu'il s'agit d'un type vraiment différent, définissez-en le type, puis créez-le en tant que membre public de ce type. Les os nus pourraient ressembler à ceci:

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

C'est générique, ainsi l'utilisateur peut spécifier quelque chose comme une fonction (par exemple, un lambda) qui assure que la valeur est correcte - elle peut transmettre la valeur sans la modifier ou la modifier (par exemple, pour un type saturant) ou peut renvoyer une exception - mais s'il ne le fait pas, ce qu'il retourne doit être une valeur acceptable pour le type spécifié.

Ainsi, par exemple, pour obtenir un type entier ne permettant que les valeurs de 0 à 10, et saturant à 0 et 10 (c’est-à-dire que tout nombre négatif devient 0 et que tout nombre supérieur à 10 devient 10, nous pourrions écrire du code sur cette règle générale. ordre:

checked<int> foo([](auto i) { return std::min(std::max(i, 0), 10); });

Ensuite, nous pouvons faire plus ou moins les choses habituelles avec un foo, avec l'assurance qu'il sera toujours dans la plage 0..10:

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;

Grâce à cela, nous pouvons rendre le membre public en toute sécurité, car le type que nous avons défini est vraiment celui que nous souhaitons: les conditions que nous voulons y appliquer sont inhérentes à ce type, et non pas un élément sur lequel nous nous basons. après le fait (pour ainsi dire) par le getter/setter.

Bien sûr, c'est pour le cas où nous voulons restreindre les valeurs d'une manière ou d'une autre. Si nous voulons juste un type qui est effectivement en lecture seule, c'est beaucoup plus facile - juste un modèle qui définit un constructeur et un operator T, mais pas un opérateur d'affectation qui prend un T comme paramètre.

Bien entendu, certains cas de saisie restreinte peuvent être plus complexes. Dans certains cas, vous voulez créer une relation entre deux choses. Ainsi, par exemple, foo doit être compris entre 0 et 1,000 et bar doit être compris entre 2x et 3x foo. Il y a deux façons de gérer des choses comme ça. La première consiste à utiliser le même modèle que ci-dessus, mais avec le type sous-jacent étant un std::Tuple<int, int> et à partir de là. Si vos relations sont vraiment complexes, vous voudrez peut-être définir entièrement une classe distincte pour définir les objets de cette relation complexe.

Résumé

Définissez votre membre comme étant du type que vous voulez vraiment, et toutes les choses utiles que le getter/setter pourrait/ferait être inclus dans les propriétés de ce type.

4
Jerry Coffin

Voici comment j'écrirais un setter/getter générique:

class Foo
{
private:
    X x_;

public:
    auto x()       -> X&       { return x_; }
    auto x() const -> const X& { return x_; }
};

Je vais essayer d'expliquer le raisonnement derrière chaque transformation:

Le premier problème avec votre version est qu'au lieu de transmettre des valeurs, vous devez passer des références const. Cela évite la copie inutile. Certes, depuis C++11, la valeur peut être déplacée, mais ce n’est pas toujours possible. Pour les types de données de base (par exemple, int), l’utilisation de valeurs au lieu de références est acceptable.

Donc, nous corrigeons d'abord pour cela.

class Foo1
{
private:
    X x_;

public:
    void set_x(const X& value)
//             ^~~~~  ^
    {
        x_ = value;
    }

    const X& get_x()
//  ^~~~~  ^
    {
        return x_;
    }
};

Pourtant il y a un problème avec la solution ci-dessus . Puisque get_x ne modifie pas l'objet, il devrait être marqué const. Cela fait partie d'un principe C++ appelé const correctness.

La solution ci-dessus ne vous laissera pas obtenir la propriété d'un objet const:

const Foo1 f;

X x = f.get_x(); // Compiler error, but it should be possible

En effet, get_x n'étant pas une méthode const, ne peut pas être appelé sur un objet const. La raison en est qu'une méthode non-const peut modifier l'objet, il est donc illégal de l'appeler sur un objet const.

Nous faisons donc les ajustements nécessaires:

class Foo2
{
private:
    X x_;

public:
    void set_x(const X& value)
    {
        x_ = value;
    }

    const X& get_x() const
//                   ^~~~~
    {
        return x_;
    }
};

La variante ci-dessus est correcte. Cependant, en C++, il existe une autre façon de l'écrire qui est plus ish C++ et moins ish Java.

Il y a deux choses à considérer:

  • nous pouvons renvoyer une référence au membre de données et si nous modifions cette référence, nous modifions en fait le membre de données lui-même. Nous pouvons utiliser cela pour écrire notre setter.
  • en C++, les méthodes peuvent être surchargées par la seule constance.

Donc, avec les connaissances ci-dessus, nous pouvons écrire notre version finale élégante C++:

Version finale

class Foo
{
private:
    X x_;

public:
    X&       x()        { return x_; }
    const X& x() const  { return x_; }
};

Par préférence, j'utilise le nouveau style de fonction de retour final. (par exemple, au lieu de int foo(), j’écris auto foo() -> int.

class Foo
{
private:
    X x_;

public:
    auto x()       -> X&       { return x_; }
    auto x() const -> const X& { return x_; }
};

Et maintenant, nous changeons la syntaxe d'appel de:

Foo2 f;
X x1;

f.set_x(x1);
X x2 = f.get_x();

à:

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();

Au-delà de la version finale

Pour des raisons de performances, nous pouvons aller encore plus loin et surcharger && et renvoyer une référence rvalue à x_, permettant ainsi de la quitter si nécessaire.

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_); }

};

Merci beaucoup pour les commentaires reçus dans les commentaires et particulièrement à StorryTeller pour ses excellentes suggestions pour améliorer ce post.

4
bolov

Votre principale erreur est que si vous n'utilisez pas de références dans les paramètres de l'API et que vous renvoyez une valeur, vous pouvez risquer d'effectuer des copies inutiles dans les deux opérations get/set ("PEUT" probablement être en mesure d'éviter ces copies).

Je vais l'écrire comme:

class Foo
{
private:
    X x_;
public:
    void x(const X &value) { x_ = value; }
    const X &x() const { return x_; }
};

Cela conservera la exactitude const, qui est une fonctionnalité très importante de C++, et compatible avec les anciennes versions de C++ (l’autre réponse nécessite c ++ 11).

Vous pouvez utiliser cette classe avec:

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_

Je trouve l'utilisation de get/set superflue avec _ ou avec camel (ie getX (), setX ()), si vous faites quelque chose de mal, le compilateur vous aidera à résoudre ce problème. 

Si vous souhaitez modifier l'objet interne Foo :: X, vous pouvez également ajouter une troisième surcharge de x ():

X &x() { return x_; }

.. de cette façon, vous pouvez écrire quelque chose comme:

Foo f;
X obj;
f.x() = obj; // replace inner object
f.x().int_member = 1; // replace a single value inside f::x_

mais je vous suggère d'éviter cela sauf si vous avez vraiment besoin de modifier très souvent la structure interne (X).

0
gabry

Utilisez des IDE pour générer. CLion offre la possibilité d'insérer des getters et des setters basés sur un membre de classe. De là, vous pouvez voir le résultat généré et suivre la même pratique. 

0
Hot Since 7cc