Почему у std::vector нет метода выпуска?

Я оказался в ситуации, когда мне хотелось бы иметь аналог 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?

Или есть способ добиться этого, что мне не хватает?


person Emerald Weapon    schedule 09.11.2016    source источник
comment
С чего бы это? Если вы считаете, что у него должна быть такая вещь, возможно, лоббируйте комитет C++. Имейте в виду, что они обычно против добавления бессмысленных излишеств в основные контейнеры. Почему бы просто не удалить и не создать заново объект std::vector? Я думаю, вы обнаружите, что реализация release для этого чрезвычайно нетривиальна.   -  person tadman    schedule 09.11.2016
comment
Ваш пример слишком упрощен. Как узнать, какие деструкторы вызывать? Как найти правильный распределитель?   -  person Kerrek SB    schedule 09.11.2016
comment
@Kerrek SB Да, я думал о такого рода проблематике, но не вижу в этом смысла. Справляется ли std::vector с разрушением так, как не смог бы простой delete []?   -  person Emerald Weapon    schedule 09.11.2016
comment
Черт побери. Я ненавижу этот молоток C++. Вы можете полностью очистить содержимое вектора, если хотите (и освободить память), и я подозреваю, что это может решить вашу основную проблему, но если нет, вам, возможно, придется расширить свой вопрос, чтобы можно было предложить лучшую альтернативу. std::vector не позволяет вам украсть право собственности на базовые данные, как это делает std::unique_ptr.   -  person Cornstalks    schedule 09.11.2016
comment
@EmeraldWeapon: Пожалуйста, обратите внимание на семантику capacity и reserve. (И снова int слишком просто.)   -  person Kerrek SB    schedule 09.11.2016
comment
c++11 позволяет en.cppreference.com/w/cpp/container/vector /shrink_to_fit (хотя это необязательно для реализации)   -  person Kenny Ostrom    schedule 09.11.2016
comment
Я думаю, что он должен иметь функцию release. Таким образом, данные можно было перемещать в классы, не связанные с std::vector. Я думаю, что изменения в семантике перемещений, связанные с STL, слишком консервативны. Некоторые детали должны быть проработаны, например, как передать информацию о распределителе. Но я предполагаю, что это можно передать, если релиз возвращает unique_ptr или shared_ptr (stackoverflow.com/questions/33845132/)   -  person alfC    schedule 13.12.2017


Ответы (5)


Может ли это наложить некоторые ограничения на внутреннюю реализацию std::vector?

Вот несколько примеров того, с чем это может конфликтовать:

  • За исключением особых случаев, базовое выделение памяти не может быть получено с помощью new T[] или уничтожено с помощью delete[], поскольку они вызовут конструкторы и деструкторы для выделенной памяти, но фактически не должны содержать никаких объектов типа T.
  • Начало массива может не совпадать с началом выделения памяти; например вектор может хранить бухгалтерскую информацию непосредственно перед началом массива
  • vector может фактически не освобождать память при уничтожении; например вместо этого выделение может исходить из пула небольших массивов, которые реализация использует для быстрого создания и уничтожения небольших векторов. (более того, все эти массивы могут быть просто фрагментами большего массива)
person Community    schedule 09.11.2016
comment
Спасибо, особенно за первый пункт. Теперь это выглядит очевидным. - person Emerald Weapon; 09.11.2016
comment
выделение может исходить из пула небольших массивов, что нарушит гарантии многопоточности, если только тип не блокирует мьютексы очень часто. - person Nicol Bolas; 11.11.2016
comment
@ Никол: А? Я не вижу, что, кроме строительства, разрушения и изменения размера, потенциально может потребовать синхронизации. Если вектор всегда зависит от распределителя... что ж, это просто передача проблемы другому программному компоненту, который все еще должен управлять гарантиями многопоточности. - person ; 11.11.2016
comment
@Hurkyl: каждая операция вставки является потенциальной операцией изменения размера. Так что vector делает много вещей. Если бы он обращался к глобальному пулу памяти, то этот пул должен был бы быть заблокирован мьютексом. Кроме того, я не уверен, но вполне уверен, что реализации контейнеров должны использовать распределители, а не статические блоки памяти. Если бы контейнеры могли взять на себя ответственность за то, откуда берется память, не было бы смысла заменять их выделения. - person Nicol Bolas; 11.11.2016
comment
@Nicol: Но вам не нужно блокировать вставку - вам нужно будет блокировать только тогда, когда вставка вызывает изменение размера, и даже тогда, только если ей нужно взаимодействовать с общим кешем, а не с локальным кешем потока небольших распределений. ). И даже если vector этого не делал, это может быть то, что делает распределитель. Я мог бы просто обратиться к распределителю прямо в своем ответе, но я не думаю, что вы не можете освободить и вызвать delete[], потому что vector уполномочен использовать allocator::deallocate для освобождения, было бы очень информативным ответом на ОП. - person ; 11.11.2016
comment
@Nicol: я знаю, что классы контейнеров должны использовать распределитель для выделения и освобождения памяти, но после некоторого беглого просмотра мне не очевидно, что использование должно быть во взаимно однозначном соответствии с очевидными операциями. (например, что деструктор может кэшировать внутреннее распределение для использования более поздним конструктором). - person ; 11.11.2016

функцияUsingAndInternallyDeletingRowPointer

И что именно эта функция будет делать? Поскольку эта память была выделена вызовом 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 и использовать его напрямую. Но это не мог быть простой указатель. Вместе с ним должен быть распределитель.

person Nicol Bolas    schedule 09.11.2016
comment
Спасибо, это именно то, что мне было интересно. Выделение/освобождение выполняется нетривиальным образом внутри std::vector. - person Emerald Weapon; 09.11.2016

Это было предложено в N4359, но оказывается, что есть некоторые тонкие проблемы, которые возлагают бремя на вызывающую сторону, чтобы избежать неправильного поведения (похоже, в основном связанные с распределителями). Обсуждение трудностей и возможных альтернатив можно найти здесь. В конечном итоге он был отклонен органом по стандартизации C++. Дальнейшее обсуждение можно найти в комментариях этот вопрос и его ответы.

person Stuart Berg    schedule 10.01.2018

Есть две причины, о которых я могу думать:

  1. изначально (до C++11) vector был совместим с оптимизацией малых объектов. То есть он мог бы указывать на себя, если бы его размер был достаточно мал. Это было непреднамеренно отключено в C++11 (семантика перемещения vector запрещает аннулирование ссылок/итераторов), но это может быть исправлено в будущих стандартах. Таким образом, исторически не было причин предоставлять его, и, надеюсь, не будет в будущем.
  2. распределители. Ваша функция, вероятно, вызовет неопределенное поведение, если передаст указатель на вектор с распределителем, которого она не ожидала.
person krzaq    schedule 09.11.2016

Мне удалось реализовать функциональность для извлечения текущего выделенного массива с помощью пользовательского распределителя. Следующий код показывает концепцию:

#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), gcc и clang. Вывод в значительной степени следующий, в зависимости также от небольших различий в реализации STL std::vector и включенной отладочной компиляции:

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!
person ceztko    schedule 01.12.2019