Savoir choisir entre un setter et un reference-getter

Article original : How to choose between a setter and a reference-getter? | Belay the C++ (belaycpp.com)
Traductrice : Chloé Lourseyre

Contexte

Quand on implemente une classe, on a souvent besoin d’implémenter un accesseur pour un ou plusieurs attributs de cette classe pour les modifier.

Il y a deux manières de faire cela :

  • Implémenter un setter, une méthode qui prend comme argument une nouvelle valeur à destination de l’attribut.
  • Implémenter un reference-getter, une méthode qui renvoie une référence vers l’attribut lui-même.

Voici un petit exemple qui montre comment accéder à l’attribut bar en utilisant les deux méthodes :

class Foo {
    int m_bar;
 
public:
    // Setter
    void set_bar(int bar) {
        m_bar = bar;
    }
 
    // Reference-getter
    int & get_bar() {
        return m_bar;
    }
};
 
int main() {
    Foo foo;
 
    // Editing via setter
    foo.set_bar(42);
 
    // Editing via reference-getter
    foo.get_bar() = 84;
 
    return 0;
}

Certains d’entre vous objecteront peut-être qu’il existe d’autres manières d’accéder à un attribut en écriture, mais je soutiens qu’il s’agira toujours d’une variation d’une de ces deux méthodes.

Le setter

Un setter est une interface lecture-seule sur une classe. Vous lui fournissez une valeur et la classe est mise à jour en conséquence.

Souvent (mais pas toujours), cela va plus ou moins directement mettre à jour l’attribut en copiant/movant le paramètre.

Exemples

// Most simple setter
void set_foo(int foo) {
    m_foo = foo;
}
// A setter that performs a test before edition
void set_foo(Foo foo) {
    if (foo.is_valid())
        m_foo = foo;
}
// A move-setter
void set_big_foo(BigFoo && big_foo) {
    m_big_foo = std::forward<BigFoo>(big_foo);
}

Le reference-getter

Un reference-getter est une méthode qui renvoie directement une référence sur l’attribut qu’on veut éditer.

C’est particulièrement pratique sur un attribut qui est un objet sur lequel on peut appeler des méthodes non-constantes.

Exemples

// Here is the implementation of the reference-getter
Foo & MyClass::get_foo() {
    return m_foo;
}
 
// ...
 
// Used to edit an attribute
myClass.getFoo().bar = 42;
 
// Used to call a non-const method
myClass.getFoo().udpate();

Comment choisir ?

C’est assez simple quand on arrive à distinguer les différences entre les deux.

Le setter est nécessaire quand on veut recréer la valeur et la place à la place de l’existante. C’est recommandé quand on modifie des valeurs très simple (entiers, flottants, pointeurs, etc.) ou si vous avez besoin d’un objet tout neuf. De plus, on doit utiliser un setter quand on veut explicitement interdire la lecture de l’attribut (écriture-seule).

Le reference-getter est nécessaire quand ce sont les données de l’attribut qui sont modifiées (et non l’attribut lui-même). Souvent, on l’utilise pour modifier une partie seulement de l’attribut ou pour appeler des fonctions de modification dessus.

En d’autres mots, le setter remplace la valeur et le reference-getter modifie la valeur.

Exemple

Prenez ce code:

#include <vector>
 
using namespace std;
 
struct Item
{
    bool validity;
    int value;
};
 
class Foo
{
public:
    Foo(size_t size) :
        m_max_size(size),
        m_data(size, {true, 0})
    {}
 
    void set_max_size(size_t max_size) {
        m_max_size = max_size;
    }
 
    Item & get_item(size_t index) {
        return m_data.at(index);
    }
 
    size_t get_data_size() const {
        return m_data.size();
    }
 
private:
    bool m_max_size;
    std::vector<Item> m_data;
};
 
static void set_foo_size(Foo & foo, size_t new_size)
{
    foo.set_max_size(new_size);
    for (size_t i = new_size ; i < foo.get_data_size() ; ++i)
        foo.get_item(i).validity = false;
}

Ici, nous avons une simple petite classe qui détient une collection de données (des Item). Ces items peuvent être valident ou invalides (true est valide, false est invalide).

Puis, on implémente une petite fonction qui change la taille max de la collection. On choisit de ne pas enlever les éléments mais à la place de les rendre invalides.

On accède à m_max_size via un setter parque que c’est une donnée élémentaire (un size_t) qui est remplacée quand on change la taille de la collection.

On accède à chaque Item de m_data en utilisant un reference-getter car on ne veut juste modifier l’item, ni plus ni moins.

Alternative

On aurait pu faire autrement pour mettre à jour la validité, en utilisant un setter plus spécifique, comme ceci :

class Foo {
 
        // ...
 
        void set_item_validity(size_t index, bool validity) {
                m_data.at(index).validity = validity;
        }
 
        // ...
 
};

Procéder ainsi empêche de modifier la value de l’Item. De fait, la décision d’utiliser cette alternative dépendra entièrement de votre implémentation.

Cependant, il faut considérer comme mauvaise pratique le fait d’implémenter un setter pour validity et value. Le faire pour un data-bucket de deux attributs n’est pas conséquent, mais plus votre codebase grossira plus vous serez pollué·e par des accesseurs inutiles. Vous avez besoin d’un accès complet ? Implémentez un reference-getter.

En conclusion

Cela peut sembler être un sujet trivial, mais je vois beaucoup de confusion entre les deux méthodes aujourd’hui. Soyez vigilant·e·s et gardez en tête que les deux méthodes existent.

Merci de votre attention et à la semaine prochaine !

Article original : How to choose between a setter and a reference-getter? | Belay the C++ (belaycpp.com)
Traductrice : Chloé Lourseyre

Laisser un commentaire