我有一个分配器感知容器,如果它是有状态的分配器(例如多态分配器),则必须存储分配器。否则,分配器不应占用任何空间。我认为 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++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;
};
有一些技术可以避免无状态分配器占用分配器感知容器中的空间。
为了避免由于分配器实例和一般空结构而导致的内存增量,使用了特殊的优化,空基优化(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;