英文:
Authoring a container to work with both c++11 and pmr allocators
问题
如何正确创建一个能与C++11和C++17的多态分配器一起使用的容器?这是我目前的通用模板样例:
解释:我包括了两个字段,res_
显示了动态内存是如何直接从容器中管理的,而字段 vec_
用于展示分配器如何向下传播。我从Pablo Halpern的讲座Allocators: The Good Parts 中吸取了很多经验,但他主要讨论了pmr分配器,而不是c++11的分配器。
#include <cstdio>
#include <vector>
#include <memory>
#include <memory_resource>
template <typename T, typename Allocator = std::allocator<T>>
struct MyContainer {
auto get_allocator() const -> Allocator {
return vec_.get_allocator();
}
MyContainer(Allocator allocator = {})
: vec_{ allocator }
{}
MyContainer(T val, Allocator allocator = {})
: MyContainer(allocator)
{
res_ = std::allocator_traits<Allocator>::allocate(allocator, sizeof(T));
std::allocator_traits<Allocator>::construct(allocator, res_, std::move(val));
}
// 其他成员函数...
private:
std::vector<T, Allocator> vec_; // 传播
T* res_ = nullptr;
};
int main() {
MyContainer<std::string, std::pmr::polymorphic_allocator<std::byte>> ctr1 = std::string{"Hello World!"};
MyContainer<double> ctr2 = 2.5;
}
然而,即使这样也不能按计划工作,因为vector期望其值类型与分配器的值类型匹配。
英文:
How do I correctly create a container that works with both, C++11 and C++17 polymorphic allocators? Here's what I have so far (as a generic boilerplate template):
Explanation: I've included two fields, res_
which shows how dynamic memory is managed directly from the container, whereas field vec_
is used to demonstrate how the allocator propagates downwards. I've taken a lot from Pablo Halpern's talk Allocators: The Good Parts but he mainly talks about pmr allocators, not the c++11 ones.
#include <cstdio>
#include <vector>
#include <memory>
#include <memory_resource>
template <typename T, typename Allocator = std::allocator<T>>
struct MyContainer {
auto get_allocator() const -> Allocator {
return vec_.get_allocator();
}
MyContainer(Allocator allocator = {})
: vec_{ allocator }
{}
MyContainer(T val, Allocator allocator = {})
: MyContainer(allocator)
{
res_ = std::allocator_traits<Allocator>::allocate(allocator, sizeof(T));
std::allocator_traits<Allocator>::construct(allocator, res_, std::move(val));
}
~MyContainer() {
Allocator allocator = get_allocator();
std::allocator_traits<Allocator>::destroy(allocator, std::addressof(res_));
std::allocator_traits<Allocator>::deallocate(allocator, res_, sizeof(T));
res_ = nullptr;
}
MyContainer(const MyContainer& other, Allocator allocator = {})
: MyContainer(allocator)
{
operator=(other);
}
MyContainer(MyContainer&& other) noexcept
: MyContainer(other.get_allocator())
{
operator=(std::move(other));
}
MyContainer(MyContainer&& other, Allocator allocator = {})
: MyContainer(allocator)
{
operator=(std::move(other));
}
auto operator=(MyContainer&& other) -> MyContainer& {
if (other.get_allocator() == get_allocator()) {
std::swap(*this, other);
} else {
operator=(other); // Copy assign
}
}
auto operator=(const MyContainer& other) -> MyContainer& {
if (other != this) {
std::allocator_traits<Allocator>::construct(get_allocator(), std::addressof(vec_), vec_);
std::allocator_traits<Allocator>::construct(get_allocator(), std::addressof(res_), other);
}
return *this;
}
private:
std::vector<T, Allocator> vec_; // Propagation
T* res_ = nullptr;
};
int main() {
MyContainer<std::string, std::pmr::polymorphic_allocator<std::byte>> ctr1 = std::string{"Hello World!"};
MyContainer<double> ctr2 = 2.5;
}
However even this doesn't work as planned, as vector expects its value type to match that of the allocator:
<source>:67:31: required from 'struct MyContainer<std::__cxx11::basic_string<char>, std::pmr::polymorphic_allocator<std::byte> >'
<source>:72:74: required from here
/opt/compiler-explorer/gcc-13.1.0/include/c++/13.1.0/bits/stl_vector.h:438:64: error: static assertion failed: std::vector must have the same value_type as its allocator
438 | static_assert(is_same<typename _Alloc::value_type, _Tp>::value,
|
What else am I missing? Should I maybe propagate differently based on allocator's propagation traits (is this required for generic containers)?
答案1
得分: 1
tl;dr
- 所有标准库容器必须使用与容器的
value_type
相同的value_type
作为分配器的value_type
,否则将形成不符合规范的代码。
因此,在这种情况下,需要为MyContainer
使用std::pmr::polymorphic_allocator<std::string>
,或者在将其传递给std::vector
之前重新绑定分配器类型,例如:// 选项1 MyContainer<std::string, std::pmr::polymorphic_allocator<std::string>> ctr1 = /* ... */; // 选项2 template <class T, class Allocator = std::allocator<T>> struct MyContainer { // ... private: std::vector<T, typename std::allocator_traits<Allocator>::template rebind_alloc<T>> vec_; }; MyContainer<std::string, std::pmr::polymorphic_allocator<std::byte>> ctr1 = /* ... */;
- 在你链接的视频中,这不是问题,因为它定义了一个用户定义的容器,所以它不是一个具有分配器意识的容器也无关紧要。
- 实现一个可以处理
std::polymorphic_allocator
和std::allocator
的容器类相对较简单 - 两者都满足名为Allocator的命名要求,因此在这种情况下所需的特殊操作只是不做特殊处理 - 将它们实现为普通分配器(基本上对分配器的所有交互使用std::allocator_traits<Alloc>
)- 这包括手动检查分配器的传播偏好(按照Allocator要求页面上的“容器操作的影响”表中描述的方式实现)
1. 为什么给定的代码示例是不符合规范的
所有具有分配器意识的容器都必须具有一个分配器,其value_type
与容器的value_type
相同。
这在标准中是规定的:(重点是我的)
> 24.2.2.5 具有分配器意识的容器(4)
> <sup>(3)</sup> 在本子句中,
> <sup>(3.1)</sup> - X
表示具有类型T
的value_type
并使用类型A
的分配器的具有分配器意识的容器类,
> [...]
> 如果X
符合容器要求,并且满足以下类型、语句和表达式是符合规范的,并且具有指定的语义。
>
> typename X::allocator_type
> - <sup>(4)</sup> 结果:A
> - <sup>(5)</sup> 要求:allocator_type::value_type
与 X::value_type
相同。
因此,对于具有分配器意识的容器,必须始终满足以下语句:
static_assert(
std::same_as<
Container::value_type,
Container::allocator_type::value_type
>
);
请注意,标准库中定义的所有容器(除了std::array
)都必须具有分配器意识(参见24.2.2.5(1) 具有分配器意识的容器)
请注意,在您的示例中,该语句将不成立:
// 假设,无法编译
using Container = std::vector<std::string, std::pmr::polymorphic_allocator<std::byte>>;
// 将是 std::string
using ContainerValueType = Container::value_type;
// 将是 std::byte(std::pmr::polymorphic_allocator<std::byte>::value_type)
using AllocatorValueType = Container::allocator_type::value_type;
// 将会失败
static_assert(std::same_as<ContainerValueType, AllocatorValueType>);
- 因此,这个版本的
std::vector
不是具有分配器意识的容器(因为它不符合这个要求) - 但是标准要求
std::vector
必须是具有分配器意识的容器
=> 这是不符合规范的,因为标准存在矛盾。
请注意,这也与您从gcc收到的错误消息相匹配:
error: static assertion failed: std::vector must have the same value_type as its allocator
2. 为什么在链接的视频中不是问题
您在评论中链接的Youtube视频(CppCon 2017:Pablo Halpern “Allocators: The Good Parts”)是关于一个用户定义的容器类,该容器类不使用任何标准库容器。
标准没有对用户定义的容器类型强加任何规则,因此基本上可以根据需要进行操作。
以下是关于视频中讨论的类的简短摘录:
template<class Tp>
class slist {
public:
using value_type = Tp;
using reference = value_type&;
// ...
// 非模板使用 polymorphic_allocator
using allocator_type = std::pm
<details>
<summary>英文:</summary>
### tl;dr
- All standard library containers must be given an allocator with a `value_type` that is the same as the `value_type` of the container; otherwise it would be ill-formed.
So in this case one would need to either use a `std::pmr::polymorphic_allocator<std::string>` for `MyContainer`, or rebind the allocator type before passing it to `std::vector`, e.g.:
```c++
// option 1
MyContainer<std::string, std::pmr::polymorphic_allocator<std::string>> ctr1 = /* ... */;
// option 2
template <class T, class Allocator = std::allocator<T>>
struct MyContainer {
// ...
private:
std::vector<T, typename std::allocator_traits<Allocator>::template rebind_alloc<T>> vec_;
};
MyContainer<std::string, std::pmr::polymorphic_allocator<std::byte>> ctr1 = /* ... */;
- This is not a problem in the video you linked, because it defines a user-defined container, so it does not matter that it is not an allocator-aware container.
- Implementing a container class that can handle both
std::polymorphic_allocator
andstd::allocator
is comparatively easy - both satisfy the named requirement Allocator, so the special sauce needed in this case is just to do nothing special - implement them as a bog-standard allocator (basically usestd::allocator_traits<Alloc>
for all interactions with the allocator)- This does include manually checking the propagation preferences of the allocator (implement them exactly as described in the table "Influence on container operations" on the Allocator requirements page)
1. Why the given code example is ill-formed
All containers that are allocator-aware containers must have an allocator with a value_type
that is the same as the value_type
of the container.
This is mandated in the standard by: (emphasis mine)
> 24.2.2.5 Allocator-aware containers (4)
> <sup>(3)</sup> In this subclause,
> <sup>(3.1)</sup> - X
denotes an allocator-aware container class with a value_type
of T
using an allocator of type A
,
> [...]
> A type X
meets the allocator-aware container requirements if X
meets the container requirements and the following types, statements, and expressions are well-formed and have the specified semantics.
>
> typename X::allocator_type
> - <sup>(4)</sup> Result: A
> - <sup>(5)</sup> Mandates: allocator_type::value_type
is the same as X::value_type
.
So the following statement must always be true for an allocator-aware container:
static_assert(
std::same_as<
Container::value_type,
Container::allocator_type::value_type
>
);
Note that all containers defined in the standard library (except std::array
) are mandated to be allocator-aware. (see 24.2.2.5 (1) Allocator-aware containers)
Note that in your example that statement will not be satisfied:
// Hypothetical, won't compile
using Container = std::vector<std::string, std::pmr::polymorphic_allocator<std::byte>>;
// will be std::string
using ContainerValueType = Container::value_type;
// will be std::byte (std::pmr::polymorphic_allocator<std::byte>::value_type)
using AllocatorValueType = Container::allocator_type::value_type;
// would fail
static_assert(std::same_as<ContainerValueType, AllocatorValueType>);
- So this version of
std::vector
would not be an allocator-aware container (because it doesn't fulfill this requirement) - But the standard mandates that
std::vector
must be an allocator-aware container
=> This is ill-formed due to contradiction in the standard.
Note that this also matches the error message you got from gcc:
error: static assertion failed: std::vector must have the same value_type as its allocator
2. Why it's not a problem in the linked video
The Youtube Video you linked in the comments (CppCon 2017: Pablo Halpern “Allocators: The Good Parts”) is about a user-defined container class that does not utilize any standard library containers.
There are no rules that the standard imposes for user-defined container types, so one can basically do whatever one wants there.
Here's a small transcript of the class the talk is about:
template<class Tp>
class slist {
public:
using value_type = Tp;
using reference = value_type&;
// ...
// non-template use of polymorphic_allocator
using allocator_type = std::pmr::polymorphic_allocator<std::byte>;
// Constructors
// Every constructor has an variant taking an allocator
slist(allocator_type a = {});
slist(const slist& other, allocator_type a = {});
slist(slist&& other);
slist(slist&& other, allocator_type a = {});
// ...
};
Note that the allocator_type
is hardcoded to std::pmr::polymorphic_allocator<std::byte>
, so allocator_type::value_type
will generally not match slist::value_type
(except the case where both are std::byte
);
So this container does not satisfy the requirements of an allocator-aware container most of the time.
But there's also no requirement for it to do so.
=> well-formed
<sup>Note: It would be ill-formed if one would pass e.g. an slist<>
to a function that mandates that its parameter must be an allocator-aware container. - But as long as one avoids that there's no issue with defining almost-conforming containers.</sup>
3. How to write a container that works with any allocator
Note that std::pmr::polymorphic_allocator
satisfies the named requirement Allocator, exactly like std::allocator
does.
(All allocators that are intended to be used with standard containers must satisfy that requirement)
So the trick to support both is just to do nothing special - treat the std::pmr::polymorphic_allocator
like any other allocator, since it's just that. (use std::allocator_traits<Alloc>
for basically everything)
Note that this also means that you should respect the std::allocator_traits<Allocator>::propagate_on_
container_copy
/ container_move_assignment
/ container_swap
values.
Which for polymorphic_allocator
means that the allocator should not propagate when copying / moving / swapping the container.
Because doing so can lead to surprising lifetime issues - see for example this answer.
(Of course those should always be respected, not only just for polymorphic_allocator
s)
答案2
得分: 0
我花了最后一天时间整理所有我找到的关于分配器的资源,并设计出了一个通用的方案。我将在这里发布它,也许有人会觉得有帮助。
类似STL的分配器感知容器实现
#include <cstdio>
#include <vector>
#include <memory>
#include <memory_resource>
#include <type_traits> /* is_nothrow_swappable */
template <typename T, typename Allocator = std::allocator<T>>
struct MyContainer {
// 省略部分...
};
int main() {
// MyContainer<int, std::pmr::polymorphic_allocator<int>> ctr1 = 5;
MyContainer<std::string, std::pmr::polymorphic_allocator<std::string>> ctr1 = std::string{"Hello World!"};
MyContainer<double> ctr2 = 2.5;
}
注意:
一般形状:
- 该容器以分配器类型模板参数形式接受分配器,这意味着使用不同分配器类型的容器将完全不同类型(意味着它们将根本无法相互移动/复制/交换)。
- 所有资源管理(内存分配/释放+对象构造/销毁)必须通过分配器接口
std::allocator_traits<Allocator>
完成。不能直接使用allocator->allocate(),因为std::allocator_traits
为std::allocator<T>
(默认分配器)提供了默认值,这些默认值不一定在该类型中可用! - 通过对分配器的私有继承,可以实现空基类优化,以便无状态分配器不占用空间(注意,STL容器有时将分配器存储在标志节点/未使用的数据部分中)。这需要可以通过
static_cast
在get_alloc_()
中检索到非常量的分配器引用;
分配器传播:
不幸的是,分配器在移动、复制和交换三个操作上有一个定制点,允许它们传播到其他容器,这事实上只会造成困扰。但如果我们想遵循STL标准,我们需要考虑这些情况:
- 移动:如果
propagate_on_container_move_assignment::value
为真,则将b.allocator
移动分配给a.allocator
- 复制:如果
propagate_on_container_copy_assignment::value
为真,则将b.allocator
复制到a.allocator
- 交换:如果
propagate_on_container_swap::value
为真,则将a.allocator
与b.allocator
交换
构造函数
- 构造函数通常可以通过赋值运算符和委托构造函数轻松实现,该构造函数接受可选的分配器参数。
- 移动构造函数需要是双重的,因为在某些情况下移动可能不是
noexcept
的(如果分配器不同,则移动赋值运算符需要将数据复制到新的内存区域,可能会抛出异常)。只有在分配器相同时(运行时检查)或分配器是已知重复的情况下才可以noexcept
(在这种情况下我们知道它们是相同的)。
移动赋值
查看Howard Hinnant的这里的精彩回答。
- 问题:容器元素由一个容器分配并在移动后由另一个容器释放。如果不同的分配器在一个元素上执行分配/释放,这被官方认定为UB!
- 如果分配器从开始就相同,或者我们知道
other.allocator
传播到我们这里(可以在编译时检查,alloc_traits::propagate_on_container_move_assignment::value
为真),这就没问题,我们可以移动。 - 但是如果分配器不匹配并且
other.allocator
不传播,我们必须使用我们的分配器通过复制构造所有元素到新的内存区域。
复制赋值
- 如果
alloc_traits::propagate_on_container_copy_assignment::value
评估为true,则首先复制分配器。 - 然后将所有元素从另一个容器复制分配。
交换
- 交换永远不能抛出异常(根据标准始终
noexcept
)。这意味着只有在分配器相同时或交换时传播分配器时(alloc_traits::propagate_on_container_swap
)才实际上可以进行交换。 - 我们可以选择在这里断言(如libstdc++所做),但这可能会阻碍有经验的用户,他们确保他的两个分配器可以彼此正常工作,因此我认为留给用户来确保他的容器定义良好。
noexcept
- 标准规定,交换始终是
noexcept
(§23.2.1[container.requirements.general]第8和10节)。交换将可能通过对成员字段的未限定的交换调用使用noexcept
移动来交换内部成员字段。 - 只有当
propagate_on_container_move_assignment::value
和is_nothrow_move_assignable<allocator_type>::value
都评估为true时,移动赋值运算符才能标记为noexcept
(因为在这种情况下分配器也会移动到新对象中)。 - 复制赋值/构造永远不能保证
noexcept
,因为它分配并构造新元素,即请求系统内存并调用构造函数。在理论上只有在容器仍然为空的情况下可能是noexcept
,但是C++不支持运行时检查。 - 扩展移动构造函数只有在我们知道分配器相同时并且
英文:
I've spent the last day assembling any resource I've found about allocators and came up with a generic design. I will post it here, someone might find it helpful.
Generic stl-like allocator aware container implementation
#include <cstdio>
#include <vector>
#include <memory>
#include <memory_resource>
#include <type_traits> /* is_nothrow_swappable */
template <typename T, typename Allocator = std::allocator<T>>
struct MyContainer {
using allocator_type = Allocator;
using alloc_traits = typename std::allocator_traits<Allocator>;
auto get_allocator() const -> Allocator& {
return allocator_;
}
MyContainer(Allocator allocator = {})
: vec_{ allocator }
{}
MyContainer(T val, Allocator allocator = {})
: MyContainer(allocator)
{
res_ = alloc_traits::allocate(allocator_, sizeof(T));
alloc_traits::construct(allocator_, res_, std::move(val));
}
~MyContainer() {
if (res_) {
alloc_traits::destroy(allocator_, res_);
alloc_traits::deallocate(allocator_, res_, sizeof(T));
}
}
MyContainer(const MyContainer& other, Allocator allocator = {})
: MyContainer(allocator)
{
// Copy resource
res_ = alloc_traits::allocate(allocator_, sizeof(T));
alloc_traits::construct(allocator_, res_, *other.res_);
// Copy types with value semantics
vec_ = other.vec_;
}
MyContainer(MyContainer&& other) noexcept
: MyContainer(std::move(other), other.get_allocator())
{}
MyContainer(MyContainer&& other, Allocator allocator = {})
: MyContainer(allocator)
{
// Move resource
res_ = std::move(other.res_);
other.res_ = nullptr;
// Move types with value semantics
vec_ = std::move(other.vec_);
}
auto operator=(MyContainer&& other) noexcept(
std::conjunction_v<alloc_traits::propagate_on_container_move_assignment,
std::is_nothrow_move_assignable<Allocator>>) -> MyContainer&
{
if constexpr(std::disjunction_v<
typename alloc_traits::propagate_on_container_move_assignment,
typename alloc_traits::is_always_equal>)
{
MyContainer tmp{ std::move(other), allocator_ };
swap_data_(tmp);
allocator_ = std::move(other.allocator_);
} else {
if (allocator_ != other.allocator_) {
// Must copy
MyContainer tmp{ other, allocator_ };
swap_data_(tmp);
} else {
MyContainer tmp{ std::move(other), allocator_ };
swap_data_(tmp);
}
}
return *this;
}
auto operator=(const MyContainer& other) -> MyContainer& {
// copy construct from other with our allocator
MyContainer tmp(other, allocator_);
swap_data_(tmp);
if constexpr (alloc_traits::propagate_on_container_copy_assignment::value) {
allocator_ = other.allocator_;
}
return *this;
}
auto swap(MyContainer& other) noexcept -> void {
// UB in case propagate_on_container_swap is true and allocators are not the same
// However no assert triggered as this could be weirdly intended by a knowing user
swap_data_(other);
if constexpr (alloc_traits::propagate_on_container_swap) {
swap(allocator_, other.allocator_); // Swap always noexcept
}
}
friend auto swap(MyContainer& lhs, MyContainer& rhs) -> void {
lhs.swap(rhs);
}
private:
auto swap_data_(MyContainer& other) noexcept {
// TBAA information for compiler optimization will not be lost unless
// unqualified swap uses XOR swap semantics.
swap(other.res_, res_);
swap(other.vec_, vec_);
}
std::vector<T, Allocator> vec_; // To model propagation
T* res_ = nullptr;
#ifdef _MSC_VER
[[msvc::no_unique_address]]
#else
[[no_unique_address]]
#endif
Allocator allocator_;
};
int main() {
// MyContainer<int, std::pmr::polymorphic_allocator<int>> ctr1 = 5;
MyContainer<std::string, std::pmr::polymorphic_allocator<std::string>> ctr1 = std::string{"Hello World!"};
MyContainer<double> ctr2 = 2.5;
}
Notes:
General shape:
- The container takes an allocator type template argument, which necessarily means that containers using a different allocator type will be a different type entirely (meaning they will not be move/copy/swap-able to each other at all).
- All resource management (allocating / freeing of memory + constructing / destroying of objects) must be done through the allocator interface
std::allocator_traits<Allocator>
. You can never use allocator->allocate() directly asstd::allocator_traits
provides defaults even forstd::allocator<T>
(the default allocator) which are not necessarily available in the type! - private inheritance from the allocator allows for empty base optimization so stateless allocators don't take up space (note stl-containers sometimes store allocator in a sentinel node / unoccupied data section). This requires a non-const allocator& to be retrievable through static_cast in
get_alloc_()
;
Allocator propagation:
Unfortunately, allocators have a customization point in which they are allowed to propagate to other containers on the three operations move, copy and swap, which in hindsight caused only harm but if we want to conform to the stl we need to account for these cases:
- move: b.allocator will be move assigned to a.allocator in case
propagate_on_container_move_assignment::value
is true - copy: b.allocator will be copied to a.allocator in case
propagate_on_container_copy_assignment::value
is true - swap: a.allocator will be swaped with b.allocator in case
propagate_on_container_swap::value
is true
Constructors
- Constructors can generally be easily implemented in terms of assignement operators and a delegating constructor, which gobbles up the optional allocator argument.
- The move ctor needs to be twofold as in some cases move cannot be noexcept (in case allocators differ the move assignement operator needs to copy data over to new memory arena which might throw). Noexcept move is only possible if the allocator is the same (runtime check) OR the allocator is knowingly duplicated (in which case we know it's the same).
move assignement
Check out Howard Hinnant's brilliant answer here.
- Problem: Container elements are allocated by one container and deallocated by the other container after move. In case different allocators perform the allocation / deallocation on one element this is officially UB!
- If allocators are the same from the start or we know that other.allocator propagates to us (can be checked at compile-time,
alloc_traits::propagate_on_container_move_assignment::value
is true) this is no problem, we can move. - But if allocators mismatch and other.allocator doesn't propagate we must copy construct all elements to the new memory arena using our allocator.
copy assignement
- Copy allocator first if
alloc_traits::propagate_on_container_copy_assignment::value
evaluates to true. - Then copy assign all elements from the other container.
swap
- Swap must never throw (is always noexcept according to standard), hence we cannot do fancy copy stuff (as this would allocate / construct which could throw). This means it's only actually possible to really swap if allocators are the same or propagated on swap (
alloc_traits::propagate_on_container_swap
). - We could choose to assert here (as is done by libstdc++) but that would potentially hinder an experienced user who made sure his two allocators work with each other, so my take is to leave it to the user to make his containers well defined.
- Include friend swap that is found by ADL so that we correspond to idiomatic unqualified swap principle.
- needs an internal function swap_data() that only swaps the guts of our container without allocator, as allocators are conditionally swapped.
noexcept
- Swap is always noexcept as defined by the standard (§23.2.1[container.requirements.general] sections 8 and 10). Swap will presumably use noexcept move through unqualified swap-calls to the member fields to exchange internal member fields.
- The move assignment operator can be marked noexcept only if both
propagate_on_container_move_assignment::value
andis_nothrow_move_assignable<allocator_type>::value
evaluate to true (as the allocator is also moved to the new object in case). - Copy assignement / construction can never guarantee
noexcept
as it allocates and constructs new elements, i.e. asks for system memory and calls constructors. Could in theory
be noexcept only if the container is still empty, but runtime checks are not supported by C++. - The move constructor can only be noexcept if we know allocators are the same and we can
noexcept
move the elements, hence we include an extended move ctor that can't guarantee that as it takes an allocator argument to leave the default move ctornoexcept
. The extended move ctor could be noexcept if alloctors compare equal but runtime checks are not supported by C++.
Exception safety, copy & swap:
- The copy & swap - idiom cannot be used in stl-conforming allocater aware containers as the idiom foresees implementing the assignement operators based on copy/move constructor. However, the allocator propagation traits might differ between copy/move assignement, so those need to be seperated. Copy & swap can however still be implemented for fields excluding allocator in the
move_assign_()
-method. - Only basic exception safety can be given (as is the standard for stl-containers).
Unresolved questions:
- Would it be theoretically possible that an object is copy/move constructed with a certain allocator which is then switched for a propagating allocator from the other object right away? (potential optimization point).
- Is it necessary to include an additional branch to check for self assignement (note: the branch would be repeated by copy ctor as it delegates to operator=(&))?
- Possible inefficiency as we runtime dispatch on allocator equality in case of noexcept move because we delegate to operator=(&&). Maybe call move_assign_ directly?
- noexcept move with extended move constructor looks like conditionally possible if allocators propagate anyway OR compare always equal.
- In case other core data also stores an allocator (e.g. is an stl-container), we can push our allocator to the leaf and retrieve it from there to save cache. For this to work, the allocators within a container hierarchy must always match up (I must look into this more to make sure this is the case for above implementation!)
Maybe someone can help me with the last questions!
rev1
I added changed things that came to mind:
- cppreference says that noexcept functions may call functions that throw an exception. I used this to delegate to the extended move ctor without losing optimization (for conciseness):
> Note that a noexcept specification on a function is not a compile-time
> check; it is merely a method for a programmer to inform the compiler
> whether or not a function should throw exceptions.
- I fixed resource allocation. Previously I had just an empty assign() function, but ofc this needed implementation.
- Fixed a few overheads:
-- nulling the res pointer in case of destruction
-- check for allocator equality when move constructing when allocator is propagated anyway. - Fixed bug: I noticed that I cannot delegate from constructors to assignement as this would check for propagation traits (which allows the weird behaviour of constructing with one allocator then propagating the allocator from the other object right away)
- I used copy & swap idiom now also for copy assignement which gets rid of an extra
free_res_()
function and adds exception safety. I will have to check again but I think assignement has now strong exception safety. - used C++20 feature
no_unique_address
as a more elegant way to address allocator storage for empty allocator types.
Sources:
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论