martinus / unordered_dense

A fast & densely stored hashmap and hashset based on robin-hood backward shift deletion

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[BUG] Allocator issues in segmented_vector

sh-mochi opened this issue · comments

auto operator=(segmented_vector&& other) noexcept -> segmented_vector& checks for allocator equality and if allocators are not equal the array is basically rebuilt (with elements moved individually). This is from the current code base:

auto operator=(segmented_vector&& other) noexcept -> segmented_vector& {
    clear();
    dealloc();
    if (other.get_allocator() == get_allocator()) {
        m_blocks = std::move(other.m_blocks);
        m_size = std::exchange(other.m_size, {});
    } else {
        // make sure to construct with other's allocator!
        m_blocks = std::vector<pointer, vec_alloc>(vec_alloc(other.get_allocator()));
        append_everything_from(std::move(other));
    }
    return *this;
}

Here append_everything_from(std::move(other)); potentially allocates memory and any OOM situation immediately crashes straight into a noexcept boundary. There is also some twisted logic going on. After

// make sure to construct with other's allocator!
m_blocks = std::vector<pointer, vec_alloc>(vec_alloc(other.get_allocator()));

the condition other.get_allocator() == get_allocator() would always be true, right? Therefore, after

segmented_vector(segmented_vector&& other, Allocator alloc)
    : segmented_vector(alloc) {
    *this = std::move(other);
}

the contaner will always end up with other's allocator (regardless of whether alloc==other.get_allocator()). There is still a silly edge case in the operator= method I guess: While std::vector<pointer, vec_alloc>(vec_alloc(other.get_allocator())) actually has an allocator that compares equal to other.get_allocator(), it is unclear if this allocator will be propagated through the assignment which depends on pocma, so you can still end up with m_blocks.get_allocator()!=other.m_blocks.get_allocator() after m_blocks = std::vector<pointer, vec_alloc>(vec_alloc(other.get_allocator()));. Propose fix:

auto operator=(segmented_vector&& other) noexcept -> segmented_vector& {
    static_assert(vec_alloc has pocma==true);
    clear();
    dealloc();
    m_blocks = std::move(other.m_blocks); // without pocma m_block could end up with an incompatible allocator
    m_size = std::exchange(other.m_size, {});
    return *this;
}

Here are some other issues regarding allocators that should be rather easy to fix:

Allocator is not propagated as it should be.

segmented_vector(segmented_vector const& other) { // missing initializer :m_blocks(other.m_blocks.get_allocator()) {
    append_everything_from(other);
}

Doesn't propagate the allocator of other:

auto operator=(segmented_vector const& other) -> segmented_vector& {
    if (this == &other) {
        return *this;
    }
    clear();
    append_everything_from(other);
    return *this;
}

This looks off - isn't this a use before init of get_allocator()?

segmented_vector(segmented_vector&& other) noexcept
    : segmented_vector(std::move(other), get_allocator()) {}

When it comes to the excellent (cough) allocator requirements, maybe an honest

static_assert(pocca && pocma && pocs, "Go away!")

would go a long way.