2009-11-12 6 views
5

Je suis récemment tombé sur des classes qui utilisent un objet de configuration au lieu des méthodes de configuration habituelles pour la configuration. Un petit exemple:Structure structs vs setters

class A { 
    int a, b; 
public: 
    A(const AConfiguration& conf) { a = conf.a; b = conf.b; } 
}; 

struct AConfiguration { int a, b; }; 

Les bons côtés:

  • Vous pouvez étendre votre objet et facilement garantir des valeurs par défaut raisonnables pour de nouvelles valeurs sans vos utilisateurs jamais besoin de savoir.
  • Vous pouvez vérifier la cohérence d'une configuration (par exemple, votre classe n'autorise que certaines combinaisons de valeurs)
  • Vous économisez beaucoup de code en désactivant les setters.
  • Vous obtenez un constructeur par défaut pour spécifier un constructeur par défaut pour votre structure de configuration et utiliser A(const AConfiguration& conf = AConfiguration()).

L'inconvénient (s):

  • Vous devez connaître la configuration au moment de la construction et ne peut pas changer ultérieurement.

Y a-t-il d'autres inconvénients à cela qui me manquent? S'il n'y en a pas: Pourquoi cela n'est-il pas utilisé plus fréquemment?

Répondre

6

Si vous passez les données individuellement ou par struct est une question de style et doit être décidée au cas par cas. La question importante est la suivante: L'objet est-il prêt et utilisable après la construction et le compilateur applique-t-il que vous transmettez toutes les données nécessaires au constructeur ou devez-vous rappeler d'appeler un groupe de setters après la construction? augmenter à tout moment sans que le compilateur ne vous donne d'indication que vous avez besoin d'adapter votre code.Donc, que ce soit

A(const AConfiguration& conf) : a(conf.a), b(conf.b) {} 

ou

A(int a_, int b_) : a(a_), b(b_) {} 

n'a pas d'importance tant que ça. (Il y a un certain nombre de paramètres où tout le monde préférerait l'ancien, mais quel numéro cela est - et si une telle classe est bien conçu - est discutable.) Cependant, si je peux utiliser l'objet comme celui-ci

A a1(Configuration(42,42)); 
A a2 = Configuration(4711,4711); 
A a3(7,7); 

ou doivent faire

A urgh; 
urgh.setA(13); 
urgh.setB(13); 

avant de pouvoir utiliser l'objet, ne faire une énorme différence. Surtout quand quelqu'un vient et ajoute un autre champ de données à A.

1

L'utilisation de cette méthode rend la compatibilité binaire plus difficile.

Si la structure est modifiée (un nouveau champ facultatif est ajouté), tout le code utilisant la classe peut nécessiter une recompilation. Si une nouvelle fonction de setter non virtuelle est ajoutée, aucune recompilation n'est nécessaire.

+0

J'ai récemment rencontré ceci avec une ancienne base de code que je soutiens. C'est assez énervant. –

+0

L'API Windows fait cela très bien en ajoutant un champ de taille, en s'assurant que les structures sont des POD, et en ajoutant seulement des champs de données à la fin. Ajoutez les anciens constructeurs de support pour créer de nouvelles structures (avec des valeurs par défaut raisonnables pour les nouveaux champs) et la compatibilité est réellement _increased_. – sbi

+0

J'ai écrit une réponse miroir, où je montre que la compatibilité est augmentée à la place. –

2

Le principal avantage est que l'objet A peut être non modifiable. Je ne sais pas si le fait d'avoir le paramètre AConfiguration donne un avantage sur un simple paramètre a et b au constructeur.

4

L'utilisation de cette méthode facilite la compatibilité binaire. Lorsque la version de la bibliothèque change et que la configuration struct la contient, le constructeur peut distinguer si la configuration «ancienne» ou «nouvelle» est passée et éviter «violation d'accès»/«segfault» lors de l'accès à des champs inexistants.

De plus, le nom mutilé du constructeur est conservé, ce qui aurait changé s'il changeait de signature. Cela nous permet également de conserver la compatibilité binaire.

Exemple:

//version 1 
struct AConfiguration { int version; int a; AConfiguration(): version(1) {} }; 
//version 2 
struct AConfiguration { int version; int a, b; AConfiguration(): version(2) {} }; 

class A { 
    A(const AConfiguration& conf) { 
    switch (conf.version){ 
     case 1: a = conf.a; b = 0; // No access violation for old callers! 
     break; 
     case 2: a = conf.a; b = conf.b; // New callers do have b member 
     break; 
    } 
    } 
}; 
0

Je soutiendrais la compatibilité binaire diminuée ici.

Le problème que je vois vient de l'accès direct à un champs struct.

struct AConfig1 { int a; int b; }; 
struct AConfig2 { int a; std::map<int,int> b; } 

Depuis que je modifié la représentation de b, je suis vissé, alors qu'avec:

class AConfig1 { public: int getA() const; int getB() const; /* */ }; 
class AConfig2 { public: int getA() const; int getB(int key = 0) const; /* */ }; 

La disposition physique de l'objet pourrait avoir le changement, mais mes getters pas et le décalage des fonctions pas non plus.

Bien sûr, pour une compatibilité binaire, il faut vérifier l'idiome PIMPL.

namespace details { class AConfigurationImpl; } 

class AConfiguration { 
public: 
    int getA() const; 
    int getB() const; 
private: 
    AConfigurationImpl* m_impl; 
}; 

Pendant que vous finissez par écrire plus de code, vous avez la garantie ici de rétrocompatibilité de votre objet aussi longtemps que vous ajoutez des méthodes complémentaires APRÈS celles qui existent déjà.

La représentation d'une instance dans la mémoire ne dépend pas du nombre de méthodes, il ne dépend que:

  • la présence ou l'absence de méthodes virtuelles
  • les classes de base
  • les attributs

Ce qui est ce qui est VISIBLE (pas ce qui est accessible).

Et ici nous garantissons que nous n'aurons aucun changement dans les attributs. La définition de AConfigurationImpl peut changer sans problème et l'implémentation des méthodes peut également changer. Plus le code signifie: constructeur, constructeur de copie, opérateur d'affectation et destructeur, ce qui est une bonne quantité, et bien sûr les getters et les setters. Notez également que ces méthodes ne peuvent plus être intégrées, car leur implémentation est définie dans un fichier source.

Que cela vous convienne ou non, vous êtes libre de décider.