为什么std::vector没有release方法?

6

我发现自己处于一种情况下,希望有一个类似于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的内部实现造成一些限制吗?

或者有我尴尬地错过的方法吗?


为什么会这样呢?如果您认为应该有这样的功能,可以向C++委员会进行游说。请记住,他们通常反对在核心容器中添加无意义的花哨功能。为什么不直接删除并重新创建std::vector对象呢?我认为您会发现为此实现“release”非常困难。 - tadman
@Kerrek SB 是的,我在思考这种问题,但是我不能明确地看到重点。std::vector是否以一种简单的 delete [] 无法处理的方式来处理销毁? - Emerald Weapon
2
@EmeraldWeapon:请仔细考虑“capacity”和“reserve”的语义。(再次强调,“int”太简单了。) - Kerrek SB
C++11允许使用http://en.cppreference.com/w/cpp/container/vector/shrink_to_fit(虽然这对于实现来说是可选的)。 - Kenny Ostrom
我认为它应该有一个“release”函数。这样,数据就可以移动到与std::vector无关的类中。我认为STL相关的移动语义修改过于保守。还有一些细节需要解决,例如如何传递有关分配器的信息。但我想如果release返回一个unique_ptrshared_ptr,那么这个问题就可以解决。(https://dev59.com/AZHea4cB1Zd3GeqPnlNV) - alfC
显示剩余2条评论
5个回答

5

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中提取内存缓冲区并直接使用可能是合理的。但它不能仅仅是一个指针,还需要一个分配器。


谢谢,这正是我想知道的。在std::vector内部,分配/释放内存并不是一件简单的事情。 - Emerald Weapon

4
这个提议出现在N4359中,但事实证明有一些微妙的问题需要调用者避免不正确的行为(似乎大部分与分配器有关)。该文讨论了困难和可能的替代方案,可在此处找到链接。最终被C++标准机构否决。更多讨论可在此问题及其答案的评论中找到。

2
我能想到的原因有两个:
  1. 在C++11之前,vector原本兼容小对象优化。也就是说,如果它的大小足够小,它可以指向自身。但这在C++11中被无意中禁用了(vector的移动语义禁止引用/迭代器失效),但这可能会在未来的标准中得到修复。所以,历史上没有提供它的理由,希望将来也不会提供。
  2. 分配器。如果您向一个使用了意料之外的分配器的vector传递指针,那么您的函数可能会导致未定义的行为。

2

这会对std::vector的内部实现造成一些限制吗?

以下是一些允许这样做会产生冲突的示例:

  • 除非特殊情况,否则底层内存分配无法通过new T[]获得,也不能通过delete[]销毁,因为这些操作会在分配但实际上不应包含任何T类型对象的内存上调用构造函数和析构函数。
  • 数组的开头可能实际上并不是内存分配的开头;例如,向量可以在数组开始之前存储记录信息。
  • vector在被销毁时可能实际上并没有释放内存;例如,分配可能来自一个小数组池,实现用于快速创建和销毁小向量。(此外,这些数组可能只是更大数组的切片)

谢谢,尤其是你提出的第一个要点。现在看起来很明显了。 - Emerald Weapon
分配可能来自一组小数组的池,这可能会破坏线程保证,除非该类型频繁锁定互斥锁。 - Nicol Bolas
@Nicol:嗯?我不明白除了构造、析构和调整大小之外,还有什么可能需要同步。如果向量总是延迟到分配器……那么,这只是将问题传递给另一个软件组件,该组件仍然必须管理线程保证。 - user1084944
@Hurkyl:每个插入操作都有可能触发重新调整大小的操作。这就是vector要做的很多事情。如果它正在访问全局内存池,那么该内存池必须被互斥锁定。此外,我不确定,但我相当确定容器实现需要使用分配器,而不是静态内存块。如果容器可以自己决定从哪里获取内存,那么替换它们的分配就没有意义了。 - Nicol Bolas
@Nicol:但是你不必在插入时锁定 - 只有在插入触发调整大小时才需要锁定,即使如此,也只有在需要与共享缓存而不是线程本地缓存的小分配进行交互时才需要。即使 vector 没有这样做,这也可能是分配器正在执行的操作。我本可以直接向分配器提出申诉,但我认为“您不能释放并调用 delete[],因为 vector 被指定使用 allocator::deallocate 来执行释放操作”不会是对 OP 很有启发性的答案。 - user1084944
@Nicol:我知道容器类应该使用分配器来进行分配和释放,但是经过简短的浏览后,我并不清楚使用是否需要与“表面”操作一一对应。(例如,析构函数是否可以缓存内部分配以供稍后的构造函数使用)不过,我现在对这一点很感兴趣! - user1084944

0
我能够使用自定义分配器实现检索当前已分配数组的功能。以下代码展示了这个概念:
#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;
}

在 MSVC15 (VS2017)、gccclang 上进行了测试。输出基本上取决于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!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接