Home > Software engineering >  Compile Time Union Array
Compile Time Union Array

Time:11-04

I am trying to keep track of segments of varying size within a contiguous block of memory. The size and order of the segments are known at compile time so I would like to calculate their offsets within the block at compile time as well. I would like to be able to be able to iterate through these segments sizes and offsets but I would also like to be able to call upon specific segments by name when I need to. In case the size, order, or number of segments changes throughout the projects construction I would like to minimize redundancy so there are less parts to adjust/reorganize.

As an example I have one block of memory that can contain a maximum of 12 object instances (all of the same type). For the sake of this example lets say its 12 triangle instances with each triangle instance being comprised of 3 2D coordinates (6 floats total). In this case each segment would represent some geometry, the first segment could just be a triangle (instance count 1), the second segment a polyhedron (lets stay 10 triangle instances), and the third segment is a square (instance count 2). The geometries will be drawn in the order they are laid out in the block of memory (first/triangle, second/polyhedron and third/square segment in that order). The offset of the first segment, the triangle, would be 0. The offset of the second segment, the polyhedron, would be 1 (0 size of triangle). The offset of the third segment, the square, would be 11 (0 size of triangle size of polyhedron). Now if I wanted to draw all of the geometry it would be nice to be able to iterate through an array of sizes and an array of offsets to tell the gpu where the geometry begins (offset) and for how long it goes (size). On the other hand if I wanted move the polyhedron geometry to the left it would be nice to be able to be able to reference the second segment / polyhedron size or offset by name, that way if I decide to change the order (such as make the polyhedron the first geometry to be drawn) I can rearrange the lineup but not have to remember to change all of the calls to the polyhedron offsets and size to its new index.

My initial thought is to have an enum representing the order of the segments (triangle, then polyhedron, then square). I could then make a union between a struct with named integer variables representing individual segment sizes that I can call by name and an array of integers that I could iterate through. The result was the following:

struct Memory {
    struct Enum {
        enum {triangleSegment, polyhedronSegment, squareSegment, count};
    };
    union SegmentSizes{

        struct Names {
            constexpr Names() = default;
            const uint8_t triangleSegment = 1, polyhedronSegment = 10, squareSegment = 2;
        };

        Names names;
        std::array<uint8_t, Enum::count> arr;
    };

    static constexpr SegmentSizes sizes{ SegmentSizes::Names() };

};

int main() {
    std::cout << std::to_string(Memory::sizes.names.triangleSegment) << std::endl;
    std::cout << std::to_string(Memory::sizes.arr[1]) << std::endl;
}

The above works... depending on the compiler. MSVC compiles it fine, but gcc seems to not like the static constexpr SegmentSizes sizes{ SegmentSizes::Names() }; line.

My idea for calculating the offsets at compile time is to construct an array of the offsets using a constexpr function, and then fill another static constexpr union with the results as such:

//Declared within Memory, below static constexpr SegmentSizes sizes
struct SegmentOffsets {
    static constexpr std::array<uint16_t, Enum::count> arr() {
        std::array<uint16_t, Enum::count> a{};
        uint16_t currOffset = 0;
        for (uint8_t i = 0; i < Enum::count;   i) {
            a[i] = currOffset;
            currOffset  = sizes.arr[i];
        }
        return a;
    }

    struct Names {
        const uint16_t triangleSegment, polyhedronSegment, squareSegment;
    };

    static constexpr Names names{ *reinterpret_cast<Names*>(arr().data()) };
};



But for a reason I don't understand the compiler claims that the arr() function is not a compile time constant.

Beyond trying to fine tune my understanding of constexpr and compile time constants a little more I am also wondering if there is simply an easier way in general to accomplish the same goal. I'm open to all suggestions.

For completeness below is the entire code snippet:

#include <array>
#include <iostream>
#include <string>


struct Memory {
    struct Enum {
        enum {triangleSegment, polyhedronSegment, squareSegment, count};
    };
    union SegmentSizes{

        struct Names {
            constexpr Names() = default;
            const uint8_t triangleSegment = 1, polyhedronSegment = 10, squareSegment = 2;
        };

        Names names;
        std::array<uint8_t, Enum::count> arr;
    };

    static constexpr SegmentSizes sizes{ SegmentSizes::Names() };

    struct SegmentOffsets {
        static constexpr std::array<uint16_t, Enum::count> arr() {
            std::array<uint16_t, Enum::count> a{};
            uint16_t currOffset = 0;
            for (uint8_t i = 0; i < Enum::count;   i) {
                a[i] = currOffset;
                currOffset  = sizes.arr[i];
            }
            return a;
        }

        struct Names {
            const uint16_t triangleSegment, polyhedronSegment, squareSegment;
        };

        static constexpr Names names{ *reinterpret_cast<Names*>(arr().data()) };
    };
};


int main() {
    std::cout << std::to_string(Memory::sizes.names.triangleSegment) << std::endl;
    std::cout << std::to_string(Memory::sizes.arr[1]) << std::endl;
}

CodePudding user response:

arr() function is not a compile time constant.

Reading from a union can only be done on the active field of a union, doing otherwise is Undefined Behavior. arr() was always broken, it's just caught when executed during compilation.

Otherwise, the whole approach can be made a lot simpler by replacing the named access with a simple function call:

#include <array>
#include <iostream>
#include <string>

template<std::size_t N>
constexpr std::array<uint16_t, N> calc_offsets(const std::array<uint8_t, N>& sizes) {
    std::array<uint16_t, N> result;
    uint16_t accum = 0;
    std::size_t i = 0;
    for(const auto & size : sizes) {
      result[i  ] = accum;
      accum  = size;
    }
    return result;
}

struct Memory {
    enum Segment {firstGroup, secondGroup, thirdGroup, segment_count};

    static constexpr std::array<uint8_t, (std::size_t)segment_count> segment_sizes = {1, 10 , 1};
    static constexpr std::array<uint16_t, (std::size_t)segment_count> segment_offsets = calc_offsets(segment_sizes);

    static constexpr uint8_t size(Segment s) {return segment_sizes[(std::size_t)s];}
    static constexpr uint16_t offset(Segment s) {return segment_offsets[(std::size_t)s];}
};


int main() {
    std::cout << std::to_string(Memory::size(Memory::firstGroup)) << std::endl;
    std::cout << std::to_string(Memory::segment_offsets[1]) << std::endl;
}
  • Related