为什么std::vector没有释放方法?

Why std::vector does not have a release method?

我发现自己处于一种情况,我希望有一个类似 unique_ptrrelease()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 中提取内存缓冲区并直接使用它可能是合理的。但它不能仅仅是一个指针。它必须有一个分配器。

我能想到的原因有两个:

  1. 最初(C++11 之前),vector 与小对象优化兼容。也就是说,如果它的尺寸足够小,它可以指向自己。这在 C++11 中被无意中禁用(vector 的移动语义禁止使 references/iterators 无效),但它可能会在未来的标准中得到修复。所以,历史上没有理由提供它,希望将来不会有。
  2. 分配器。如果传递指向带有分配器的向量的指针,您的函数可能会调用未定义的行为

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!