5

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?

ceztko
  • 14,736
  • 5
  • 58
  • 73
Emerald Weapon
  • 2,392
  • 18
  • 29
  • Why would it? If you think it should have such a thing, maybe lobby the C++ committee. Keep in mind they're usually opposed to adding pointless frills to the core containers. Why not just delete and recreate the `std::vector` object? I think you'll find implementing `release` for this is extremely non-trivial. – tadman Nov 09 '16 at 19:17
  • Your example is too simplistic. How would you know which destructors to call? How would you find the correct allocator? – Kerrek SB Nov 09 '16 at 19:18
  • @Kerrek SB Yes, I was thinking about this kind kind of problematics, but I can't see the point precisely. Does `std::vector` handle destruction in a way that a simple `delete []` would not be able to? – Emerald Weapon Nov 09 '16 at 19:22
  • Gosh dang it. I hate having this C++ dupe hammer. [You can completely clear the contents of a vector if you want (and free the memory)](http://stackoverflow.com/questions/10464992/c-delete-vector-objects-free-memory), and I suspect that may solve your underlying problem, but if not you might have to expand on your question so a better alternative can be suggested. `std::vector` doesn't allow you to steal ownership of the underlying data like `std::unique_ptr` does. – Cornstalks Nov 09 '16 at 19:22
  • 1
    @EmeraldWeapon: Please contemplate the semantics of `capacity` and `reserve`. (And again, `int` is too simple.) – Kerrek SB Nov 09 '16 at 19:23
  • c++11 allows http://en.cppreference.com/w/cpp/container/vector/shrink_to_fit (although it is optional for the implementation) – Kenny Ostrom Nov 09 '16 at 19:38
  • I think it *should* have a `release` function. In this way, the data could be moved to classes unrelated to `std::vector`. I think the modifications to the STL related move semantics where too conservative. Some details have to be worked out, for example how to pass the information about the allocator. But I guess that can be passed if release returns a `unique_ptr` or a `shared_ptr` (https://stackoverflow.com/questions/33845132/using-stdunique-ptr-with-allocators) – alfC Dec 13 '17 at 09:46

5 Answers5

4

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.

alfC
  • 14,261
  • 4
  • 67
  • 118
Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
4

This was proposed in 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 this question and its answers.

Stuart Berg
  • 17,026
  • 12
  • 67
  • 99
2

There are two reasons I can think of:

  1. 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.
  2. allocators. Your function is likely to invoke undefined behaviour if passed a pointer to a vector with allocator it didn't expect
krzaq
  • 16,240
  • 4
  • 46
  • 61
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 by delete[], since these would call constructors and destructors on memory that that is allocated but should not actually contain any objects of type T.
  • 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)
  • Thanks, especially for the first of your points. It now looks obvious. – Emerald Weapon Nov 09 '16 at 19:48
  • "*the allocation might come from a pool of small arrays*" Which would break threading guarantees, unless the type locks mutexes very frequently. – Nicol Bolas Nov 10 '16 at 23:35
  • @Nicol: Eh? I don't see what other than construction, destruction, and resizing could potentially require synchronization. If vector always deferred to the allocator... well, that's just passing the problem onto a different software component that still has to manage threading guarantees. –  Nov 10 '16 at 23:53
  • @Hurkyl: Every insertion operation is a potential resize operation. So that's a lot of things that `vector` does. If it were accessing a global pool of memory, then that pool would have to be mutex-locked. Also, I'm not certain, but I'm fairly sure that container implementations are required to use allocators, rather than static blocks of memory. If containers could take it on themselves as to where memory comes from, there would be no point in being able to replace their allocations. – Nicol Bolas Nov 11 '16 at 00:02
  • @Nicol: But you don't have to lock on insert -- you would only have to lock when the insert triggers a resize and even then, only if it needs to interact with a shared cache rather than a thread local cache of small allocations). And even if `vector` didn't do it, this might be a thing the allocator is doing. I could have just appealed to the allocator directly in my answer, but I don't think "you can't release and call `delete[]` because `vector` is mandated to use `allocator::deallocate` to do deallocations" would have been a very informative answer to the OP. –  Nov 11 '16 at 00:08
  • @Nicol: I know that the container classes are supposed to use the allocator to do allocations and deallocations, but after some brief skimming, it's not obvious to me that the usage needs to be in one-to-one correspondence with "apparent" operations. (e.g. that the destructor could cache the internal allocation for use by a later constructor) I am curious now on this point, though! –  Nov 11 '16 at 00:14
0

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!
ceztko
  • 14,736
  • 5
  • 58
  • 73