|
Page 1 of 12 Libzypp/ZYpp Programmers guide This is a recompilation of knowledge and common pitfalls which arose during ZYpp development.
Design decisions A few notes on design decisions and consequences. Being in a hurry it might be tempting to violate them, or weaken them. Don't do it! What looks like a quick result, may easily end in a bunch of workarounds, strange side effects, and finally the wish to correct it and the hindsight, that it's almost impossible. - The worst thing you can do is changing the semantic of an Object, after it's used by the surrounding code. And after the library is released, you can't do it anyway.
- And keep in mind, that an interface is more than just a couple data of functions. It's also a promise on behaviour.
- Using code (and compiler) will not just rely on your functions doing what they promise to do. They will also rely on the promised behaviour of your objects. And this is extremely important if your Objects are used in container classes, algorithms and the interface of other Classes.
This is not C. You can't code functionality and just document the intended usage. You can and must define functionality and behaviour. At first this may sounds like it is more effort. But in contrary to your documentation, the compiler is able to understand your code. If your coding of behaviour is correct, the compiler will prevent others from using your classes in a way it is not intended to be used. It might cost YOU some more time to enable the compiler to do this. But once you did it, the compiler is your and everybody elses coworker. He will remind you, if you do things which can't be done. Long before your testcases fail and you wonder why. 1. Decision is to define the objects responsibility. 2. How should it be used and accessed? - Is it a data type (like Arch, Edition, string, int...)?
Then the object must perform COW (CopOnWrite). (or must be NonCopyable, which is quite strange for a data type, but might be an option until COW is implemented) - Will it appear as 'pointer to something' ( Resolvable (public visible)
+Resolvable::Ptr +Resolvable::constPtr)?
In case 'something' uses a pimpl, you spend 2 reference counter per object. one for the interface class and one for the implementation. Access to the Type will be via _Ptr and _constPtr. As it appears like a pointer, the application will have to use it like a pointer, This incl. test for != NULL before accessing the object. This way it's easy to code 'no Object': ist's a NULL Object::Ptr - Will it appear as 'reference to something' ( SourceImpl (hidden) +Source_Ref +Source_constRef )?
( We currently can't build _constRef but if we will make them available. ) Basically similar to _Ptr, but no reference count for the interface needed, just for the piml. For a _Ref, pimpl has to model an 'ObjectImpl*' doing no COW. (shared_ptr<ObjectImpl>, or intrusive_ptr. RW pointers are NOT appropriate). For a _constRef, pimpl has to model an 'const ObjectImpl*'. AND you MUST spend some effort on coding 'no Object'. It appears as a 'reference to something' so a 'something' must always be available! It's not acceptable for a _Ref type, to have a NULL pimpl. A Reference promisses that there is an Object, and you can access this at any time. - Thus you must be able to provide some 'dummy' Impl object.
- Most probably the default ctor of your Object will refer to this 'dummy' Impl. (be careful not to alter dummy data in the dummy object via non const interface methods).
- You may want to define a static const Object_constRef (that's one reason why we need the _constRef soon. Until this use _ref and place a doxygen /** \todo need _consRef */ comment.)
3. Providing operators People have written books about this. If your class uses pimpl (and it should if it's not a trivial struct) it's the best idea to define and < (if one wants to define them at all) in terms of the pimpl pointer. Same Object, if same implementation object (the same of courses for != <= >....) These operators also affect how your object will behave and how they are treated when used in container classes. Changing the semantic of these operators later, is usually a pretty bad idea. So if you find, that in some or most pieces of your code a different semantic of 'equal' (same name, or same url or same whatever) is needed, then DO NOT consider changing operator==, or implementing some operator== based on non trivial or obvious conditions). Spend a 'normal' method for this, even if typing '==' is shorter. Don't define type conversion operators. (you may use them, if you know why you should not.) NEVER define type conversion to some integral type (like bool). ( in Source_Ref: Conversion via some 'typedef void (Source_Ref::*unspecified_bool_type)();' is the only safe way to implement a conversion to bool to allow pointer style tests for NULL.
) 4. Worry about constness. What's a const in a 'const Source'? For a nontrivial class it's sometimes hard to tell what's const. Source is a container of ResObjects. The containers content ist defined by the Source metadata. const ResStore & Source::resolvables();
At the time this accessor was put into the interface it was 'const' (const ResStore & Source::resolvables() CONST;). Someone removed the const. I can imagine why, but it's wrong. You want and must be able to query for the ResObjects in a Source, even (inf fact especially) if the source is const. You can't airy change the semantic of such an accessor, because it hinds you writing a snippet of code. It breaks (maybe uncommitted) code and hampers writing new correct code. And the code written, demanding a non const accessor is wrong, and so has to be written again. And tested again. That's a fast success, worth nothing. So think twice before changing the semantic of an Object. Maybe the Object is wrong, but it may also be you're doing it the wrong way. Const applies to methods and variables, as well as to the semantic of an object. A const data member of an object can't be changed, but data members of a const object may change. ReferenceCount is the best example. Even if the Object is const, the reference counter have to be adjusted. Basically you have 2 exceptions: - Some data must and may be changed ANY TIME during the objects lifetime.
- Then you declare the data member 'mutable'.
class ReferenceCounted { ... mutable unsigned _counter; };
- Late initialisation of data members (which is what probably applies to Source).
If the first access to some data member is done via a const Object, the data member has to be changed(initialized) before returning it. Tempting to use 'mutable', but that's not correct. Being mutable, the compiler will never complain about attempts to change it. But once initialized, the member is const. If you use mutable, you are responsible for not changing the data. You have to write some comment for your co-workers, that it must not be changed after init, and your co-workers, must read and remember it, when trying to fix something 10 minutes before freeze. That one of the few situations, where it is advisable to have a const method which explicitly casts away constness and calls a nonconst init method: bool init();
bool lateInit() const { return const_cast<Type*>(this)->init(); }
const SomeThing & access() const { if ( ! _initialized ) lateInit(); return _soemthing; }
And don't be fooled by methods you may find somehere doing something like: const SomeThing & access() const { static bool _initialized = lateInit(); return _soemthing; }
Looks like you can avoid the 'if ( ! _initialized )' which has to be performed on any call to access(). But this does NOT work if lateInit() has to initialize nonstatic data. lateInit() will not be called once per object, but once int the lifetime of the application. And please avoid irrelevant consts, esp. in return values of functions and methods. It always looks as if you have forgotten someting. - If you pass or return by value, then write
Type ( a copy )
or const Type & ( a reference but to an immutable object ) const Pathname provideFile() const; that a NOOP, and everybody will wonder if you meant
'const Pathname &' ?
This const has no consequence: Pathname p = provideFile(); const Pathname p = provideFile();
both is legal. The same way it is strange to pass a: provideFile(const unsigned media_nr)
You pass by value! Thus the const is a restriction that applies to your own implementation of provideFile only. In case your implementation gets an additional feature, which allowed to look for alternative media_nr (e.g. 0 for 'any'), you have to create a local copy of the argument (lucky if its a copyable type), or you must change the signature of the interface.
|