Compile Time Swizzling

Published 2020-10-25 C++SugarTemplates Code is not tested and omits many details, intentionally or not.

Motivation

In GLSL code, swizzling offers a handy syntax to manipulate vectors. Inside a cubemap vertex shader for instance, you may use it like this:

vec3 reverse_z = vec3(model_position.xy, -model_position.z);
vec4 projected = camera_proj * vec4(mat3(camera_view) * reverse_z, 1.0);
gl_Position = projected.xyww;

Solutions were found to reproduce this in C++, that each have their shortcomings.

glm::swizzle(vec, glm::X, glm::Y, glm::Z); // at run time
glm::swizzle<glm::X, glm::Y, glm::Z>(vec); // at compile time

Unsatisfied with the previous, I put my two cents on the question and settled on a tradeoff using vec["xyz"]. The underlying code has some limitations, but nice properties too:

Vec<int, 4> constexpr sequence{1, 4, 9, 16};
Vec<int, 4> constexpr reversed = sequence["wzyx"];
std::cout << sequence << reversed;

// Outputs:
// 1 4 9 16
// 16 9 4 1

A Simple Vector Class

Let's start off by building a simple Vec class template with a constructor and a stream output operator. The template parameters will be its Type and Size, mimicking std::array; in fact, I will use an std::array to store the vector coefficients:

template<class Type, std::size_t Size>
class Vec {
public:
    /* ... */

private:
    std::array<Type, Size> coefficients_;
};

Ideally, the constructor should support list initialization as advertised above. We can use a contructor template that matches anything, and then constrains the type of arguments:

template<class... Args>
requires (std::is_convertible_v<Args, Type> && ...)
constexpr Vec(Args&&... args) :
    coefficients_{std::forward<Args>(args)...}
{}

Realistic code would also check for std::is_nothrow_convertible and mark the constructor noexcept accordingly. Interestingly, std::is_constructible would always be false here, as arrays do not have constructors. In order to support aggregate initialization, std::array has a single public data member, the underlying raw array; that's another, simpler possibility.

The output stream operator simply folds over the pack of coefficients provided by std::apply:

friend std::ostream& operator<<(std::ostream& os, Vec const& vec) {
    std::apply([&os](auto const&... coefficient) {
        ((os << coefficient << ' '), ...) << '\n';
    }, vec.coefficients_);
    return os;
}

Another version that does not print final separation characters can be found on cppreference.

Swizzling Operator

To swizzle using a string, we must first transform characters into their corresponding axis; a simple switch statement will do. For brevity, I shrunk the number of lines and omitted [[fallthrough]] but admittedly, this utility function could be more legible.

constexpr std::size_t axisIndex(char axis) {
    switch (axis) {
        case 'x': case 'r': case 's': case 'u': return 0;
        case 'y': case 'g': case 't': case 'v': return 1;
        case 'z': case 'b': case 'p':           return 2;
        case 'w': case 'a': case 'q':           return 3;
    }
    return 0;
}

The swizzling operator takes strings as char array references so that their length can be deduced as template parameter N. It returns a vector holding the same Type with length N - 1, because of the null termination character.

template<std::size_t N>
constexpr auto operator[](char const (&axes)[N]) const {
    return [this, &axes]<std::size_t... I>(std::index_sequence<I...>) {
        return Vec<Type, N - 1>{coefficients_.at(axisIndex(axes[I]))...};
    }(std::make_index_sequence<N - 1>{});
}

Done! The templated lambda deduces a pack of std::size_t from its parameter, and is invoked right away on an std::index_sequence instance. Inside the lambda, we iterate over the characters and fetch their corresponding axis index.

One caveat: besides using at(), there is no bounds checking on the axes...

What About Assignment?

This swizzling operator quickly shows its limits as it only copies the subscripted coefficients into a new Vec. Enabling assignment is actually straightforward: the swizzling operator should form an std::tuple of references to coefficients when called on non-const instances. Then in a wrapper class, you can add proper assignment operators and reach a working prototype in no time. However, a number of subtleties - mostly related to move semantics - have to be taken into account, leading to rather lenghty code. Also, rebind vs. assign-through is a consideration.

Suggestions welcome! Header file here: swizzling.h.