0

I'm trying out these new pmr thingies, and stuff seems really interesting.

So in my example I'm hooking up a monotonic_buffer_resource to a buffer and overlay it with a logging resource to gain insight on what allocations are going on. Then I'm creating a pmr::vector using that resource and pushing back my pmr aware container type (which is essentially just a wrapper over a pmr::string.

Now I initialize the temporaries in the vector initializer list with my memory resource and pass them to std::vector. What I thought would happen was that vector would move these objects in place, instead they're copied! I thought that temporaries are always flagged as rvalues when passed into constructors, so I'm a bit surprised. On another note, it also doesn't help when I specifically std::move() these objects. What is going on?

LiveDemo

struct pmr_aware_container
{
    using allocator_type = std::pmr::polymorphic_allocator<std::byte>;

    /* ctors */

    // default
    pmr_aware_container() : pmr_aware_container{allocator_type{}} {} // delegate to aa constructor

    explicit pmr_aware_container(const allocator_type alloc)
        : str_("Hello long string!!!", alloc) {
        printf("default constructor called!\n");
    }

    // copy
    // pmr_aware_container(const pmr_aware_container&) = default;

    pmr_aware_container(const pmr_aware_container& other, allocator_type alloc = {}) 
        : str_(other.str_, alloc) {
        printf("Copy constructor called!\n");
    }

    // move
    pmr_aware_container(pmr_aware_container&& other) noexcept
        : str_{std::move(other.str_), other.get_allocator() }
    {
        printf("Noexcept move constructor called!\n");
    }

    pmr_aware_container(pmr_aware_container&& other, const allocator_type& alloc)
        : str_(std::move(other.str_), alloc)
    {
        printf("Specific move constructor called!\n");
    }

    // assignement

    pmr_aware_container& operator=(const pmr_aware_container& rhs) = default;
    pmr_aware_container& operator=(pmr_aware_container&& rhs) = default;

    ~pmr_aware_container() = default;

    allocator_type get_allocator() const {
        return str_.get_allocator();
    }

    std::pmr::string str_ = "Hello long string!!!";
};


class LoggingResource : public std::pmr::memory_resource
{
public:
    LoggingResource(std::pmr::memory_resource *underlying_resource) : underlying_resource_{underlying_resource} { }

private:
    void *do_allocate(size_t bytes, size_t align) override {
        printf("Allocating %d bytes!\n", bytes);
        return underlying_resource_->allocate(bytes, align);
    }

    void do_deallocate(void*p, size_t bytes, size_t align) {
        underlying_resource_->deallocate(p, bytes, align);
    }

    bool do_is_equal(std::pmr::memory_resource const& other) const noexcept override {
        return underlying_resource_->is_equal(other);
    }

    std::pmr::memory_resource* underlying_resource_;
};

int main()
{
    std::array<std::byte, 2024> buf;
    std::pmr::monotonic_buffer_resource mbs{buf.data(), buf.size()};

    LoggingResource log_resource{&mbs};

    std::pmr::vector<pmr_aware_container> v{ { pmr_aware_container{ &log_resource}, pmr_aware_container{ &log_resource} }, &log_resource};
}

Output:

Allocating 21 bytes!
default constructor called!
Allocating 21 bytes!
default constructor called!
Allocating 80 bytes!
Allocating 21 bytes!
Copy constructor called!
Allocating 21 bytes!
Copy constructor called!

Resources used in progress:

Creating Allocator-Aware Types by Jason Turner

std::pmr Comes With a Cost by David Sankel

Allocators: The Good Parts by Pablo Halpern

Bonus Question:

I can see from the assembly that the LoggingResource still contains a vtable and hence is not devirtualized. Can I do something to encourage this? I have seen that there was a major improvement in this area from gcc 9 to gcc 11, whereas in gcc 9 even the monotonic_buffer_resource still created a vtable. Anyone knows what I have to do to make the vtable go away for my a custom memory resource?

glades
  • 3,778
  • 1
  • 12
  • 34

1 Answers1

2

The copy constructor is used because you cannot move elements out of a std::initializer_list. This is a long-standing issue with std::initializer_list since the days of C++11; there have been several proposals to support move semantics, but none that are close to being accepted. Too bad...

If you rewrite your code like this, you'll see pmr_aware_container(pmr_aware_container&&, const allocator_type&) being called:

std::pmr::vector<pmr_aware_container> v{&log_resource};
v.push_back(std::move(elem1));
v.push_back(std::move(elem2));
Brian Bi
  • 111,498
  • 10
  • 176
  • 312
  • Thank you! I was actually aware of that but I temporarily forgot. So in case of pmr this means never use initializer lists? Thats actually too bad, especially for creating user-friendly APIs. Guess I have to wrap everything now :/ – glades Aug 05 '22 at 17:31
  • 1
    @glades If you need move semantics, don't use `std::initializer_list`. It's not an issue of whether you're using polymorphic allocators or not. – Brian Bi Aug 05 '22 at 17:40
  • True. Would you happen to know the answer to the bonus question as well? :) – glades Aug 05 '22 at 18:08
  • @glades No, sorry. I would suggest posting it as a separate question to draw more attention to it. – Brian Bi Aug 05 '22 at 18:09
  • Ty, I did so: https://stackoverflow.com/questions/73254236/how-to-achieve-total-devirtualization-of-custom-pmr-allocators – glades Aug 05 '22 at 18:59