Monday, 9 January 2012

Default constructors (4)

Even the exalted STL isn't immune to the allure of default constructors.

In my first post, I mentioned that having a singular state weakens your class invariant, and it does. On the other hand, we have the case where STL explicitly allows singular states in the form of iterators. STL-compatible iterators which are default constructible are assumed to have singular state until assigned to with a non-singular state, before which all operations except assignment and destruction are illegal (from n3242.pdf):

Iterators can also have singular values that are not associated with any sequence. [Example: After the declaration of an uninitialized pointer x (as with int* x;), x must always be assumed to have a singular value of a pointer. —end example] Results of most expressions are undefined for singular values; the only exceptions are destroying an iterator that holds a singular value, the assignment of a non-singular value to an iterator that holds a singular value, and, for iterators that satisfy the DefaultConstructible requirements, using a value-initialized iterator as the source of a copy or move operation.

It is defined this way, of course, because iterators are a generalisation of pointers, which C++ inherited from C, which already work this way. The responsibility for correct iterator usage is then pushed onto iterator users, though considerate iterator authors could provide feedback on incorrect usage as a QOI point.

Still, it does mean that authors have slightly less to worry about, which is good because iterators are hard enough to define correctly at the best of times. And there is a slight efficiency gain to be had where default construction+assignment should be no less efficient than direct assignment; a valid concern in something as fundamental as iteration.

All of this works because C++ programmers are already conditioned to think this way about pointers and there isn't great leap of faith needed to think of iterators in the same way. If you are able to convince your users to think the same way about the classes you provide then perhaps this is another option for you. However, it's a harder sell if you don't have precedent to fall back on.

There is another area of STL where I've seen classes incorrectly crippled with a default constructor, and that's because of std::vector. When faced with a need to reduce the size of a vector, people inevitably reach for resize, which is defined as:

void resize(size_type sz, T c = T());

The defaulted argument is only used when you increase the size of a vector, and specifies the value of the new elements. When you are shrinking the vector, you are having to construct a temporary for no good reason, and that temporary is default constructed if you don't explicitly provide it. You can get away with only creating the temporary when necessary by splitting resize into two overloads:

void resize(size_type sz);
void resize(size_type sz, T c);

Here, the first overload can be written to only default construct a temporary when sz is greater than the vector's current size. That temporary is then used to fill up the space.

However, if your contained class doesn't provide a default constructor, you still won't work. C++ is a statically-typed language and the code still needs to be able to call a default constructor, even if it's not called. Attempting to do so will cause a compile error.

The answer to all of this is not to add a default constructor to your class. The answer is to use erase instead:

// Shrink vec by n:
vec.resize(vec.size() - n);           // Don't do this
vec.erase (vec.end() - n, vec.end()); // Do this instead

No comments:

Post a Comment