问题
I found myself in a situation where I would have liked to have an analog of unique_ptr
's release()
for std::vector<>
. E.g.:
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);
Is there a particular reason why this kind of possibility is not provided? May that impose some constraint on std::vector
's the internal implementation?
Or there is a way to achieve this that I am embarrassingly missing?
回答1:
May that impose some constraint on std::vector's the internal implementation?
Here are some examples of things that allowing this would conflict with:
- Barring special cases, the underlying memory allocation cannot be obtained by
new T[]
, nor destroyed bydelete[]
, since these would call constructors and destructors on memory that that is allocated but should not actually contain any objects of typeT
. - The beginning of the array might not actually be the beginning of the memory allocation; e.g. the vector could store bookkeeping information just before the start of the array
vector
might not actually free the memory when destroyed; e.g. instead the allocation might come from a pool of small arrays that the implementation uses for quickly creating and destroying small vectors. (furthermore, these arrays might all just be slices of a larger array)
回答2:
functionUsingAndInternallyDeletingRowPointer
And what exactly would this function do? Because that memory was allocated by calling std::allocator_traits<std::allocator<T>>::allocate
, which expects it to be deleted by calling std::allocator_traits<std::allocator<T>>::deallocate
. Furthermore, each element of the vector
was constructed with a call to std::allocator_traits<std::allocator<T>>::construct
, and therefore must be destroyed by a call to std::allocator_traits<std::allocator<T>>::destroy
.
If that function tries to do delete []
on that pointer, it won't work. Or at the very least, it isn't required to work.
It might be reasonable to be able to extract a memory buffer from a vector
and use it directly. But it could not be a mere pointer. It would have to have an allocator along with it.
回答3:
There are two reasons I can think of:
- originally (pre-C++11),
vector
was compatible with small object optimization. That is, it could've pointed into itself if its size was small enough. This was inadvertently disabled in C++11 (vector
's move semantics forbid invalidating of references/iterators), but it may be fixed in future standards. So, there was no reason to provide it historically, and hopefully there won't be in the future. - allocators. Your function is likely to invoke undefined behaviour if passed a pointer to a vector with allocator it didn't expect
回答4:
This was proposed in N4359, but it turns out there are some subtle issues that place a 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 this question and its answers.
回答5:
I was able to implement the functionality to retrieve the current allocated array using a custom allocator. The following code show the concept:
#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('\0');
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;
}
Tested with MSVC15 (VS2017), gcc and clang. The output is pretty much the following, depending also on small differences on STL implementation of std::vector
and debug compilation enabled:
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!
来源:https://stackoverflow.com/questions/40514362/why-stdvector-does-not-have-a-release-method