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