How to Generate a Custom Compile-Time Offset Sequence in Modern C++
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:
- Alignment Calculation: Correctly advance the current byte offset to satisfy the alignment requirements (
alignof(T)) of the next type. - Array-to-Sequence Conversion: Calculate the values in a
constexpr std::arrayand expand those values as template arguments intostd::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 constexprarray 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.