如何将(空)分配器存储在我的容器中而不占用空间?

问题描述 投票:0回答:2

我有一个分配器感知容器,如果它是有状态的分配器(例如多态分配器),则必须存储分配器。否则,分配器不应占用任何空间。我认为 stdlib 会为此使用类似空基优化的东西,但是,我似乎无法让它工作:

演示

#include <cstdio>
#include <memory>
#include <memory_resource>
#include <iostream>


template <typename T, typename Allocator>
struct MyContainerBase {
    Allocator allocator;
};

template <typename T, typename Allocator = std::allocator<T>>
struct MyContainer : private MyContainerBase<T, Allocator>
{
    using base_ = MyContainerBase<T, Allocator>;

    T myint_;
};

int main() {
    MyContainer<int> c;

    std::cout << "size = " << sizeof(MyContainer<int>) << std::endl;
}

产量

size = 8

我查看了向量的 libc++ 和 libstdc++ 实现,但我根本找不到存储的分配器?但它仍然可以与 pmr 分配器一起使用。我该怎么做呢?另外,我至少必须有一些分配器对象引用与

std::allocator_traits<Allocator>(myalloc);

一起使用
c++ allocator
2个回答
2
投票

在 C++20 之前,这需要使用空基优化,但使用 C++20 解决方案非常简单:使用

no_unique_address
属性:

template <typename T, typename Allocator>
struct MyContainerBase {
#ifdef _MSC_VER
    [[msvc::no_unique_address]]
#else
    [[no_unique_address]]
#endif
    Allocator allocator;
};

在线编译器


0
投票

有一些技术可以避免无状态分配器占用分配器感知容器中的空间。

空基地优化

为了避免由于分配器实例和一般空结构而导致的内存增量,使用了特殊的优化,空基优化(EBO)。它基于这样的假设:如果一个类继承自一个空基类,则后者所占用的空间可以被编译器优化,从而导致派生类的整体大小不会增加。

因此,实现容器是为了使其包含部分或全部属性的类或子类继承自分配器。如果后者是无状态的,编译器会优化结构体,不增加大小;否则,整体大小将等于分配器和结构体大小的总和。

例如,

std::vector
容器可以通过以下方式实现优化。

struct impl
: public allocator_type
{
    pointer         start;
    pointer         finish;
    pointer         end;
};

impl _impl;

这种设计的一个缺点是需要转换派生类的实例来访问基类,在本例中是分配器。因此,许多实现更喜欢引入辅助函数,允许访问分配器而无需执行任何显式强制转换操作。

例如,可以通过以下方式创建两个辅助函数。

allocator_type& _get_allocator() noexcept
{ return this->_impl; }

const allocator_type& _get_allocator() const noexcept
{ return this->_impl; }

虽然设计看起来很稳健,但在 C++17 中引入关键字final,允许类变得不可派生,完全破坏了原始的 EBO 实现。事实上,尝试继承不可导出的分配器(通常是空结构)会产生编译错误。从这个意义上说,所有标准分配器感知容器的 libstdc++ (GCC) 实现都被破坏了。

解决该问题的一种方法是尝试保留 EBO 的优点,涉及专门化派生类,以便它继承分配器(如果可派生),或者将分配器存储为属性,否则。

std::vector
容器为例,该结构可以通过以下方式重新实现。

template <bool = !std::is_final_v<allocator_type>>
struct impl
: public allocator_type
{
    pointer         start;
    pointer         finish;
    pointer         end;
};

template <>
struct impl<false>
{
    pointer         start;
    pointer         finish;
    pointer         end;
    allocator_type  alloc;
};

impl<> _impl;

这种妥协要求,除其他外,几乎强制引入辅助函数来访问分配器,因为如果没有它们,则需要在编译时实施一系列检查,以了解选择了两个专业化中的哪一个,然后访问分配器最合适的方式。

压缩对

为了降低复杂性,可以创建一个抽象层,压缩对。它只是一个外部类,作为pair类实现,允许存储两个对象,只要有可能就在其上执行EBO。

此类可以将 EBO 实现移至幕后,并减少容器实现的复杂性和代码重复。当必须将相同的优化应用于两个或多个分配器感知容器时,后者是相关的。两个成员函数

first()
second()
可用于访问存储在压缩对中的两个对象。

std::vector
容器的示例中,之前的实现可以替换为以下实现,为简单起见,使用压缩对的 Boost 版本。

struct impl
{
    pointer         start;
    pointer         finish;
    pointer         end;
};

boost::compressed_pair<impl, allocator_type> _impl;

虽然设计更好,但这仍然有一个重要的限制:如果分配器是不可导出的,则无法执行优化。此外,EBO 本身是一个复杂的习惯用法,它需要增加代码的大小,并使程序员远离实现的最重要方面,而专注于优化。

无唯一地址

但是,自 C++20 以来,这些问题已通过引入新的标准编译器属性 no_unique_address 得到解决。它向编译器建议可以优化结构的哪些属性以不占用内存。因此,直接实例化分配器(通常是任何空结构)并将上述属性与类型声明并置就足够了。

总结一下

std::vector
容器的示例,其实现如下。

struct impl
{
    pointer         start;
    pointer         finish;
    pointer         end;
[[no_unique_address]]
    allocator_type  alloc;
};

impl _impl;
© www.soinside.com 2019 - 2024. All rights reserved.