在CppCon上进行关于分配器的演讲之后,我遇到了以下代码:
#include <iostream>
#include <string>
#include <utility>
#include <vector>
namespace {
template <typename T>
class MyAllocator {
public:
using value_type = T;
MyAllocator(std::string iType) : _type(std::move(iType)) {}
T* allocate(const std::size_t iNo) { return new T[iNo]; }
void deallocate(T* iPtr, const std::size_t) { delete[] iPtr; }
constexpr bool operator!=(const MyAllocator& oth) const {
return _type != oth._type;
}
const std::string& getType() const noexcept { return _type; }
private:
std::string _type;
};
using MyString =
std::basic_string<char, std::char_traits<char>, MyAllocator<char>>;
} // anonymous namespace
int main(int, char**) {
::MyString str1(::MyAllocator<char>("ForStr1"));
::MyString str2(::MyAllocator<char>("ForStr2"));
::MyString str3(::MyAllocator<char>("ForStr3"));
std::vector<::MyString> aVector;
aVector.reserve(1024);
aVector.push_back(str1);
aVector.push_back(str2);
std::cout << "[0]: " << aVector[0].get_allocator().getType() << "\n"
<< "[1]: " << aVector[1].get_allocator().getType() << "\n";
aVector.insert(aVector.begin(), str3);
const auto& type0 = aVector[0].get_allocator().getType();
const auto& type1 = aVector[1].get_allocator().getType();
const auto& type2 = aVector[2].get_allocator().getType();
std::cout << "[0]: " << type0 << "\n"
<< "[1]: " << type1 << "\n"
<< "[2]: " << type2 << "\n";
return 0;
}
我想这里的一般主题是关于“嵌套容器中的分配器”。虽然从功能上来说,我明白了这个问题,但我无法理解代码中发生了什么。
在代码中,我们有一个自定义的分配器,本质上,它的行为类似于默认分配器,只是它在内部存储一种数据。
我使用同一分配器的三个不同实例构建三个不同的字符串:
using MyString =
std::basic_string<char, std::char_traits<char>, MyAllocator<char>>;
::MyString str1(::MyAllocator<char>("ForStr1"));
::MyString str2(::MyAllocator<char>("ForStr2"));
::MyString str3(::MyAllocator<char>("ForStr3"));
现在我有一个简单的
std::vector<MyString>
:
std::vector<::MyString> aVector;
aVector.reserve(1024);
我预留空间是为了避免重新分配。
现在我推前两根弦:
aVector.push_back(str1);
aVector.push_back(str2);
std::cout << "[0]: " << aVector[0].get_allocator().getType() << "\n"
<< "[1]: " << aVector[1].get_allocator().getType() << "\n";
// As expected, it prints:
// [0]: ForStr1
// [1]: ForStr2
打印的结果正是我所期望的。我假设分配器由
std::string
容器拥有。
但是如果我用以下命令强制进行一些复制/移动(重新排列):
aVector.insert(aVector.begin(), str3);
// Now we have vector be like:
// [str3:ForStr3] [str1:ForStr1] [str2:ForStr2]
然后,与向量内的字符串关联的分配器似乎已损坏:
const auto& type0 = aVector[0].get_allocator().getType();
const auto& type1 = aVector[1].get_allocator().getType();
const auto& type2 = aVector[2].get_allocator().getType();
std::cout << "[0]: " << type0 << "\n"
<< "[1]: " << type1 << "\n"
<< "[2]: " << type2 << "\n";
打印:
[0]: ForStr1
[1]: ForStr2
[2]: ForStr2
我期望:
[0]: ForStr3
[1]: ForStr1
[2]: ForStr2
为什么会有这种行为?有没有我错过的UB? 与
std::string
关联的分配器是对象本身的一部分,不是吗?
这是分配器水平传播的结果,由容器根据POCCA、POCMA和POCS分别执行复制分配、移动分配和交换操作。
在您的示例中,
insert()
成员函数移动两个已经存在的元素以释放新元素的第一个位置。该过程可以总结为以下方案。
str2
对象是在数组末尾移动构造的,使原始对象处于有效但未定义的状态(*str2
)。在移动构造期间,分配器也会移动。值得注意的是,移出的分配器保证与移至的分配器相等,因此 *str2
对象仍然包含其原始分配器。 +-----------------------------------
| str1(a1) | *str2(a2) | str2(a2) |
+-----------------------------------
str1
对象被移动分配给 *str2
对象。容器通过 propagate_on_container_move_assignment
接口检查 std::allocator_traits
类型特征。由于分配器不提供这样的类型,因此该类将选择其默认类型,该类型的计算结果为 false。这意味着分配器无法传播到目标对象,必须采用其分配器。 +-----------------------------------
| *str1(a1) | str1(a2) | str2(a2) |
+-----------------------------------
str1
对象被移动分配给 *str2
对象。与上一步类似,容器通过 propagate_on_container_copy_assignment
接口检查 std::allocator_traits
类型特征。由于分配器不提供这样的类型,因此该类将选择其默认类型,该类型的计算结果为 false。这意味着分配器无法传播到目标对象,必须采用其分配器。 +----------------------------------
| str3(a1) | str1(a2) | str2(a2) |
+----------------------------------