Introduction

In low-level systems programming, serialization frameworks, and custom memory allocators, calculating struct-like byte offsets for a parameter pack at compile time is a common requirement. While std::index_sequence typically generates contiguous sequences like 0, 1, 2, 3... via std::make_index_sequence, C++ allows you to create arbitrary, custom std::index_sequence (or std::integer_sequence) values representing byte offsets that account for both sizeof and alignof.

In this guide, we will explore how to compute aligned offsets at compile time and map them directly into a std::index_sequence<Offsets...> in Modern C++ (C++17 and C++20).

The Core Challenge

To produce an offset sequence, we need two components:

  1. Alignment Calculation: Correctly advance the current byte offset to satisfy the alignment requirements (alignof(T)) of the next type.
  2. Array-to-Sequence Conversion: Calculate the values in a constexpr std::array and expand those values as template arguments into std::index_sequence<Offsets...>.

Step 1: Calculating Aligned Offsets

First, let's write a constexpr helper function to align an offset to a given boundary:

constexpr std::size_t align_to(std::size_t offset, std::size_t alignment) {
    return (offset + alignment - 1) & ~(alignment - 1);
}

Next, we can compute an array of offsets for any parameter pack TArgs...:

#include <array>
#include <cstddef>
#include <utility>

template <typename... TArgs>
constexpr auto calculate_offsets() {
    constexpr std::size_t count = sizeof...(TArgs);
    std::array<std::size_t, count> offsets{};
    
    if constexpr (count > 0) {
        std::size_t current_offset = 0;
        std::size_t index = 0;

        auto process_type = [&](std::size_t size, std::size_t alignment) {
            current_offset = align_to(current_offset, alignment);
            offsets[index++] = current_offset;
            current_offset += size;
        };

        (process_type(sizeof(TArgs), alignof(TArgs)), ...); // Fold expression
    }
    
    return offsets;
}

Step 2: Converting std::array to std::index_sequence

To transform our constexpr std::array into std::index_sequence<Offsets...>, we use a helper template that unpacks indices via std::make_index_sequence:

namespace detail {
    template <const auto& Arr, std::size_t... Indices>
    constexpr auto array_to_sequence_impl(std::index_sequence<Indices...>) {
        return std::index_sequence<Arr[Indices]...>{};
    }
}

template <typename... TArgs>
struct offset_sequence_builder {
    static constexpr auto offsets = calculate_offsets<TArgs...>();
    
    using type = decltype(detail::array_to_sequence_impl<offsets>(
        std::make_index_sequence<sizeof...(TArgs)>{}
    ));
};

template <typename... TArgs>
using make_offset_sequence = typename offset_sequence_builder<TArgs...>::type;

Complete Working Example

Here is a complete, self-contained example showing how to create and consume the custom offset sequence:

#include <iostream>
#include <utility>
#include <array>
#include <cstddef>

constexpr std::size_t align_to(std::size_t offset, std::size_t alignment) {
    return (offset + alignment - 1) & ~(alignment - 1);
}

template <typename... TArgs>
constexpr auto calculate_offsets() {
    constexpr std::size_t count = sizeof...(TArgs);
    std::array<std::size_t, count> offsets{};
    
    if constexpr (count > 0) {
        std::size_t current_offset = 0;
        std::size_t index = 0;
        auto process_type = [&](std::size_t size, std::size_t alignment) {
            current_offset = align_to(current_offset, alignment);
            offsets[index++] = current_offset;
            current_offset += size;
        };
        (process_type(sizeof(TArgs), alignof(TArgs)), ...);
    }
    
    return offsets;
}

namespace detail {
    template <const auto& Arr, std::size_t... Indices>
    constexpr auto array_to_sequence_impl(std::index_sequence<Indices...>) {
        return std::index_sequence<Arr[Indices]...>{};
    }
}

template <typename... TArgs>
struct offset_sequence_builder {
    static constexpr auto offsets = calculate_offsets<TArgs...>();
    
    using type = decltype(detail::array_to_sequence_impl<offsets>(
        std::make_index_sequence<sizeof...(TArgs)>{}
    ));
};

template <typename... TArgs>
constexpr auto make_offset_sequence() {
    return typename offset_sequence_builder<TArgs...>::type{};
}

template <std::size_t... Offsets>
void use_offset_sequence(std::index_sequence<Offsets...>) {
    std::cout << "Generated Offsets: ";
    ((std::cout << Offsets << " "), ...);
    std::cout << std::endl;
}

int main() {
    // Assuming standard 64-bit platform: int (4), double (8), void* (8)
    auto seq = make_offset_sequence<int, double, void*>();
    
    // Output: Generated Offsets: 0 8 16
    use_offset_sequence(seq);
    
    return 0;
}

Key Takeaways

  • C++17 Fold Expressions allow clean and concise sequential processing without needing complex recursive template metaprogramming.
  • By passing a static constexpr array reference as a non-type template parameter (NTTP), you can unpack calculated runtime-style values into template arguments.
  • The resulting std::index_sequence<Offsets...> is fully resolved at compile time with zero runtime overhead.