std::pmr::generator, a generator without heap allocation

Not too long ago I did a talk at our meetup, showing the basic usage of std::generator, comparing it to resumable functions via lambdas and classes.

I mentioned that a std::generator runs on the heap and there is a way to run it on the stack, but did not go into details. Since someone asked me about how to do that, here it is.

C++17 PMR

In brief, std::pmr makes it easier to have various allocator implementations. One allocator strategy can be a stack allocator.

For the context of this post, this info should be sufficient. For more info about pmr allocators, I can recommend this talk on the SwedenCpp Youtube channel.

The std::pmr::generator

C++23 brought us std::generator, and std::pmr::generator as a type alias that can be used with PMR allocators.

namespace pmr {
    template<class Ref, class V = void>
    using generator = std::generator<Ref, V, std::pmr::polymorphic_allocator<>>;
}

The third template parameter tells the coroutine machinery to use PMR for frame allocation. That is the difference from std::generator, and allows the usage of a custom allocator — for example, a stack-based one.

But what about the usage?

When using/creating that generator, the allocator_arg_t convention is the part that is easy to miss and needs some explanation.

There are two arguments which need to be passed before the function arguments.
And the order matters:

  • std::allocator_arg_t must be passed first, indicating that an allocator is following,

  • polymorphic_allocator is the second function parameter.

  • From the third position, the arguments the generator functions itself needs follow.

This convention predates C++23. It is a pattern I have seen in std::tuple and std::packaged_task.
However, the best explanation is a working code example.

A simple example

#include <generator>
#include <memory_resource>
#include <iostream>
#include <array>
#include <ranges>


std::pmr::generator<long> fibonacci(std::allocator_arg_t, (1)
                                    std::pmr::polymorphic_allocator<>,
                                    long base) //: pre (base == 0 || base == 1)
{
    long a = base;
    long b = 1;
    while (true) {
        co_yield a;
        a = std::exchange(b, a + b);
    }
}

int main()
{
    alignas(std::max_align_t) std::array<std::byte, 256> buf; (2)
    std::pmr::monotonic_buffer_resource arena(buf.data(), buf.size(),
                                    std::pmr::null_memory_resource());
    std::pmr::polymorphic_allocator<> alloc{&arena};

    long sum = 0;
    for (long x : fibonacci(std::allocator_arg, alloc, 0) (3)
                | std::views::take(20)) {
        sum += x;
    }

    std::cout << "Sum of first 20 (base 0): " << sum << '\n';
}
1 The generator signature, using std::allocator_arg_t and polymorphic_allocator<> as the first two parameters. Any parameters the function itself needs follow.
2 A 256-byte stack array which feeds a monotonic_buffer_resource. We use null_memory_resource(), which throws std::bad_alloc if the buffer runs out of memory A polymorphic_allocator wraps the resource and is passed to the generator.
3 Using the generator as a range. No surprise here.

No heap

The compiled output contains zero calls to malloc, operator new, or any global allocator. Only the PMR vtable dispatch into the stack arena is present. You can verify this with a similar code snippet here: Godbolt.

The important thing to know is that the coroutine frame is allocated when the generator is constructed, before iteration begins. If the buffer is too small, null_memory_resource() throws at construction time, not during the loop. Which is good.
So keep in mind to use null_memory_resource() since overflow shall fail loudly and does not silently fall back to heap allocation.

Where to read more

As far as I know, at the moment of writing this, std::pmr::generator is not widely documented in this usage. The design for generator is in this proposal paper: P2502. The design paper for pmr::generator is this one: P2787.

Cppreference covers the API but does not prominently explain the allocator convention or warn about the input_range constraint.

I hope this post helps to use std::pmr::generator to your advantage.

Disclaimer

This post was written by a human. AI was used for language polishing.