5
\$\begingroup\$

I've come up with a type that allows me to encapsulate any container class (that supports std::pmr::polymorphic_allocator<T>) with a buffer and a memory resource object. So it has three member variables. However, one downside of it is that it's by default neither copyable nor movable. So it should better be constructed at the use-site (and not earlier only to be passed around).

I also declared an explicit default constructor to prevent initialization with user-specified arguments since that could lead to errors. The existence of the explicit default constructor causes the trait std::is_aggregate_v to return false which I think is the correct design choice for this struct.

And I chose buffer_resource_container as the name of this generic struct template. I could not find a better name.

Then there is also a utility function template reserve_capacity that takes any std::vector type and calls its reserve function and does not throw in the case of failure.

Here is a minimal example:

#include <concepts>
#include <memory_resource>
#include <array>
#include <vector>
#include <system_error>
#include <new>
#include <stdexcept>
#include <exception>
#include <iostream>
#include <cstddef>


template <class Container, std::size_t DefaultBufferCapacity>
requires ( std::same_as<typename Container::allocator_type, std::pmr::polymorphic_allocator<typename Container::value_type>> )
struct [[ nodiscard ]] buffer_resource_container
{
    alignas ( alignof( typename Container::value_type ) ) std::array<std::byte, DefaultBufferCapacity * sizeof( typename Container::value_type )> buffer;
    std::pmr::monotonic_buffer_resource buffer_resource { std::data( buffer ), std::size( buffer ) };
    Container container { &buffer_resource };

    buffer_resource_container( ) noexcept ( noexcept( Container { &buffer_resource } ) ) = default;
};

template <class ValueType, std::size_t DefaultBufferCapacity>
using buffer_resource_vector = buffer_resource_container<std::pmr::vector<ValueType>, DefaultBufferCapacity>;


template <class ValueType, class Allocator>
[[ nodiscard ]] std::errc inline
reserve_capacity( std::vector<ValueType, Allocator>& vec, const std::size_t new_capacity ) noexcept
{
    try
    {
        vec.reserve( new_capacity );
    }
    catch ( const std::bad_alloc& )
    {
        return std::errc::not_enough_memory;
    }
    catch ( const std::length_error& )
    {
        return std::errc::value_too_large;
    }
    catch ( const std::exception& )
    {
        return std::errc::resource_unavailable_try_again;
    }

    return std::errc { };
}

int main( )
{
    constexpr auto default_capacity { 500uz };
    static buffer_resource_vector<char, default_capacity> brv { };
    auto& buffer { brv.container };
    const auto cap { 200uz };
    const auto err_code { reserve_capacity( buffer, cap ) };

    if ( err_code != std::errc { } )
    {
        std::cout << "An error occurred during buffer allocation:"
                  << std::make_error_condition( err_code ).message( )
                  << "\n";
        return 1;
    }
}

As can be seen, the requires clause of the struct enforces a constraint. What other constraints can be employed? Maybe one that makes sure the container has a constructor that takes a const Allocator& argument (since it is needed in the struct).

The code in main shows how I think buffer_resource_container and reserve_capacity can be used together to improve efficiency by lowering the chances of doing reallocations.


What can be improved here? Are there any issues in the code above? Any suggestions?

\$\endgroup\$
1
  • \$\begingroup\$ For reference: live \$\endgroup\$
    – digito_evo
    Commented Apr 29 at 9:15

1 Answer 1

5
\$\begingroup\$

The container wrapper is pretty nice. There are quite a few cases where the ideal container is something like a static_vector or inplace_vector. But instead of hardcoding the container type, you made this generic. There are some issues though:

It's not as convenient as a regular container

Ideally, you'd be able to write something like:

static buffer_resource_vector<int, 10> bvr{};
bvr.push_back(42);

However, you require that one writes brv.container.push_back(42). You can get the convenience of the above code if you make buffer_resource_container inherit from Container:

