考虑到副本构造的要求,我如何在C++11中编写一个有状态分配器
How can I write a stateful allocator in C++11, given requirements on copy construction?
据我所知,要与STL一起使用的分配器的要求容器在C++11标准第17.6.3.5节的表28中列出。
我对其中一些需求之间的交互有点困惑。给定类型X
是类型T
的分配器,类型Y
是"的类型U
、实例a
、a1
和a2
的X
和Y
的实例b
,表中显示:
-
只有在分配了存储时,表达式
a1 == a2
才计算为true
来自a1
的分配可以由a2
解除分配,反之亦然。 -
表达式CCD_ 15形式良好,不会通过异常退出,之后CCD_ 16为真。
-
表达式
X a(b)
格式良好,不会通过异常退出,并且之后为CCD_ 18。
我读到这里是说,在这样的复印件和原件可以互换的方式。更糟糕的是,同样跨类型边界为true。这似乎是一项相当繁重的要求;像据我所知,它使得大量类型的分配器变得不可能。
例如,假设我有一个freelist类,我想在分配器中使用它,以便缓存释放的对象。除非我错过了什么,否则我不会在分配器中包括该类的实例,因为大小或T
和U
的排列可能不同,因此自由列表条目为不兼容。
我的问题:
-
我以上的解释正确吗?
-
我在一些地方读到C++11改进了对"有状态"的支持分配器"。考虑到这些限制,情况如何?
-
你对如何做我想做的事情有什么建议吗做也就是说,如何在分配器中包含已分配的类型特定状态?
-
一般来说,关于分配器的语言似乎很草率。(例如表28的序言假设
a
属于X&
类型,但是表达式重新定义a
。(此外,至少GCC的支持是不符合要求的。是什么导致了分配器的这种怪异?只是偶尔使用过的功能?
分配器的相等并不意味着它们必须具有完全相同的内部状态,只是意味着它们都必须能够释放使用任一分配器分配的内存X
型分配器a
和Y
型分配器b
的分配器a == b
的交叉型相等性在表28中定义为"与a == Y::template rebind<T>::other(b)
相同"。换句话说,由a
分配的a == b
如果内存可以由分配器解除分配,分配器通过将b
重新绑定到a
的value_type
来实例化。
您的自由列表分配器不需要能够解除分配任意类型的节点,您只需要确保FreelistAllocator<T>
分配的内存可以由FreelistAllocator<U>::template rebind<T>::other
解除分配。假设在大多数正常的实现中,FreelistAllocator<U>::template rebind<T>::other
与FreelistAllocator<T>
是相同的类型,这很容易实现。
简单示例(Coliru现场演示(:
template <typename T>
class FreelistAllocator {
union node {
node* next;
typename std::aligned_storage<sizeof(T), alignof(T)>::type storage;
};
node* list = nullptr;
void clear() noexcept {
auto p = list;
while (p) {
auto tmp = p;
p = p->next;
delete tmp;
}
list = nullptr;
}
public:
using value_type = T;
using size_type = std::size_t;
using propagate_on_container_move_assignment = std::true_type;
FreelistAllocator() noexcept = default;
FreelistAllocator(const FreelistAllocator&) noexcept {}
template <typename U>
FreelistAllocator(const FreelistAllocator<U>&) noexcept {}
FreelistAllocator(FreelistAllocator&& other) noexcept : list(other.list) {
other.list = nullptr;
}
FreelistAllocator& operator = (const FreelistAllocator&) noexcept {
// noop
return *this;
}
FreelistAllocator& operator = (FreelistAllocator&& other) noexcept {
clear();
list = other.list;
other.list = nullptr;
return *this;
}
~FreelistAllocator() noexcept { clear(); }
T* allocate(size_type n) {
std::cout << "Allocate(" << n << ") from ";
if (n == 1) {
auto ptr = list;
if (ptr) {
std::cout << "freelistn";
list = list->next;
} else {
std::cout << "new noden";
ptr = new node;
}
return reinterpret_cast<T*>(ptr);
}
std::cout << "::operator newn";
return static_cast<T*>(::operator new(n * sizeof(T)));
}
void deallocate(T* ptr, size_type n) noexcept {
std::cout << "Deallocate(" << static_cast<void*>(ptr) << ", " << n << ") to ";
if (n == 1) {
std::cout << "freelistn";
auto node_ptr = reinterpret_cast<node*>(ptr);
node_ptr->next = list;
list = node_ptr;
} else {
std::cout << "::operator deleten";
::operator delete(ptr);
}
}
};
template <typename T, typename U>
inline bool operator == (const FreelistAllocator<T>&, const FreelistAllocator<U>&) {
return true;
}
template <typename T, typename U>
inline bool operator != (const FreelistAllocator<T>&, const FreelistAllocator<U>&) {
return false;
}
1( 我以上的解释正确吗?
您是对的,您的空闲列表可能不太适合分配器,它需要能够处理多个大小(和对齐(来适应。这是免费列表需要解决的问题。
2( 我在一些地方读到C++11改进了对"有状态分配器"的支持。考虑到这些限制,情况如何?
与其说它有多大的改善,不如说它是天生的。在C++03中,该标准只是促使实现者提供可以支持不平等实例和实现者的分配器,从而有效地使有状态分配器不可移植。
3( 你对如何做我想做的事情有什么建议吗?也就是说,如何在分配器中包含已分配的类型特定状态?
您的分配器可能必须是灵活的,因为您不应该确切地知道它应该分配什么内存(以及什么类型(。这个要求对于将您(用户(与使用分配器的某些容器(如std::list
、std::set
或std::map
(的内部隔离是必要的。
您仍然可以将此类分配器与std::vector
或std::deque
等简单容器一起使用。
是的,这是一项成本高昂的要求。
4( 一般来说,关于分配器的语言似乎很草率。(例如,表28的序言说假设a的类型为X&,但有些表达式重新定义了a。(此外,至少GCC的支持是不一致的。是什么导致了分配器的这种怪异?它只是一个不常用的功能吗?
一般来说,标准并不是很容易阅读,不仅仅是分配器。你必须小心。
老实说,gcc不支持分配器(它是一个编译器(。我猜想您说的是libstdc++(gcc附带的标准库实现(。libstdc++是旧的,因此它是为C++03量身定制的。它已经适应了C++11,但还没有完全一致(例如,仍然对字符串使用写时复制(。原因是libstdc++非常关注二进制兼容性,而C++11所需的许多更改将破坏这种兼容性;因此,必须谨慎地介绍它们。
我读到这里是说,所有分配器都必须是可复制的,这样副本才能与原件互换。更糟糕的是,跨类型边界也是如此。这似乎是一项相当繁重的要求;据我所知,它使得大量类型的分配器变得不可能。
如果分配器是某个内存资源的轻量级句柄,那么满足这些要求是很琐碎的。只是不要试图将资源嵌入到单独的分配器对象中。
例如,假设我有一个freelist类,我想在分配器中使用它来缓存释放的对象。除非我遗漏了什么,否则我不能在分配器中包含该类的实例,因为t和U的大小或对齐方式可能不同,因此自由列表条目不兼容。
[分配器要求]第9段:
分配器可以约束可以对其进行实例化的类型以及可以调用其
construct
成员的自变量。如果一个类型不能与特定的分配器一起使用,则分配器类或对construct
的调用可能无法实例化。
分配器可以拒绝为除给定类型T
之外的任何对象分配内存。这将防止它在基于节点的容器(如std::list
(中使用,这些容器需要分配自己的内部节点类型(而不仅仅是容器的value_type
(,但它对std::vector
可以正常工作。
这可以通过防止分配器反弹到其他类型来实现:
class T;
template<typename ValueType>
class Alloc {
static_assert(std::is_same<ValueType, T>::value,
"this allocator can only be used for type T");
// ...
};
std::vector<T, Alloc<T>> v; // OK
std::list<T, Alloc<T>> l; // Fails
或者您只能支持适合sizeof(T)
:的类型
template<typename ValueType>
class Alloc {
static_assert(sizeof(ValueType) <= sizeof(T),
"this allocator can only be used for types not larger than sizeof(T)");
static_assert(alignof(ValueType) <= alignof(T),
"this allocator can only be used for types with alignment not larger than alignof(T)");
// ...
};
- 我以上的解释正确吗
不完全是。
- 我在一些地方读到C++11改进了对"有状态分配器"的支持。考虑到这些限制,情况如何
C++11之前的限制甚至更糟!
现在,它清楚地指定了分配器在复制和移动时如何在容器之间传播,以及当分配器实例被可能与原始实例不相等的不同实例替换时,各种容器操作的行为。如果没有这些说明,就不清楚如果用有状态分配器交换两个容器会发生什么。
- 你对如何做我想做的事情有什么建议吗?也就是说,如何在分配器中包含已分配的类型特定状态
不要将它直接嵌入到分配器中,将其单独存储,并让分配器通过指针(可能是智能指针,这取决于您如何设计资源的生存期管理(来引用它。实际的分配器对象应该是某个外部内存源(例如竞技场、池或管理自由列表的东西(的轻量级句柄。共享同一源的分配器对象应该比较相等,即使对于具有不同值类型的分配器也是如此(见下文(。
我还建议,如果你只需要支持一种类型,就不要试图支持所有类型的分配。
- 一般来说,关于分配器的语言似乎很草率。(例如,表28的序言说假设a的类型为X&,但有些表达式重新定义了a。(
是的,正如您在https://github.com/cplusplus/draft/pull/334(谢谢(。
此外,至少GCC的支持是不符合要求的。
它不是100%,但会在下一个版本中出现。
是什么导致了分配器的这种怪异?它只是一个不常用的功能吗?
是的。而且有很多历史包袱,很难具体说明它是否广泛有用。我的ACCU 2012演示有一些细节,如果在阅读后你认为你可以让它变得更简单,我会非常惊讶;-(
关于分配器何时比较相等,请考虑:
MemoryArena m;
Alloc<T> t_alloc(&m);
Alloc<T> t_alloc_copy(t_alloc);
assert( t_alloc_copy == t_alloc ); // share same arena
Alloc<U> u_alloc(t_alloc);
assert( t_alloc == u_alloc ); // share same arena
MemoryArena m2
Alloc<T> a2(&m2);
assert( a2 != t_alloc ); // using different arenas
分配器相等的含义是,对象可以释放彼此的内存,因此,如果您从t_alloc
分配一些内存,而(t_alloc == u_alloc)
是true
,则意味着您可以使用u_alloc
解除分配该内存。如果它们不相等,则u_alloc
无法解除分配来自t_alloc
的内存。
如果你只是有一个自由列表,其中任何内存都可以添加到任何其他自由列表中,那么你的所有分配器对象可能会相互比较相等。
- Constexpr替代了新的放置方式,可以让内存中的对象保持未初始化状态
- 当有分配器意识的容器被复制/移动时,反弹分配器是否被复制/移走
- 我不断收到 [错误] ID 返回 1 退出状态错误,但看不到问题所在
- OSX MetalKit CVMetalTextureCacheCreateTextureFromImage失败,状态:
- 将 std::allocate_shared 与多态资源分配器一起使用
- 尝试将lambda函数放在队列中时出现一般分配器错误(可能是与unique_ptr有关的错误)
- std::future_error:无关联状态
- C++17 - 使用自定义分配器的节点提取/重新插入 - 适用于 clang++/libc++,但不适用于 libstd
- 如何避免LED在循环状态变化中闪烁?
- boost 是否有按特殊类型值编码状态"compact optional"?
- 为什么系统函数总是在C++中返回已转移的退出状态?
- C++11 个有状态分配器,带有 std::basic_string<> g++ 6.3.0
- 如何将自定义分配器的完全相同状态传递给多个容器?
- 复制有状态分配器:标准库分配器语义和内部内存
- 分配器无状态意味着什么?
- 这是存储 std::分配器状态的正确方法 - 在这种情况下,由 Windows 上的共享内存支持
- 考虑到副本构造的要求,我如何在C++11中编写一个有状态分配器
- 编译器支持STL容器中的有状态分配器
- std::vector::swap()和有状态分配器是否应使所有迭代器无效
- 有没有比编写存储对有状态分配器对象的引用的包装器分配器更好的方法