The Well-Crafted Class in C++: Part 2
- Effective Operator Overloading
- Typecasts and Conversions
- Input and Output
- Efficiency Concerns
- Testing and Debugging
- Documentation
- Summary
In the first article of this series, I talked about some issues surrounding the careful creation of a quality class or library in C++. You looked at constructors, methods and parameter lists, and issues with inheritance. Most of these issues are just good OOP practices mixed with common sense.
You may have noticed that I didn't always separate interface issues from implementation issues. I'm not stressing that separation here, either.
That distinction exists for the library user, to be sure. When I call a method that happens to access an internal data structure, I should not generally care whether that structure is an array or a B-tree. To keep a stable application programming interface (API), we make this separation between interface and implementation. Besides assuring future compatibility, it also prevents our having to change documentation in most cases.
But the programmer who is designing the library doesn't have the luxury of dealing only with the interface. Obviously, the interface has to have an implementation behind it. With that in mind, let's proceed.
Effective Operator Overloading
C++ makes operator overloading possible. Where appropriate, go ahead and use this feature. But when you do so, make sure that you choose the operators in a sensible way, and make sure that there are no "holes" in the set of operators. If it makes sense to define a +, it probably makes sense to define a also (although sometimes it may not). If you define + and -, you probably want to define += and -= also.
Logically, you should avoid repeating code. Define the more complex functions in terms of the simpler ones; for example, let += call the + operator. On the other hand, if you want to avoid the creation of a new object when += is invoked, violate this rule and duplicate the code.
Here's a contrived example:
class Dollar { int _amt, _cents; public: Dollar(amt,cents=0) : _amt(amt), _cents(cents) {} Dollar operator+(Dollar other) { int val = _amt + other._amt; int cval = _cents + other._cents; return Dollar(val,cval); } Dollar operator+=(Dollar other) { _amt += other._amt; _cents += other._cents; return *this; // Avoid creating new object } }
Of course, in such a simple case, code duplication is not a real issue, and here we haven't actually duplicated anything word for word, anyhow. In other cases, the common code might be several lines, in which case you might wish to put it in a common private method, to be called more than once.
If you define ++ and -- operators, make sure that you define both forms (pre- and post-). Make sure also that they behave in the traditional way we expect. The prefix form does the increment before the value is returned; the postfix form saves the value to be returned and then does the increment. There's nothing in C++ to enforce thisyou have to follow the convention consciously.
Here we continue the same example:
class Dollar { // ... Dollar operator++() // prefix { ++_amt; return *this; } Dollar operator++(int) // postfix { int val = _amt++; Dollar obj = Dollar(val); // Or the one-liner: return obj; // return Dollar(_amt++); } }
There might be some operators (such as () and ->) that you rarely want to define. If you simply don't want them to be used, you can always just ignore them. Alternatively, you could raise an exception that explicitly points out that these are not implemented.
If inheritance is an issue, there is a third and better alternative. You could make these virtual functions so that child classes could define them as needed.