How to Compose Allocator-aware Types¶
Suppose we want to make an allocator-aware type trip_descriptor to describe a booked airline flight.
To build our type, we’ll use the allocator-aware container classes already provided by the standard library. The data members of our class might look something like this
class trip_descriptor { public: // ... int trip_id() const noexcept { return trip_id_; } // ... other methods private: int trip_id_; std::pmr::string passenger_name_; std::pmr::vector<flight_descriptor> legs_; };
For the class to be allocator-aware, we need to support specifying a custom allocator in each constructor. Let’s fill those in
class trip_descriptor { public: using allocator_type = std::pmr::polymorphic_allocator<>; trip_descriptor( int trip_id, std::pmr::string&& passenger_name, std::pmr::vector<flight_descriptor>&& legs, allocator_type alloc = {}) : trip_id_{trip_id}, passenger_name_{std::move(passenger_name), alloc}, legs_{std::move(legs), alloc} {} trip_descriptor(const trip_descriptor& other, allocator_type alloc = {}) : trip_id_{other.trip_id_}, passenger_name_{other.passenger_name_, alloc}, legs_{other.legs, alloc} {} trip_descriptor(trip_descriptor&& other, allocator_type alloc) : trip_id_{other.trip_id_}, passenger_name_{std::move(other.passenger_name_), alloc}, legs_{std::move(other.legs_), alloc} {} trip_descriptor(trip_descriptor&& other) noexcept = default; trip_descriptor& operator=(const trip_descriptor&) = default; trip_descriptor& operator=(trip_descriptor&&) = default; // ... int trip_id() const noexcept { return trip_id_; } // ... other methods private: int trip_id_; std::pmr::string passenger_name_; std::pmr::vector<flight_descriptor> legs_; };
As we can clearly see, making the class allocator-aware leads to quite a bit of additional boilerplate code, and this increases the maintenance burden for the class: If we add an additional data member, we need to go through and modifying all of these constructors.
Is there a way to avoid all the boilerplate? Fortunately, by using C++ structured-binding reflection and an allocator_aware_helper class, we can. Here’s how we can rewrite trip_descriptor to avoid the boilerplace:
struct trip_descriptor_data { int trip_id_; std::pmr::string passenger_name_; std::pmr::vector<flight_descriptor> legs_; }; class trip_descriptor final : public allocator_aware_helper<trip_descriptor_data> { using base = allocator_aware_helper<trip_descriptor_data>; public: using base::base; trip_descriptor( int trip_id, std::pmr::string&& passenger_name, std::pmr::vector<flight_descriptor>&& legs, allocator_type alloc = {}) : base{ trip_descriptor_data{ .trip_id_{trip_id}, .passenger_name_{std::move(passenger_name}, .legs{std::move(legs)} }, alloc } {} // ... int trip_id() const noexcept { return trip_id_; } // ... other methods };
Now when we write code like
trip_descriptor trip1; // fill in trip1 std::pmr::polymorphic_allocator<> alloc = /* a custom allocator */; trip_descriptor trip2{std::move(trip1), alloc};
trip_descriptor will correctly move construct with the specified allocator by using the inherited constructors from allocator_aware_helper.
How does allocator_aware_helper work? By using a combination of structured bindings and SFINAE, it’s able to decompose trip_descriptor_data into a tuple. It can then access members individually and forward the user-provided allocator if a member is allocator-aware. For example, here’s what allocator_aware_helper’s move constructor looks like
template <class T> T make_allocator_aware_move_constructor_initializer( T &other, std::pmr::polymorphic_allocator<> alloc) noexcept { if constexpr (basc::allocator_aware<T>) { return T{std::move(other), alloc}; } else { return T{std::move(other)}; } } template <class Data, size_t... Indexes> class allocator_aware_impl<Data, std::index_sequence<Indexes...>> : protected Data { public: using allocator_type = std::pmr::polymorphic_allocator<>; // ... allocator_aware_impl(allocator_aware_impl &&other, allocator_type alloc) noexcept : Data{make_allocator_aware_move_constructor_initializer( basrf::tuple_convertible_get<Indexes>(static_cast<Data &>(other)), alloc)...} {} // ... }; template <class Data> class allocator_aware_helper : public allocator_aware_impl< Data, std::make_index_sequence<basrf::tuple_convertible_arity_v<Data>>> { // ... };
Similarly, allocator_aware_helper provides allocator-aware copy construction and default construction.
For the full example, check out the source code here.
Using allocator_aware_helper, our type trip_descriptor need only worry about type-specific constructors, making it much easier to create and maintain.