Logo Spiria

Des amis de confiance avec l'idiome de la carte d'accès en C++

21 mai 2015.

Le mot clé friend en C++ a toujours été controversé. On lui reproche de rompre l’encapsulation. Cet article vise à présenter un autre idiome, moins bien connu, qui permet de limiter la portée d’une déclaration friend. Prenons l’exemple d’une classe Citizen, qui contient des données publiques et privées.

Le mot clé friend en C++ a toujours été controversé. On lui reproche de rompre l’encapsulation.

Cet article vise à présenter un autre idiome, moins bien connu, qui permet de limiter la portée d’une déclaration friend. Prenons l’exemple d’une classe Citizen, qui contient des données publiques et privées.

class Citizen {
    public:
    string getName() const;
    
    private:
    string getFavouriteFood() const;
    string getSocialSecurityNumber() const;
    
    string _name;
    string _favouriteFood;
    string _socialSecurityNumber;
};

string Citizen::getName() const {
    return _name;
}

string Citizen::getFavouriteFood() const {
    return _favouriteFood;
}

string Citizen::getSocialSecurityNumber() const {
    return _socialSecurityNumber;
}

Imaginons maintenant la classe Government, qui a besoin d’imprimer le numéro d’assurance sociale d’un Citizen.

class Government {
    private:
    void printCitizenInfo(const Citizen &citizen) const;
};

void Government::printCitizenInfo(const Citizen &citizen) const {
    cout << "Citizen Name: " << citizen.getName() << endl;
    cout << "Citizen SSN: " << citizen.getSocialSecurityNumber() << endl; // COMPILE ERROR
}

Imaginons maintenant une classe Spy, qui cherche à obtenir l’information privée du Citizen.

class Spy {
    private:
    void printCitizenInfo(const Citizen &citizen) const;
};

void Spy::printCitizenInfo(const Citizen &citizen) const {
    cout << "Citizen Name: " << citizen.getName() << endl;
    cout << "Citizen SSN: " << citizen.getSocialSecurityNumber() << endl; // COMPILE ERROR
}

Dans ce scénario, ni l’une ni l’autre des deux classes, Government et Spy, n’arrivent à avoir accès à l’information du Citizen.

Comment donner à la classe Government l’accès à l’information sans la rendre publique? Un moyen efficace, mais maladroit, serait de faire de la classe Government un friend de la classe Citizen.

class Citizen {
    friend class Government;

    public:
    string getName() const;
    
    private:
    string getFavouriteFood() const;
    string getSocialSecurityNumber() const;
    
    string _name;
    string _favouriteFood;
    string _socialSecurityNumber;
};

Maintenant, la classe Government a accès au numéro d’assurance sociale de Citizen, mais pas la classe Spy. L’inconvénient, c’est que Government connaît aussi les préférences alimentaires de Citizen, une violation manifeste de son intimité!

Votre premier réflexe pourrait être d’utiliser des fonctions friend, mais en fait, cela ne résoud pas le problème puisque la fonction friend aura également accès à toute l’information de Citizen, ce qu’on essaie justement d’empêcher.

Une solution serait de repenser les classes comme suit :

class Citizen {
    public:
    string getName() const;
    void getSocialSecurityNumber(Government &government) const;
    
    private:
    string getFavouriteFood() const;
    string getSocialSecurityNumber() const;
    
    string _name;
    string _favouriteFood;
    string _socialSecurityNumber;
};

void Citizen::getSocialSecurityNumber(Government &government) {
    government.setSocialSecurityNumber(*this, _socialSecurityNumber);
}

class Government {
    public:
    void setSocialSecurityNumber(const Citizen &citizen, string socialSecurityNumber);
    
    private:
    void printCitizenInfo(const Citizen &citizen) const;
    string findSocialSecurityNumber(const Citizen &citizen) const;

    map _socialSecurityNumberMap;
};

string Government::printCitizenInfo(const Citizen &citizen) const {
    cout << "Citizen Name: " << citizen.getName() << endl;
    cout << "Citizen SSN: " << findSocialSecurityNumber(citizen) << endl;
}

Maintenant, Citizen donne expressément son numéro d’assurance sociale à la classe Government. La classe Government peut cacher le numéro d’assurance sociale de Citizen dans un tableau associatif, par exemple, et imprimer l’information du Citizen plus tard.

Mais il y a une solution plus élégante pour permettre à la classe Citizen de donner un accès direct à cette information à la classe Government : l’idiome Passkey. Cet idiome permet de restreindre la portée de la déclaration friend à une ou plusieurs méthodes plutôt qu’à l’ensemble de la classe.

template 
class Passkey {
    private:
    friend T;
    Passkey() {}
    Passkey(const Passkey&) {}
    Passkey& operator=(const Passkey&) = delete;
};

class Citizen {
    public:
    string getName() const;
    string getSocialSecurityNumber(Passkey) const;
    
    private:
    string getFavouriteFood() const;
    
    string _name;
    string _favouriteFood;
    string _socialSecurityNumber;
};

class Government {
    private:
    void printCitizenInfo(const Citizen &citizen) const;
};

void Government::printCitizenInfo(const Citizen &citizen) const {
    cout << "Citizen Name: " << citizen.getName() << endl;
    cout << "Citizen SSN: " << citizen.getSocialSecurityNumber(Passkey()) << endl; // THIS COMPILES
}

class Spy {
    private:
    void printCitizenInfo(const Citizen &citizen) const;
};

void Spy::printCitizenInfo(const Citizen &citizen) const {
    cout << "Citizen Name: " << citizen.getName() << endl;
    cout << "Citizen SSN: " << citizen.getSocialSecurityNumber(Passkey()) << endl; // COMPILE ERROR
}

Seule la classe Government peut invoquer le mot de passe : l’implémentation Spy échoue car elle ne peut pas instancier les constructeurs privés Passkey. Seule la classe Government a un accès friend à ces constructeurs privés. Notez qu'autant le constructeur par défaut que le constructeur par copie de Passkey sont utilisés et nécessaires dans l'exemple ci-haut.

L’implémentation de Passkey ci-dessus exige C++11 à cause de la déclaration modèle "friend T". Pour obtenir une classe générique Passkey semblable en C++03, l’on peut employer une solution de contournement à base de macros. Pour obtenir davantage d’information sur cette solution et sur l’application de l’idiome Passkey aux fonctions friend, je vous encourage à lire la discussion suivante, très détaillée, dans StackOverflow:

http://stackoverflow.com/questions/3324898/can-we-increase-the-re-usability-of-this-key-oriented-access-protection-pattern

J’espère que, après avoir lu cet article, vous ne vous sentirez plus coupable de rompre une encapsulation de classe pour une « bonne » raison, car vous pourrez le faire de façon mesurée, et vivre en paix avec la déclaration friend.