template <class Container, std::size_t DefaultBufferCapacity>
requires (std::same_as<typename Container::allocator_type, std::pmr::polymorphic_allocator<typename Container::value_type>>)
struct [[nodiscard]] buffer_resource_container: public Container
{
    alignas(typename Container::value_type) std::array<std::byte, DefaultBufferCapacity * sizeof(typename Container::value_type)> buffer;
    std::pmr::monotonic_buffer_resource buffer_resource {std::data(buffer), std::size(buffer)};

    buffer_resource_container() noexcept(…): Container{&buffer_resource} {}
};

You might want to add a get_container() convenience function to allow easy access to just the container part:

Container& get_container() {
    return *static_cast<Container*>(this);
}

You could also make it easier to write the desired type, consider:

template<template<class, class> class Container, class T, std::size_t N>
class Inplace: public Container<T, std::pmr::polymorphic_allocator<T>> {
    using container_type = Container<T, std::pmr::polymorphic_allocator<T>>;
    using value_type = container_type::value_type;
    alignas(value_type) std::array<std::byte, N * sizeof(value_type)> buffer;
    std::pmr::monotonic_buffer_resource buffer_resource{std::data(buffer), std::size(buffer)};

public:
    Inplace() noexcept(…): container_type{&buffer_resource} {}
};

This allows you to write:

Inplace<std::vector, int, 10> bvr;
bvr.push_back(42);

About reserve_capacity()

I don't think this function is very useful. It's only working for std::vectors, but ideally you want something that works on any kind of container, and maybe for more than just reserve(). You can make a generic wrapper function or class that can take any function and when calling it, catches those exceptions and converts them into a std::errc, although it's a bit of work and maybe less pretty.

[…] reserve_capacity can be used […] to improve efficiency by lowering the chances of doing reallocations.

But with a std::pmr::monotonic_buffer_resource you typically use it with a large enough buffer to begin with, so you won't have reallocations at all, so reserve() would be useless. It could still give a very small performance benefit as adding elements to the container within the reserved size would not have to call into the allocator, but I'm not sure it is worth it. I would only do this is benchmarking and profiling the code tells you that this is really problematic.

If you reserve more than the initial buffer size, then what is the point of using a std::pmr::monotonic_buffer_resource to begin with? At least when combining this with a std::vector, as both the monotonic buffer resource and the vector will grow their allocations geometrically.

\$\endgroup\$
11
  • 1
    \$\begingroup\$ Standard advice is not to subclass standard library containers - could you elaborate on why it's not harmful in this case? \$\endgroup\$ Commented Apr 29 at 15:41
  • 1
    \$\begingroup\$ "But with a std::pmr::monotonic_buffer_resource you won't have reallocations at all, and the memory is already there" No, actually the user can request more capacity than what is already available in the buffer member. The container will switch to dynamic allocation in this case. This means the call to reserve can throw. Hence I put it in the function reserve_capacity that catches the exceptions. \$\endgroup\$
    – digito_evo
    Commented Apr 29 at 16:28
  • \$\begingroup\$ @TobySpeight It's a good point. Nothing is virtual so we don't have to worry about the STL container's destructors not being virtual. Another issue is slicing, which is not a problem when accessing the container. Maybe there is a scenario where copying the sliced container is problematic? \$\endgroup\$
    – G. Sliepen
    Commented Apr 29 at 17:30
  • 1
    \$\begingroup\$ @digito_evo Oops, you are right. I was assuming you'd use a std::pmr::null_memory_resource as the upstream allocator. \$\endgroup\$
    – G. Sliepen
    Commented Apr 29 at 17:33
  • 3
    \$\begingroup\$ Nice. Consider creating a new Code Review question with the updated code. \$\endgroup\$
    – G. Sliepen
    Commented Apr 30 at 5:14

Not the answer you're looking for? Browse other questions tagged or ask your own question.