cpp_weekly icon indicating copy to clipboard operation
cpp_weekly copied to clipboard

NTuples: named tuples with compile-time field names and strong typing

Open RPeschke opened this issue 4 months ago • 2 comments

C++Weekly

NTuples Library

Hello Jason Turner,

I’ve been working on a small C++ library called NTuples that provides named tuples with compile-time field names and strong typing. It integrates seamlessly with ranges and structured bindings, and allows both predefined field names or creating them on the fly.

Here’s a minimal example:

for (auto &&e : std::views::iota(1, 10)
              | std::views::transform([](auto i) {
                  return nt::ntuple(
                    index = i,
                    index_squared = i * i,
                    nt_field(cubed) = int(i * i * i), // names on the fly
                    nt_field(str_conversion) =
                        "this is the string number " + std::to_string(i)
                  );
                })
              | std::views::filter([](auto &&t) {
                  return t.cubed >= 216; // filter on named field
                }))
{
    // Access by name
    std::cout << e.index << "   ";
    std::cout << e.index_squared.v << "   ";
    std::cout << e.cubed << "   ";

    // Access by index (like std::tuple)
    std::cout << nt::get<3>(e) << "   ";

    // Print entire ntuple
    std::cout << e << std::endl;

    // Structured bindings also work
    auto [index, index_squared, cubed, str_conversion] = e;
}

With NTuple, you can:

Use predefined names with nt_new_field_name(index); for consistency across a project,

Or create ad-hoc names on the fly with nt_field(cubed), which is great for prototyping or quick transformations.

The library is header-only, constexpr-friendly, and easy to install via vcpkg:

vcpkg install rp-ntuples

Additional resources:

📄 More detailed documentation is available here: NTuple PDF

💻 An example repository that uses the library can be found here: ntuples-test

It might make for an interesting C++ Weekly episode on how modern C++ metaprogramming can turn tuples into something expressive and practical.

Best regards,

Length

around 10 minutes

RPeschke avatar Sep 08 '25 18:09 RPeschke

In which scenarios would this be useful over simply defining your own struct?

AmmoniumX avatar Sep 23 '25 02:09 AmmoniumX

Thanks for the question!

NTuple is similar to an ordinary tuple in that it’s a generic container, but it differs from a plain struct or std::tuple in several key ways:

  • No upfront definition needed, ntuples can be created on the fly without declaring a struct type first.
  • Compile-time reflection, each ntuple “knows” what fields it has and their names.
  • Fields carry their identity, an individual field remembers its own name, so reusing it in another ntuple automatically keeps the same identity.

For example:

auto nt1 = nt::ntuple(
    nt_field(a) = 1,
    nt_field(b) = 2.0,
    nt_field(c) = std::string("hello")
);

auto nt2 = nt::ntuple(
    nt1.a,             // reuses the field "a" from nt1
    nt_field(b) = 3.0 
);

Here, nt1.a is reused in nt2, and the new ntuple automatically retains the correct field name.

This means you can also predefine certain names that should stay consistent across an entire project. It further allows you to define comparison operators that, for example, only look at the shared fields in two ntuples.

Data-frame–like structures

The mechanism extends beyond single ntuples into data-frame–like structures. The example repository includes a vector_frame class, which is essentially a std::vector of ntuples with extra functionality.

For instance:

auto vf1 = nt::fill_vector_frame(10, 
    [](auto i) {
        return nt::ntuple(
            index = i,
            index_squared = i * i,
            nt_field(cubed) = (double) i * i * i
        );
    });

A vector_frame can be printed directly, but it also provides column access: each field of the underlying ntuples can be viewed as an array (nt::span).

Example:

for (auto &&e : vf1.cubed() ) {
    std::cout << e << std::endl;
}

This is especially useful when combining with algorithms that don’t know about your ntuple types. Suppose you have a generic average function:

double average(const nt::span<double>& data);

You can call it directly on a column:

std::cout << "Average: "
          << average(vf1.cubed.get_primitive())
          << std::endl;

This way you get the benefits of named tuple–like objects in your code, while still being able to “fall back” to primitive arrays when needed.

Named function arguments

In addition, NTuple can also be used to implement named function arguments with defaults and compile-time checking:

template <typename... ARGS>
void my_function(ARGS &&...args)
{
    auto t0 = bind_args(
        nt_field(argument1) = 1,
        nt_field(argument2) = 15,
        argument3 = nt::required<int>(),   // required
        argument4 = std::string("hello")   // default
    )(args...);

    std::cout << t0 << std::endl;
}

This function defines four arguments inside bind_args. Only argument3 is required; the others have defaults.

It can be called like this:

void function_with_named_arguments() {
    my_function(
        6,                   // positional argument → argument1
        argument3 = 15,      // required argument (out of order is fine)
        nt_field(argument2) = 42
        // argument4 is not provided → default "hello"
    );
}

If you call the function with the wrong arguments, or forget to set argument3, an error will be issued. For functions with many arguments, this pattern makes calls safer and more readable.

If you have any further questions, please let me know.

RPeschke avatar Sep 23 '25 13:09 RPeschke