tags:

views:

153

answers:

5

I recently came across classes that use a configuration object instead of the usual setter methods for configuration. A small example:

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

struct AConfiguration { int a, b; };

The upsides:

  • You can extend your object and easily guarantee reasonable default values for new values without your users ever needing to know about it.
  • You can check a configuration for consistency (e.g. your class only allows some combinations of values)
  • You save a lot of code by ommiting the setters.
  • You get a default constructor for specifying a default constructor for your Configuration struct and use A(const AConfiguration& conf = AConfiguration()).

The downside(s):

  • You need to know the configuration at construction time and can't change it later on.

Are there more downsides to this that I'm missing? If there aren't: Why isn't this used more frequently?

+1  A: 

Using this method makes binary compatability harder.

If the struct is changed (one new optional field is added), all code using the class might need a recompile. If one new non-virtual setter function is added, no such recompilation is necessary.

gnud
I've recently run into this with an old code-base I'm supporting. It's quite annoying.
Ben S
The Windows API does this just fine by adding a size field, making sure the structs are PODs, and by only appending data fields at the end. Add supporting old constructors to create new structs (with reasonable default values for new fields) and compatibility is actually _increased_.
sbi
I wrote a mirror answer, where I show that compatibility is increased instead.
Pavel Shved
+2  A: 

The main upside is that the A object can be unmutable. I don't know if having the AConfiguration stuct actualy gives any benefit over just an a and a b parameter to the constructor.

Rasmus Kaj
+4  A: 

Using this method makes binary compatibility easier.

When the library version changes and if the configuration struct contains it, then constructor can distinguish whether "old" or "new" configuration is passed and avoid "access violation"/"segfault" when accessing non-existant fields.

Moreover, the mangled name of constructor is retained, which would have changed if it changed its signature. This also lets us retain binary compatibility.

Example:

//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;
     }
   }  
};
Pavel Shved
+6  A: 

Whether you pass the data individually or per struct is a question of style and needs to be decided on a case-by-case basis.

The important question is this: Is the object is ready and usable after construction and does the compiler enforce that you pass all necessary data to the constructor or do you have to remember to call a bunch of setters after construction who's number might increase at any time without the compiler giving you any hint that you need to adapt your code. So whether this is

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

or

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

doesn't matter all that much. (There's a number of parameters where everyone would prefer the former, but which number this is - and whether such a class is well designed - is debatable.) However, whether I can use the object like this

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

or have to do this

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

before I can use the object, does make a huge difference. Especially so, when someone comes along and adds another data field to A.

sbi
A: 

I would support the decreased binary compatibility here.

The problem I see comes from the direct access to a struct fields.

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

Since I modified the representation of b, I am screwed, whereas with:

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

The physical layout of the object might have change, but my getters have not and the offset to the functions have not either.

Of course, for binary compatibility, one should check out the PIMPL idiom.

namespace details { class AConfigurationImpl; }

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

While you do end up writing more code, you have the guarantee here of backward compatibility of your object as long as you add supplementary methods AFTER the existing ones.

The representation of an instance in memory does not depend on the number of methods, it only depends on:

  • the presence or absence of virtual methods
  • the base classes
  • the attributes

Which is what is VISIBLE (not what is accessible).

And here we guarantee that we won't have any change in the attributes. The definition of AConfigurationImpl might change without any problem and the implementation of the methods might change too.

The more code means: constructor, copy constructor, assignment operator and destructor, which is a fair amount, and of course the getters and setters. Also note that these methods can no longer be inlined, since their implementation are defined in a source file.

Whether or not it suits you, you're on your own to decide.

Matthieu M.