Thursday, 31 December 2015

Generic static_cast for classes

If you can static_cast the data members of one class to the corresponding data members of another class, shouldn't you be able to static_cast directly between those classes? I certainly think so, and here I show how to do exactly that.

Motivation


I have two classes that, when simplified, look something like this:
struct float2
{
    float x;
    float y;
};

struct int2
{
    int i;
    int j;
};
These are 2D coordinate classes, one a vector, and the other its integer analogue. To convert from one type to the other, without writing any helper functions, it would look like this:
auto ci = int2{ 1, 2 };
auto cf = float2{
    static_cast<float>(ci.i),
    static_cast<float>(ci.j)
};
That's a mouthful! And it gets worse with the corresponding 3D and 4D types. Wouldn't it be far nicer to be able to write this?
auto ci = int2{ 1, 2 };
auto cf = static_cast<float2>(ci);

Conversion Operator


No problem, I hear you say. Simply write a conversion operator for int2:
    explicit operator float2() const
    {
        return { static_cast<float>(i), static_cast<float>(j) };
    }
Great! Job done, right? Well, only if you don't mind int2 being dependent on float2. And I assume you want a similar conversion operator in float2 so that you can cast from float2 to int2, so I hope you don't mind cyclic dependencies.

Templates


If you are like me, and you have a whole bunch of classes like this, you will want to avoid those pesky dependencies. Sounds like a job for generic programming!
    template<typename T>
    explicit operator T() const
    {
        return T{
            static_cast<decltype(T::x)>(i),
            static_cast<decltype(T::y)>(j)
        };
    }
Well, that's hardly the most generic code; it assumes T has data members named x and y. What if I have another type, short2, which has data members i and j? Maybe we could write a constructor for float2 instead.
    template<typename T>
    float2(T const& other) :
            x(static_cast<float>(other.i)),
            y(static_cast<float>(other.j))
    {
    }
This time we assume that T has data members named i and j, so this is no good either. In fact, this is even less promising than our previous approach; at least that didn't directly reference any data member instances. What we really want is for static_cast to be able to automatically deduce the target type something like this:
    template<typename T>
    explicit operator T() const
    {
        return T{ static_cast<auto>(i), static_cast<auto>(j) };
    }
While this isn't allowed in C++, there is a solution that comes close.

auto_static_cast


GManNickG devised an ingenious auto_cast "operator", which wraps the object under conversion in a "forwarding" proxy object which converts to any type, thereby emulating automatic casting. His auto_cast is actually slightly safer than static_cast, as it disallows downcasting. We want to emulate the behaviour of static_cast, so our version will be slightly modified.
#include <utility>

template<typename T>
class auto_static_cast_proxy;

template<typename T>
auto_static_cast_proxy<T> auto_static_cast(T&&);

template<typename T>
class auto_static_cast_proxy
{
    friend auto_static_cast_proxy auto_static_cast<T>(T&&);

private:
    T&& value;

    auto_static_cast_proxy(T&& value) :
        value(std::forward<T>(value))
    {
    }

public:
    template<typename U>
    operator U() const
    {
        return static_cast<U>(std::forward<T>(value));
    }
};

template<typename T>
auto_static_cast_proxy<T> auto_static_cast(T&& value)
{
    return auto_static_cast_proxy<T>(std::forward<T>(value));
}

The Solution


Using this bit of Voodoo wizardry, let's modify our conversion operator:
    template<typename T>
    explicit operator T() const
    {
        return T{ auto_static_cast(i), auto_static_cast(j) };
    }
And there you have it. Generic static_cast for the masses. Of course, this operator will work on any constructor, not just aggregate initialization; I'm not sure whether this is a good or bad thing, but you could selectively enable the operator using SFINAE (e.g. to only support PODs) if you so wished.

The Better Solution


We can make it even easier to write generic static_cast operators for our types using a simple helper function:
template<typename T, typename... Args>
T static_cast_args(Args&&... args)
{
    return T{ auto_static_cast(std::forward<Args>(args))... };
}
Thus, our operator implementation becomes:
    template<typename T>
    explicit operator T() const
    {
        return static_cast_args<T>(i, j);
    }
Lovely.

1 comment: