为什么std::vector没有释放方法?
Why std::vector does not have a release method?
我发现自己处于一种情况,我希望有一个类似 unique_ptr
的 release()
的 std::vector<>
。例如:
std::vector<int> v(SOME_SIZE);
//.. performing operations on v
int* data = v.release(); // v.size() is now 0 and the ownership of the internal array is released
functionUsingAndInternallyDeletingRowPointer(data);
没有提供这种可能性有什么特别的原因吗?这可能会对 std::vector
的内部实现施加一些限制吗?
或者有一种方法可以实现这一点,我很尴尬地想念它?
functionUsingAndInternallyDeletingRowPointer
这个函数到底有什么作用?因为该内存是通过调用 std::allocator_traits<std::allocator<T>>::allocate
分配的,它期望通过调用 std::allocator_traits<std::allocator<T>>::deallocate
将其删除。此外,vector
的每个元素都是通过调用 std::allocator_traits<std::allocator<T>>::construct
构造的,因此必须通过调用 std::allocator_traits<std::allocator<T>>::destroy
.
来销毁
如果该函数试图对那个指针执行 delete []
,它将无法工作。或者至少,不需要 工作。
能够从 vector
中提取内存缓冲区并直接使用它可能是合理的。但它不能仅仅是一个指针。它必须有一个分配器。
我能想到的原因有两个:
- 最初(C++11 之前),
vector
与小对象优化兼容。也就是说,如果它的尺寸足够小,它可以指向自己。这在 C++11 中被无意中禁用(vector
的移动语义禁止使 references/iterators 无效),但它可能会在未来的标准中得到修复。所以,历史上没有理由提供它,希望将来不会有。
- 分配器。如果传递指向带有分配器的向量的指针,您的函数可能会调用未定义的行为
May that impose some constraint on std::vector's the internal implementation?
以下是一些允许这样做会与以下内容发生冲突的示例:
- 除非特殊情况,底层内存分配 不能 由
new T[]
获得,也不能由 delete[]
销毁,因为它们会调用构造函数和析构函数已分配但实际上不应包含任何 T
类型对象的内存。
- 数组的开头实际上可能不是内存分配的开头;例如该向量可以在数组开始之前存储簿记信息
vector
销毁时可能不会真正释放内存;例如相反,分配可能来自一个小数组池,该实现用于快速创建和销毁小向量。 (此外,这些数组可能只是一个更大数组的切片)
这是在 N4359, but it turns out there are some subtle issues that place burdens on the caller to avoid incorrect behavior (mostly related to allocators, it seems). A discussion of the difficulties and possible alternatives can be found here. It was ultimately rejected by the C++ standards body. Further discussion can be found in comments 及其答案中提出的。
我能够使用自定义分配器实现检索当前分配数组的功能。下面的代码展示了这个概念:
#ifdef _MSC_VER
#define _CRT_SECURE_NO_WARNINGS
#endif
#include <cassert>
#include <cstring>
#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()
{
MyAllocator<char> allocator;
{
std::vector<char, MyAllocator<char>> test(allocator);
test.resize(5);
test.resize(std::strlen("Hello World") + 1);
std::strcpy(test.data(), "Hello World");
std::cout << "Current buffer: " << test.data() << std::endl;
test.pop_back();
test.push_back('!');
test.push_back('[=10=]');
try
{
(void)allocator.release();
}
catch (...)
{
std::cout << "Expected throw on release() while the container has still ownership" << std::endl;
}
}
auto allocation = allocator.release();
std::cout << "Final buffer: " << allocation.Ptr.get() << std::endl;
return 0;
}
使用 MSVC15 (VS2017) 测试,gcc and clang。输出大致如下,还取决于 std::vector
的 STL 实现和启用的调试编译的细微差异:
MyAllocator()
MyAllocator(const MyAllocator &rhs)
allocate(5)
allocate(12)
deallocate("", 5)
Current buffer: Hello World
allocate(18)
deallocate("Hello World!", 12)
Expected throw on release() while the container has still ownership
deallocate("Hello World!", 18)
Final buffer: Hello World!
我发现自己处于一种情况,我希望有一个类似 unique_ptr
的 release()
的 std::vector<>
。例如:
std::vector<int> v(SOME_SIZE);
//.. performing operations on v
int* data = v.release(); // v.size() is now 0 and the ownership of the internal array is released
functionUsingAndInternallyDeletingRowPointer(data);
没有提供这种可能性有什么特别的原因吗?这可能会对 std::vector
的内部实现施加一些限制吗?
或者有一种方法可以实现这一点,我很尴尬地想念它?
functionUsingAndInternallyDeletingRowPointer
这个函数到底有什么作用?因为该内存是通过调用 std::allocator_traits<std::allocator<T>>::allocate
分配的,它期望通过调用 std::allocator_traits<std::allocator<T>>::deallocate
将其删除。此外,vector
的每个元素都是通过调用 std::allocator_traits<std::allocator<T>>::construct
构造的,因此必须通过调用 std::allocator_traits<std::allocator<T>>::destroy
.
如果该函数试图对那个指针执行 delete []
,它将无法工作。或者至少,不需要 工作。
能够从 vector
中提取内存缓冲区并直接使用它可能是合理的。但它不能仅仅是一个指针。它必须有一个分配器。
我能想到的原因有两个:
- 最初(C++11 之前),
vector
与小对象优化兼容。也就是说,如果它的尺寸足够小,它可以指向自己。这在 C++11 中被无意中禁用(vector
的移动语义禁止使 references/iterators 无效),但它可能会在未来的标准中得到修复。所以,历史上没有理由提供它,希望将来不会有。 - 分配器。如果传递指向带有分配器的向量的指针,您的函数可能会调用未定义的行为
May that impose some constraint on std::vector's the internal implementation?
以下是一些允许这样做会与以下内容发生冲突的示例:
- 除非特殊情况,底层内存分配 不能 由
new T[]
获得,也不能由delete[]
销毁,因为它们会调用构造函数和析构函数已分配但实际上不应包含任何T
类型对象的内存。 - 数组的开头实际上可能不是内存分配的开头;例如该向量可以在数组开始之前存储簿记信息
vector
销毁时可能不会真正释放内存;例如相反,分配可能来自一个小数组池,该实现用于快速创建和销毁小向量。 (此外,这些数组可能只是一个更大数组的切片)
这是在 N4359, but it turns out there are some subtle issues that place burdens on the caller to avoid incorrect behavior (mostly related to allocators, it seems). A discussion of the difficulties and possible alternatives can be found here. It was ultimately rejected by the C++ standards body. Further discussion can be found in comments
我能够使用自定义分配器实现检索当前分配数组的功能。下面的代码展示了这个概念:
#ifdef _MSC_VER
#define _CRT_SECURE_NO_WARNINGS
#endif
#include <cassert>
#include <cstring>
#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()
{
MyAllocator<char> allocator;
{
std::vector<char, MyAllocator<char>> test(allocator);
test.resize(5);
test.resize(std::strlen("Hello World") + 1);
std::strcpy(test.data(), "Hello World");
std::cout << "Current buffer: " << test.data() << std::endl;
test.pop_back();
test.push_back('!');
test.push_back('[=10=]');
try
{
(void)allocator.release();
}
catch (...)
{
std::cout << "Expected throw on release() while the container has still ownership" << std::endl;
}
}
auto allocation = allocator.release();
std::cout << "Final buffer: " << allocation.Ptr.get() << std::endl;
return 0;
}
使用 MSVC15 (VS2017) 测试,gcc and clang。输出大致如下,还取决于 std::vector
的 STL 实现和启用的调试编译的细微差异:
MyAllocator()
MyAllocator(const MyAllocator &rhs)
allocate(5)
allocate(12)
deallocate("", 5)
Current buffer: Hello World
allocate(18)
deallocate("Hello World!", 12)
Expected throw on release() while the container has still ownership
deallocate("Hello World!", 18)
Final buffer: Hello World!