Skip to content

type_traits: fix empty pack indexing#387

Open
benedekkupper wants to merge 1 commit into
intel:mainfrom
benedekkupper:fix-empty-pack-indexing
Open

type_traits: fix empty pack indexing#387
benedekkupper wants to merge 1 commit into
intel:mainfrom
benedekkupper:fix-empty-pack-indexing

Conversation

@benedekkupper

Copy link
Copy Markdown

Compiling with C++26 fails on empty packs with this message:
include/stdx/type_traits.hpp:302:7: error: cannot index an empty pack
302 | using nth_t =
| ^~~~~
The fix is to prioritize __type_pack_element over C++26 pack indexing in
type_traits.hpp, since the builtin evaluates lazily (only when a member is
accessed) while pack indexing is checked eagerly when the class is
instantiated with an empty pack.

Compiling with C++26 fails on empty packs with this message:
include/stdx/type_traits.hpp:302:7: error: cannot index an empty pack
  302 | using nth_t =
      |       ^~~~~
The fix is to prioritize __type_pack_element over C++26 pack indexing in
type_traits.hpp, since the builtin evaluates lazily (only when a member is
accessed) while pack indexing is checked eagerly when the class is
instantiated with an empty pack.

Signed-off-by: Benedek Kupper <kupper.benedek@gmail.com>
@elbeno

elbeno commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Thanks! Can you give an example of code that would fail currently, so that we can have a test?

@benedekkupper

benedekkupper commented Jul 2, 2026

Copy link
Copy Markdown
Author

The context is that I'm using CIB's msg::callback<> with no message type, so msg::message has no fields. I had no issue with this while I was building with C++23, but with an update to C++26, it started failing with the above error message. Here is the full description by Claude:

// Minimal reproduction of the C++26 pack indexing bug in stdx::nth_t.
//
// PROBLEM
// -------
// With C++26 pack indexing (`Ts...[N]`), the compiler eagerly validates
// the expression when the surrounding class template is instantiated, even
// if the alias member is never actually used.  When the pack is empty the
// expression `{}...[I]` is always ill-formed, so instantiating e.g.
// `message<"foo">` (a message with no fields) produces:
//
//   error: cannot index an empty pack
//
// FIX
// ---
// Prefer `__type_pack_element<N, Ts...>` (GCC/Clang builtin) over the
// C++26 pack-indexing syntax.  The builtin is evaluated lazily: it is only
// checked when the alias is actually instantiated with a concrete index,
// so an empty pack in the class body does not trigger an error.
//
// Reproduces with: g++ -std=c++26   (GCC ≥ 15, __cpp_pack_indexing defined)
// Fixed    with:   prefer __type_pack_element over Ts...[N]

#include <cstddef>
#include <type_traits>

// ── nth_t with C++26 pack indexing (BROKEN for empty packs) ──────────────────
template <unsigned int N, typename... Ts>
using nth_t_broken = Ts...[N];   // C++26 pack indexing

// ── nth_t preferring the builtin (FIXED) ─────────────────────────────────────
template <unsigned int N, typename... Ts>
using nth_t_fixed =
#if __has_builtin(__type_pack_element)
    __type_pack_element<N, Ts...>;
#else
    Ts...[N];   // fallback if builtin unavailable
#endif

// ── Class that exposes nth_field_t (mirrors msg::detail::message) ─────────────
template <typename... Fields>
struct message_broken {
    // Eagerly checked when Fields={}: error: cannot index an empty pack
    template <std::size_t I> using nth_field_t = nth_t_broken<(unsigned int)I, Fields...>;
};

template <typename... Fields>
struct message_fixed {
    // Lazily checked; no error when Fields={}
    template <std::size_t I> using nth_field_t = nth_t_fixed<(unsigned int)I, Fields...>;
};

// ── Instantiate with no fields ────────────────────────────────────────────────
// Uncomment the broken line to see the error:
// message_broken<> broken_instance;   // error: cannot index an empty pack

message_fixed<> fixed_instance;        // OK

// ── Instantiate with fields – nth_field_t must still work ─────────────────────
struct FieldA {};
struct FieldB {};
static_assert(std::is_same_v<message_fixed<FieldA, FieldB>::nth_field_t<0>, FieldA>);
static_assert(std::is_same_v<message_fixed<FieldA, FieldB>::nth_field_t<1>, FieldB>);

int main() {}

@elbeno

elbeno commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

I think this is just a GCC bug, right? It's not supposed to instantiate the alias member template when the class template is instantiated. Clang treats this correctly.

@benedekkupper

Copy link
Copy Markdown
Author

I think this is just a GCC bug, right? It's not supposed to instantiate the alias member template when the class template is instantiated. Clang treats this correctly.

Correct, the bug is present up to the latest GCC version, while clang compilation passes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants