Friday, 6 January 2012

Default constructors (2)

As I was saying last post, the provision of a default constructor when a class doesn't naturally lend itself to being configurationless means that you'll end up having to support a weakened invariant which will both make your class more awkward to reason about and harder to maintain.

But sometimes it is necessary for a default constructor to be provided, as jogobom pointed out.

There are APIs out there which rely on being able to default construct objects. Qt has already been mentioned. COM, ATL and MFC are three others. I myself have written a framework in the past which required it, and then ended up having to invent workarounds for classes for which it would be prohibitive to provide one.

No matter how rude it is for those frameworks to require you to bend your classes to their will, and it is undoubtedly rude, the fact remains that your code is going to have to change before theirs does.

What are you able to do about it? Well, it's generally not a good idea to try and force a default state into a class that doesn't want it. What we want is to separate the 'real' class from the singular state. That way, the class can continue to be easily maintained and reasoned about, and the singularity can be solely responsible for ensuring it's not accidentally used.

There are a couple of easy ways of achieving that. First, you might consider using a boost::optional. If you're familiar with C# then it's closely related to a Nullable type. This way, you can defer construction of your 'real' object while the Optional ensure correct access to it.

Another option exists if you already employ the Pimpl idiom. This is the layer of indirection you need between construction of the client-facing object and the real implementation. You just need to defer allocation of the 'impl' and constrain access to it.

Examples coming soon.

Thursday, 5 January 2012

Default constructors

A great many classes I see have default constructors. A great many of those are implemented by zeroing out the class members for them to be 'initialised' properly at a later time.

Let's remind ourselves what constructors are for, from the inventor of C++ himself:

It is the job of every constructor to establish the class invariant, so that every member function can rely on it.

A default constructor is one that takes no arguments. That suggests that there is a state which can be arrived at without user configuration and which doesn't violate the class invariant. And for that to be the case, one of the following must be true:

  • You have a well-defined invariant which allows a naturally 'empty' state.
  • You have a weakened invariant which allows a singular state.

Containers are a good example of the first case. Their invariant is such that an empty container is valid and useful, and it can be arrived at easily without any configuration.

A class which zeroes its members in the constructor and requires a further initialisation step is an example of the second case. The majority of operations that may be done on this post-default-construction, pre-initialisation object are illegal, which makes this default state singular.

If this singular state is so useless, why not avoid it entirely by establishing useful states in all constructors and strengthening your class invariant? If a default constructor isn't capable of achieving this, don't provide one.

More on this to come.

Wednesday, 4 January 2012

Down-to operator

Speaking of pseudo operators, I was amused recently when a colleague introduced me to the 'down-to' operator:

int n = 10;
while (n --> 0)
    printf("%d\n", n);

This time our pseudo operator is constructed from a post-decrement and a greater-than, causing the loop body to be invoked with values of n from 9 down to 0 inclusive.

Entertaining, but not something I'd advocate in real code.

Tuesday, 3 January 2012

Truth

A quick call out to the truthiness operator in C++, Ruby and Javascript (and probably other languages):

bool b = !!obj;

This will convert an object into its logically true or false value. For example, it will convert a pointer to true if it is non-null, or false if it is null. And it will convert a numerical object to true if it is non-zero, or false if it is zero. And similarly for nil/undefined in the aforementioned languages.

Of course, as anyone who knows about tokenisation in these languages will tell you, it's really just the ! operator applied twice. And the ! operator is really the 'falsiness' operator.

But it's a useful construct to learn, and it's the most succinct way of expressing what it does.

Monday, 2 January 2012

Equality

What does it mean for two objects to have the same state?

A naive answer could be that those objects have the same state if they have the same bit pattern in memory. While it's true that having the same bit pattern means objects have the same state, the converse isn't necessarily true. Think about two std::vector<int>s. They compare equal if they have the same contents, but those contents would exist in two different locations in memory, and so be referenced by different pointers inside each std::vector.

Another answer could be that the objects compare equal with ==. Again, that's true, but not every class defines an operator==, nor does it always make sense to provide one. What would it mean to compare two stream objects, for example?

A better way to think about state equality is in terms of substitutability. Wherever an object is constructed in a program, if it can be substituted with another object without changing the behaviour of the program then those objects have the same state.

Sunday, 1 January 2012

