DEV Community

Cover image for Replicating Swift's Enumeration Associated Values in C++
Javier Salcedo
Javier Salcedo

Posted on

Replicating Swift's Enumeration Associated Values in C++

Lately I'm working on a Metal engine as a hobby project.
I work with Metal in my day job and it has become my favourite graphics API in terms of developer experience.
At home I wanted the full "Apple Experience" so I decided to use Swift instead of the usual suspects (C/C++, Rust...).

I've never worked with Swift before so it's all new. Coming from C++ I feel a bit constrained from time to time (I still heavily dislike the way structs and classes work), but so far I'd describe it as a "comfy" language. It's easy to work with, the syntax is simple (most of the time), the tooling is pretty good, and it has a lot of syntax sugar.

One example of syntax sugar that I love is they way you can add arbitrary values to enums, officially called Associated Values.
I love it because it allows me to simplify APIs and reduce boiler plate a lot.

Let's see a simplified example from my project:

let PI: Float = 3.1416
let TAU = 2 * PI

struct Vec3 { let x, y, z : Float } // Imagine there's vector math here

enum Axis
{
    case X, Y, Z
    case CUSTOM(Vec3)
}

struct Transform
{
    // ...
    func rotate_around(axis: Axis, radians: Float)
    {
        var the_axis: Vec3
        switch axis
        {
            case .X: the_axis = Vec3(x:1, y:0, z:0)
            case .Y: the_axis = Vec3(x:0, y:0, z:0)
            case .Z: the_axis = Vec3(x:0, y:0, z:1)
            case .CUSTOM(let a): the_axis = a
        }

        print("Rotating \(radians) radians around the(\(the_axis.x), \(the_axis.y). \(the_axis.z)) axis")
        // Do math stuff with the axis
    }
    // ...
}

let t = Transform(/* ctr arguments*/)

t.rotate_around(axis: .X, radians: 0.25 * TAU)
t.rotate_around(axis: .Y, radians: 0.5  * TAU)
t.rotate_around(axis: .Z, radians: 0.75 * TAU)

let my_axis = Vec3(x: 0, y: 0.3333, z: 0.6667)
t.rotate_around(axis: .CUSTOM(my_axis), radians: TAU)
Enter fullscreen mode Exit fullscreen mode

The output of which is:

Rotating 1.5708 radians around the(1.0, 0.0. 0.0) axis
Rotating 3.1416 radians around the(0.0, 0.0. 0.0) axis
Rotating 4.7124 radians around the(0.0, 0.0. 1.0) axis
Rotating 6.2832 radians around the(0.0, 0.3333. 0.6667) axis

Look at how nice and simple the implementation of rotate_around is! ❤️

It was just a matter of time until I encountered something at work that could use a feature like this. And that moment came today.
At work we use C++, and enums there are not this complex, so APIs like this can get a lot of boilerplate to get them working.
But thankfully, this line in Swift's docs made me realise there's a way to get a similar thing working on C++:

Enumerations similar to these are known as discriminated unions, tagged unions, or variants in other programming languages.

Variants!
So I went on and drafted a quick C++ alternative to my Swift code:

#include <iostream>
#include <variant>

constexpr float PI = 3.1416f;
constexpr float TAU = 2 * PI;

struct Vec3 { float x, y, z; }; // Imagine there's vector math here

enum class PredefinedAxis
{
    X, Y, Z
};
using CustomAxis = Vec3;
using Axis = std::variant<PredefinedAxis, CustomAxis>;

struct Transform
{
    // ...
    void rotate_around_axis(Axis axis, float radians)
    {
        Vec3 the_axis;
        if (auto* predefined = std::get_if<PredefinedAxis>(&axis))
        {
            switch (*predefined)
            {
                case PredefinedAxis::X: the_axis = Vec3{.x=1, .y=0, .z=0}; break;
                case PredefinedAxis::Y: the_axis = Vec3{.x=0, .y=1, .z=0}; break;
                case PredefinedAxis::Z: the_axis = Vec3{.x=0, .y=0, .z=1}; break;
            }
        }
        else if (auto* custom = std::get_if<CustomAxis>(&axis))
        {
            the_axis = *custom;
        }
        else
        {
            // We shouldn't get here!!
        }

        std::cout << "Rotating " << radians << " radians around the (" << the_axis.x << ", " << the_axis.y << ", " << the_axis.z << ") axis" << std::endl;
        // Do math stuff with the axis
    }
    // ...
};

auto main() -> int
{
    auto t = Transform{/* ctr arguments */};

    t.rotate_around_axis(PredefinedAxis::X, 0.25 * TAU);
    t.rotate_around_axis(PredefinedAxis::Y, 0.5  * TAU);
    t.rotate_around_axis(PredefinedAxis::Z, 0.75 * TAU);

    auto my_axis = Vec3{.x = 0, .y = 0.3333, .z = 0.6667};
    t.rotate_around_axis(my_axis, TAU);
}
Enter fullscreen mode Exit fullscreen mode

Which outputs:

Rotating 1.5708 radians around the (1, 0, 0) axis
Rotating 3.1416 radians around the (0, 1, 0) axis
Rotating 4.7124 radians around the (0, 0, 1) axis
Rotating 6.2832 radians around the (0, 0.3333, 0.6667) axis

Of course, this being C++ it's more verbose and it feels a bit hacky (especially the using part), but it works!
The implementation of rotate_around_axis is messier, but I actually like the use of it way better!

I look forward to learning more cool features from Swift that I can copy in C++!

Top comments (0)