自定义分配器可以替代智能指针的向量?

时间:2019-05-27 10:36:35

标签: c++ c++11 shared-ptr unique-ptr allocator

这个问题是关于拥有指针,使用指针,智能指针,向量和分配器的。

我对代码体系结构的想法有些迷茫。此外,如果这个问题在某个地方已经有答案,1.抱歉,但到目前为止我还没有找到满意的答案,并且2.请指出我的意思。

我的问题如下:

我有几个存储在向量中的“事物”和这些“事物”的多个“消费者”。因此,我的第一次尝试如下:

std::vector<thing> i_am_the_owner_of_things;
thing* get_thing_for_consumer() {
    // some thing-selection logic
    return &i_am_the_owner_of_things[5]; // 5 is just an example
}

...

// somewhere else in the code:
class consumer {
    consumer() {
       m_thing = get_thing_for_consumer();
    }

    thing* m_thing;
};

在我的应用程序中,这是安全的,因为在任何情况下,“事物”的寿命都超过了“消费者”。但是,可以在运行时添加更多“事物”,这可能会成为问题,因为如果重新分配std::vector<thing> i_am_the_owner_of_things;,所有thing* m_thing指针都将变为无效。

对此情况的一种解决方法是将唯一的指针存储到“事物”,而不是直接存储“事物”,即如下所示:

std::vector<std::unique_ptr<thing>> i_am_the_owner_of_things;
thing* get_thing_for_consumer() {
    // some thing-selection logic
    return i_am_the_owner_of_things[5].get(); // 5 is just an example
}

...

// somewhere else in the code:
class consumer {
    consumer() {
       m_thing = get_thing_for_consumer();
    }

    thing* m_thing;
};

这里的缺点是“事物”之间的内存一致性丢失了。可以通过使用自定义分配器重新建立此内存一致性吗?我在考虑类似分配器的事情,该分配器总是一次为例如10个元素分配内存,并且在需要时会添加更多10个元素大小的内存块。

示例:
最初:
v =☐☐☐☐☐☐☐
更多元素:
v =☐☐☐☐☐☐☐☐☐ 再一次:
v =☐☐☐☐☐☐☐☐☐☐

使用这样的分配器,我什至不必使用{things”的std::unique_ptr,因为在std::vector的重新分配时间,已经存在的元素的内存地址不会改变。

作为替代方案,我只能考虑通过std::shared_ptr<thing> m_thing来引用“消费者”中的“事物”,而不是当前的thing* m_thing,但这对我来说似乎是最糟糕的方法,因为“事物”不应拥有“消费者”,并且使用共享指针可以创建共享所有权。

那么,分配器方法是一个好方法吗?如果是这样,怎么办?我必须自己实现分配器还是现有的分配器?

4 个答案:

答案 0 :(得分:11)

如果您可以将thing视为值类型,请这样做。它简化了事情,您不需要智能指针即可规避指针/引用无效问题。后者可以用不同的方式解决:

  • 如果在程序执行过程中通过thingpush_front插入了新的push_back实例,请使用std::deque而不是std::vector。然后,此容器中没有指向元素的指针或引用无效(但是,迭代器也无效了-感谢@ odyss-jii指出了这一点)。如果您担心自己严重依赖std::vector的完全连续内存布局的性能优势,请创建一个基准和配置文件。
  • 如果在程序执行期间在容器的中间插入了新的thing实例,请考虑使用std::list。插入或删除容器元素时,没有指针/迭代器/引用无效。在std::list上进行迭代比std::vector慢得多,但是请确保这是您所遇到的实际问题,然后再对此进行过多担心。

答案 1 :(得分:1)

这个问题没有一个正确的答案,因为它在很大程度上取决于确切的访问模式和所需的性能特征。

话虽如此,这是我的建议:

继续按原样连续存储数据,但不要存储指向该数据的别名指针。相反,请考虑使用一个更安全的替代方法(这是一种行之有效的方法),在此方法中,您将在使用ID之前立即根据ID提取指针-附带说明,在多线程应用程序中,您可以锁定尝试调整底层存储大小的尝试,如此薄弱的参考生活。

因此,您的消费者将存储一个ID,并根据需要从“存储”中获取指向数据的指针。这也使您可以控制所有“提取”,以便您可以跟踪它们,实施安全措施等。

void consumer::foo() {
    thing *t = m_thing_store.get(m_thing_id);
    if (t) {
        // do something with t
    }
}

或更高级的替代方法可帮助实现多线程方案中的同步:

void consumer::foo() {
    reference<thing> t = m_thing_store.get(m_thing_id);
    if (!t.empty()) {
        // do something with t
    }
}

reference是一些线程安全的RAII“弱指针”。

有多种实现方法。您既可以使用开放地址哈希表,也可以使用ID作为键;如果平衡得当,这将为您大致提供O(1)访问时间。

另一种选择(最好的情况O(1),最坏的情况O(N))是使用“引用”结构,该结构具有32位ID和32位索引(大小与64-位指针)-索引用作排序缓存。提取时,如果索引中的元素具有预期的ID,则首先尝试索引。否则,您会遇到“缓存未命中”的情况,并对存储进行线性扫描以根据ID查找元素,然后将最后一个已知的索引值存储在引用中。

答案 2 :(得分:-1)

IMO最好的方法是创建一个安全的新容器。

优点:

  • 更改将在单独的抽象级别进行
  • 对旧代码的更改将降至最低(只需将std::vector替换为新容器)。
  • 这将是“干净的代码”方式

缺点:

  • 似乎还有很多工作要做

其他答案建议使用std::list来完成这项工作,但分配数量较大且随机访问速度较慢。因此,IMO最好由两个std::vector组成自己的容器。

因此它可能开始看起来像这样(最小示例):

template<typename T>
class cluster_vector
{
public:
    static const constexpr cluster_size = 16;

    cluster_vector() {
       clusters.reserve(1024);
       add_cluster();
    }

    ...

    size_t size() const {
       if (clusters.empty()) return 0;
       return (clusters.size() - 1) * cluster_size + clusters.back().size();
    }

    T& operator[](size_t index) {
        thowIfIndexToBig(index);
        return clusters[index / cluster_size][index % cluster_size];
    }

    void push_back(T&& x) {
        if_last_is_full_add_cluster();
        clusters.back().push_back(std::forward<T>(x));
    }

private:
    void thowIfIndexToBig(size_t index) const {
        if (index >= size()) {
            throw std::out_of_range("cluster_vector out of range");
        }
    }

    void add_cluster() {
       clusters.push_back({});
       clusters.back().reserve(cluster_size);
    }

    void if_last_is_full_add_cluster() {
       if (clusters.back().size() == cluster_size) {
           add_cluster();
       }
    }

private:
    std::vector<std::vector<T>> clusters;
}

这样,您将提供不会重新分配项目的容器。它不计量T做什么。

答案 3 :(得分:-2)

  

[共享指针]对我来说似乎是最糟糕的方法,因为“事物”不应拥有“消费者”,而使用共享指针,我将创建共享所有权。

那又怎样?也许代码的自我记录要少一些,但是它将解决您的所有问题。 (顺便说一下,您通过使用“消费者”一词来弄乱事物,在传统的生产者/消费者范式中,拥有所有权。)

此外,在您的当前代码中返回原始指针对于所有权已经完全不明确。一般来说,如果可以的话,最好避免使用原始指针(例如不需要调用delete。)如果您选择unique_ptr

std::vector<std::unique_ptr<thing>> i_am_the_owner_of_things;
thing& get_thing_for_consumer() {
    // some thing-selection logic
    return *i_am_the_owner_of_things[5]; // 5 is just an example
}