Parameterised error handlers

++year;

Last post I suggested using a default value in place of an exception to be returned when the function fails to return its proper result:

type from_ascii(const char* c, type resultOnFailure)
{
    for (int i = 0; i != sizeof(names)/sizeof(names[0]); ++i)
        if (!strcmp(c, names[i]))
            return (type)i;

    return resultOnFailure;
}

This is fine but its usage is limited to those occasions where you have an appropriate default in advance of calling the function. Also, sometimes the construction of the default value is non-trivial and isn't something you want to waste time doing (or worse, have the construction fail) if you don't even end up using it.

Enter parameterised error handling. The idea is to provide is a functor to be invoked when the return value cannot be generated:

type from_ascii(const char* c, std::function<type()> onError)
{
    for (int i = 0; i != sizeof(names)/sizeof(names[0]); ++i)
        if (!strcmp(c, names[i]))
            return (type)i;

    return onError();
}

Now we can do what we like to handle the error. We can throw an exception. We can return the default value we want. We can prompt the user for a value. We can read it from a file. We can log the error or inform the user before doing any of the above, especially if you forward the function arguments to the functor:

type from_ascii(const char* c, std::function<type(const char*)> onError)
{
    for (int i = 0; i != sizeof(names)/sizeof(names[0]); ++i)
        if (!strcmp(c, names[i]))
            return (type)i;

    return onError(c);
}

Calling this function is simply a matter of passing an appropriately-defined functor. C++11's lambda support makes this particularly easy:

auto e = Direction::from_ascii("banana", [](const char* c) {
    Log("Invalid Direction::type name: %s", c);
    return Up;
});

You can define your functor however you like, passing what you think is important, maybe an error code which tells you why the functor got invoked, or a flag which the functor can set to retry the operation.

Finally, if performance is a concern, you can always take the functor as a templated argument, then you can have your cake and eat it:

template <typename ErrorFunc>
type from_ascii(const char* c, ErrorFunc onError)
{
    for (int i = 0; i != sizeof(names)/sizeof(names[0]); ++i)
        if (!strcmp(c, names[i]))
            return (type)i;

    return onError(c);
}

This approach isn't appropriate for everything but it's another feather to your error handling cap.

Saturday, 31 December 2011

Error defaults

The other day, I wrote a function which threw an exception when it failed to convert an ASCII string into an enumerator:

type from_ascii(const char* c)
{
    for (int i = 0; i != sizeof(names)/sizeof(names[0]); ++i)
        if (!strcmp(c, names[i]))
            return (type)i;

    throw conversion_error_exception();
}

Arguably, the use of an exception here is slightly gratuitous. It's how C# does things, and it's not wrong, but it's not usually the best API when we come to use it. And of course there are performance concerns.

You usually see two solutions to this kind of problem, none of which are particularly appealing to me.

First is the 'sentinel value' solution:

enum type
{
    Left,
    Right,
    Up,
    Down,

    Unknown
};

type from_ascii(const char* c)
{
    for (int i = 0; i != sizeof(names)/sizeof(names[0]); ++i)
        if (!strcmp(c, names[i]))
            return (type)i;

    return Unknown;
}

The Unknown state is somewhat untidy, though is fine for enums with limited scope within a program, where the author of the enum is likely to be the only one using it and he knows all the contexts in which an Unknown enumerator needs to be handled. Additionally, there is likely to be no overhead, assuming that the addition of the extra state doesn't require the enum's underlying type to change.

The other popular solution is the pass-by-reference method:

bool from_ascii(const char* c, type& result)
{
    for (int i = 0; i != sizeof(names)/sizeof(names[0]); ++i)
        if (!strcmp(c, names[i]))
        {
            result = (type)i;
            return true;
        }

    return false;
}

Oddly enough, C# also supports this method. However, it's a bad method as you have lost referential transparency and strict value semantics.

Here is another solution. Quite often, I find myself just defaulting an enum variable to a particular state if the conversion fails. So why not just make that part of the function?

type from_ascii(const char* c, type resultOnFailure)
{
    for (int i = 0; i != sizeof(names)/sizeof(names[0]); ++i)
        if (!strcmp(c, names[i]))
            return (type)i;

    return resultOnFailure;
}

This doesn't solve every problem though. Another technique coming soon.