When I first started programming C++ in the 1990s, there were language features that I found appalling. One of those was operator overloading, which was used in the most basic of C++ example code, e.g.
cout << "hello, world" << endl;
This made the C programmer in me squirm. Why would you make the meaning of operators change wildly based on the context? Also you might lose track of what code is actually being generated. It could have side effects that you don't know about, or be orders of magnitude slower! I still fill this way, and avoid operator overloading, other than operator=, for most things.
...but having said that, I find operator overloading to be incredibly valuable when it comes to maintaining and refactoring a large code base. A pattern that has become routine for us is a basic type is initially used for some state and needs to be extended.
For example: in track groups, we originally had 32 groups (1-32), and a track could have different types of membership in any number of groups. For 32 groups, we used unsigned ints as bitmasks. Some years later, to support 64 groups we changed it to WDL_UINT64 (aka uint64_t). Straightforward (but actually quite tedious and in hindsight we should've skipped the type change and gone right to the next step). To increase beyond 64 bits, there's no longer a basic type that can be used. So instead:
struct group_membership {
enum { SZ = 2 };
WDL_UINT64 m_data[SZ];
const group_membership operator & (const group_membership &o) const
{
group_membership r;
for (int i = 0; i < SZ; i ++) r.m_data[i] = m_data[i] & o.m_data[i];
return r;
}
// and a bunch of other operators, a couple of functions to clear/set common masks, etc
private:
// prevent direct cast to int/int64/etc. necessary because we allow casting to bool below which would otherwise be quietly promoted to int
operator int() const { return 0; }
public:
operator bool() const { for (int i = 0; i < SZ; i ++) if (m_data[i]) return true; return false; }
};
Then we replace things like:
WDL_UINT64 group;
with
group_membership group;
and after that, (assuming you get all of the necessary operators mentioned in the comment above) most code just works without modification, e.g.:
if (group & group_mask) { /* ... */ }
To be fair, we could easy tweak all of the code that the operator overloading implements to use functions, and not do this, but for me knowing that I'm not screwing up the logic in some subtle way is a big win. And if you have multiple branches and you're worried about merge conflicts, this avoids a lot of them.
Also, it's fun to look at the output of gcc or clang on these things. They end up producing pretty much optimal code, even when returning and copying structs. Though you should be sure to keep the struct in question as plain-old-data (ideally no constructor, or a trivial constructor, and no destructor).
Thus concludes my "a thing I appreciate about C++" talk. Next week, templates.
Recordings:
super8_novideo - 1 -- [9:36]
super8_novideo - 2 -- [4:16]
super8_novideo - 3 -- [4:34]
super8_novideo - 4 -- [8:20]
12 Comments