Brian
Brian

Reputation: 279

Design pattern to create smart references to elements in a vector

Because references in a vector point to locations of memory and not the abstract element, it can cause a few problems when altering the memory of the vector.

  1. If a reference points to an element in a vector, and then that element is shuffled to another spot in the vector, the reference doesn't track the element, and will point to incorrect data after the shuffle.

  2. If a element is invalidated, you can still access that elements contents without any safety checks, if you declared a references before invalidating the element.

  3. If the vector resizes, all current references may be invalidated.

I wrote an example program that demonstrates all three problems.

#include <iostream>
#include <vector>

struct entity {  //Simple struct of data.
     bool alive;
     float data;
};

class manager {
    std::vector<entity> vec;  
    size_t count; // Amount of currently alive entities
public:

    //Reserves initial_amount of entities, all set to dead, count set to 0.
    manager(size_t initial_amount) : vec(initial_amount, { false, 0.0f }), count(0) {} 

    entity& create(float f) {
        vec[count] = {true, f};
        return vec[count++];
    }

    void refresh() {                    //Two iterators, one starts at the front of the vector, the other at
        size_t front = 0;               //count. The front iterator searches for dead entities and swaps them
        size_t back  = count;           //with alive entities from the back iterator. For each swap we decrement
                                        //count by 1, with the final result being all alive entities are between
        while(true) {                   //0 and count.
            for( ; true; ++front) {
                if (front > back)      return;
                if (!vec[front].alive) break;
            }
            for( ; true; --back) {
                if (vec[back].alive) break;
                if (back <= front)   return;
            }

            std::swap(vec[back], vec[front]);
            --count;

            ++front;
            --back;
        }
    }

    void grow(size_t n) {
        vec.resize(n);
    }

    void print() { //Prints all alive entities.
        for (size_t index = 0; index < count; index++)
            std::cout << vec[index].data << " ";

        std::cout << std::endl;
    }
};

int main() {
    using namespace std;

    manager c(10);
    entity& d1 = c.create(5.5);
    entity& d2 = c.create(10.5);
    entity& d3 = c.create(7.5);

                             // Correct behavior
    cout << d1.data << endl; // 5.5
    cout << d2.data << endl; // 10.5
    cout << d3.data << endl; // 7.5
    cout << endl;

    d2.alive = false;        // "Kill" the entity
    c.refresh();             // removes all dead entities. (this will swap d2's and d3's data in the vector, 
                             // but wont change the locations they point to)

                             // Oh no! d2 and d3 still point to the same locations in the vector and now their data
                             // is incorrect after the swap, also d2 is dead maybe that should just be an error.
    cout << d1.data << endl; // 5.5
    cout << d2.data << endl; // 7.5
    cout << d3.data << endl; // 10.5
    cout << endl;

    c.print();               // Correct behavior, prints only alive entities.
    cout << endl;

    d3.data = 6.5;           // Trying to change the value of d3, which should still be alive.
    c.print();               // Error, because d3 still points to the 3rd slot the intended value hasn't been changed.
    cout << endl;

    c.grow(10000);

    cout << d1.data << endl; // After resize all these references are invalidated,
    cout << d2.data << endl; // and using them is undefined behavior.
    cout << d3.data << endl;
    return 0;
}

Is there a design pattern to create a smart reference or proxy type that solves these problems? An object that will track its elements position in the vector, does specific behavior if the element is alive or dead, and stay valid after a resize?

I'm fine with the implementation of the smart/proxy reference to not be an actual reference, could be a pointer, integer index, or whatever. But this is specifically for elements in a vector, not a linked-list, map, etc.

Upvotes: 1

Views: 72

Answers (1)

Jarod42
Jarod42

Reputation: 217245

With std::vector<std::shared_ptr<entity>>, you may have the security you want:

class manager {
    std::vector<std::shared_ptr<entity>> vec;  
public:

    //Reserves initial_amount of entities
    explicit manager(size_t initial_amount) { vec.reserve(initial_amount); }

    std::weak_ptr<entity> create(float f) {
        vec.push_back(std::make_unique<entity>(entity{true, f}));
        return vec.back();
    }

    void refresh() {
        vec.erase(std::remove_if(vec.begin(), vec.end(),
                                 [](const auto& ent) {return !ent->alive;}),
                  vec.end());
    }

    void grow(size_t n) { vec.reserve(n); }

    void print() { //Prints all alive entities.
        for (const auto& ent : vec)
            std::cout << ent->data << " ";
        std::cout << std::endl;
    }
};

And then the test:

int main() {
    manager c(10);
    auto d1 = c.create(5.5);
    auto d2 = c.create(10.5);
    auto d3 = c.create(7.5);

    // Correct behavior
    if (auto e = d1.lock()) std::cout << e->data << std::endl; else std::cout << "Die\n"; // 5.5
    if (auto e = d2.lock()) std::cout << e->data << std::endl; else std::cout << "Die\n"; // 10.5
    if (auto e = d3.lock()) std::cout << e->data << std::endl; else std::cout << "Die\n"; // 7.5
    std::cout << std::endl;

    if (auto e = d2.lock()) e->alive = false;        // "Kill" the entity
    c.refresh();             // removes all dead entities.

    if (auto e = d1.lock()) std::cout << e->data << std::endl; else std::cout << "Die\n"; // 5.5
    if (auto e = d2.lock()) std::cout << e->data << std::endl; else std::cout << "Die\n"; // Die
    if (auto e = d3.lock()) std::cout << e->data << std::endl; else std::cout << "Die\n"; // 10.5
    std::cout << std::endl;

    c.print();               // Correct behavior, prints only alive entities.
    std::cout << std::endl;

    if (auto e = d3.lock()) e->data = 6.5; // Trying to change the value of d3,
                                           // which should still be alive.
    c.print();
    std::cout << std::endl;

    c.grow(10000);

    if (auto e = d1.lock()) std::cout << e->data << std::endl; else std::cout << "Die\n"; // 5.5
    if (auto e = d2.lock()) std::cout << e->data << std::endl; else std::cout << "Die\n"; // Die
    if (auto e = d3.lock()) std::cout << e->data << std::endl; else std::cout << "Die\n"; // 6.5
}

Demo

Upvotes: 1

Related Questions