我想为basic_string
创建一个自定义分配器,该分配器使我可以获取已分配的字符串内部数组的所有权。我的特定用例是.NET互操作方案,在该方案中将字符串封送回托管代码非常昂贵,因为它要求将字符串分配到特定的池中(至少在Windows中),更重要的是,该数组在堆中的所有权必须转移。我能够为std::vector
编写这样的自定义分配器,并成功验证了主要编译器(MSVC,gcc,clang)的兼容性。我现在尝试对std::vector
使用相同的分配器,并且观察到奇怪的行为,因为所有主要的STL实现似乎都没有为第一次分配(通常是前16个字节)使用提供的分配器。它遵循我正在使用的代码:
basic_string
下面是代码的输出,显示未使用分配器。在MSVC,clang和gcc(#include <memory>
#include <stdexcept>
#include <vector>
#include <iostream>
// The requirements for the allocator where taken from Howard Hinnant tutorial:
// https://howardhinnant.github.io/allocator_boilerplate.html
template <typename T>
struct MyAllocation
{
size_t Size = 0;
std::unique_ptr<T> Ptr;
MyAllocation() { }
MyAllocation(MyAllocation && other) noexcept
: Ptr(std::move(other.Ptr)), Size(other.Size)
{
other.Size = 0;
}
};
// This allocator keep ownership of the last allocate(n)
template <typename T>
class MyAllocator
{
public:
using value_type = T;
private:
// This is the actual allocator class that will be shared
struct Allocator
{
[[nodiscard]] T* allocate(std::size_t n)
{
T *ret = new T[n];
if (!(Current.Ptr == nullptr || CurrentDeallocated))
{
// Actually release the ownership of the Current unique pointer
Current.Ptr.release();
}
Current.Ptr.reset(ret);
Current.Size = n;
CurrentDeallocated = false;
return ret;
}
void deallocate(T* p, std::size_t n)
{
(void)n;
if (Current.Ptr.get() == p)
{
CurrentDeallocated = true;
return;
}
delete[] p;
}
MyAllocation<T> Current;
bool CurrentDeallocated = false;
};
public:
MyAllocator()
: m_allocator(std::make_shared<Allocator>())
{
std::cout << "MyAllocator()" << std::endl;
}
template<class U>
MyAllocator(const MyAllocator<U> &rhs) noexcept
{
std::cout << "MyAllocator(const MyAllocator<U> &rhs)" << std::endl;
// Just assume it's a allocator of the same type. This is needed in
// MSVC STL library because of debug proxy allocators
// https://github.com/microsoft/STL/blob/master/stl/inc/vector
m_allocator = reinterpret_cast<const MyAllocator<T> &>(rhs).m_allocator;
}
MyAllocator(const MyAllocator &rhs) noexcept
: m_allocator(rhs.m_allocator)
{
std::cout << "MyAllocator(const MyAllocator &rhs)" << std::endl;
}
public:
T* allocate(std::size_t n)
{
std::cout << "allocate(" << n << ")" << std::endl;
return m_allocator->allocate(n);
}
void deallocate(T* p, std::size_t n)
{
std::cout << "deallocate(\"" << p << "\", " << n << ")" << std::endl;
return m_allocator->deallocate(p, n);
}
MyAllocation<T> release()
{
if (!m_allocator->CurrentDeallocated)
throw std::runtime_error("Can't release the ownership if the current pointer has not been deallocated by the container");
return std::move(m_allocator->Current);
}
public:
// This is the instance of the allocator that will be shared
std::shared_ptr<Allocator> m_allocator;
};
// We assume allocators of different types are never compatible
template <class T, class U>
bool operator==(const MyAllocator<T>&, const MyAllocator<U>&) { return false; }
// We assume allocators of different types are never compatible
template <class T, class U>
bool operator!=(const MyAllocator<T>&, const MyAllocator<U>&) { return true; }
int main()
{
std::cout << "Test MyAllocator<char>" << std::endl;
using MyString = std::basic_string<char, std::char_traits<char>, MyAllocator<char>>;
MyAllocator<char> allocator;
MyString str(allocator);
str = "0123456789ABCDE"; // 16 bytes including null termination. No use of the allocator
// str = "0123456789ABCDEF"; // 17 bytes including null termination. Here the allocator is used,
// tipically doubling the space required
}
链接)中类似:
Wandbox
相反,如果我的分配需要超过16个字节,例如我的代码中的注释行,则输出在gcc中是这样的(在MSVC中具有类似的输出,在clang中需要> = 24个字节:
Test MyAllocator<char>
MyAllocator()
MyAllocator(const MyAllocator &rhs)
这显示了所有STL实现之间的通用模式,因为看起来它们只是忽略了对小字符串的分配器的使用,这是一种优化。可悲的是,库开发人员并没有做到干净,因为他们可能完全像我在做的那样将任何行为封装在字符串的自定义分配器中,可能会浪费分支中的CPU周期(甚至是存储)。问题如下:C ++标准是否不需要在所有数据分配中使用分配器?字符串是否有特殊的子句/例外?对于Test MyAllocator<char>
MyAllocator()
MyAllocator(const MyAllocator &rhs)
allocate(31)
deallocate("0123456789ABCDEF", 31)
,相同的代码似乎也可以正常工作。
您将看到的是短字符串优化(SSO)。该标准允许使用较小的内部缓冲区构建std::vector
,该字符串可用于避免进行任何动态内存分配。这是非常有利的,因为大多数字符串都很小,因此您可以节省很多分配。
遗憾的是,标准中对此缓冲区的大小没有限制。 MSVC使用16个字符,libc ++使用22个字符。
这意味着您要么需要确保分配的字符串足够大以使用分配器,要么只需要实现自己的字符串类即可。分配足够内存的一种技巧是使用
std::vector
由于缓冲区是字符串的一部分,如果您要求更多的内存,则它必须动态分配内存的字符串大小。
字符串是否有特殊的子句/例外?对于
std::string
,相同的代码似乎也可以正常工作。
std::string str;
str.reserve(sizeof(str) + 1);
要求移动向量不会使任何指针/引用/迭代器无效,这意味着它不能具有这样的缓冲区。 std::vector
没有允许实施SSO的要求。
这在std::vector
中被提及,它谈到std::string
和Table 71,复杂度要求是NoteB,其中对于除X u(rv)
以外的所有容器,NoteB是恒定复杂度,具有线性复杂度。