Создание безопасного держателя буфера на C ++ - PullRequest
6 голосов
/ 10 июля 2020

Бывают ситуации, когда мне нужно передать буфер char* туда и обратно. Моя идея - создать объект, который может содержать объект, владеющий данными, но также предоставлять данные как char* для чтения. Поскольку этот объект содержит владельца, утечек памяти нет, потому что владелец уничтожается вместе с объектом, когда он больше не нужен. бывает. На самом деле я знаю, как это исправить, но мой класс как бы заманил меня к этому. Поэтому я считаю то, что я сделал, плохим, и, возможно, есть более безопасный способ сделать это на C ++.

Пожалуйста, взгляните на мой класс, который содержит владельца буфера, а также необработанный указатель на этот буфер. Я использовал GenericObjectHolder как что-то, что содержит для меня владельца, без того, чтобы мой класс Buffer параметризовался этим владельцем.

#include <iostream>
#include <string>
#include <memory>
#include <queue>

//The library:
class GenericObjectHolder
{
public:
    GenericObjectHolder()
    {
    }

    virtual ~GenericObjectHolder() {
        
    };
};

template <class T, class Holder = GenericObjectHolder>
class Buffer final
{
public:
    //Ownership WILL be passed to this object
    static Buffer fromOwned(T rawBuffer, size_t size)
    {
        return Buffer(std::make_unique<T>(rawBuffer), size);
    }

    //Creates a buffer from an object that holds the buffer
    //ownership and saves the object too so it's only destructed
    //when this buffer itself is destructed
    static Buffer fromObject(T rawBuffer, size_t size, Holder *holder)
    {
        return Buffer(rawBuffer, std::make_unique<T>(rawBuffer), size, holder);
    }

    //Allocates a new buffer with a size
    static Buffer allocate(size_t size)
    {
        return Buffer(std::make_unique<T>(new T[size]), size);
    }

    ~Buffer()
    {
        if (_holder)
            delete _holder;
    }

    virtual T data()
    {
        return _rawBuffer;
    }

    virtual size_t size() const
    {
        return _size;
    }

    Buffer(T rawBuffer, std::unique_ptr<T> buffer, size_t size)
    {
        _rawBuffer = rawBuffer;
        _buffer = std::move(buffer);
        _size = size;
    }

    Buffer(T rawBuffer, std::unique_ptr<T> buffer, size_t size, Holder *holder)
    {
        _rawBuffer = rawBuffer;
        _buffer = std::move(buffer);
        _size = size;
        _holder = holder;
    }


    Buffer(const Buffer &other)
        : _size(other._size),
          _holder(other._holder),
          _buffer(std::make_unique<T>(*other._buffer)) 
    {
    }


private:
    Holder *_holder;
    T _rawBuffer;
    std::unique_ptr<T> _buffer;
    size_t _size = 0;
};

//Usage:
template <class T>
class MyHolder : public GenericObjectHolder
{
public:
    MyHolder(T t) : t(t)
    {
    }

    ~MyHolder()
    {
    }

private:
    T t;
};

int main()
{
    std::queue<Buffer<const char*, MyHolder<std::string>>> queue;
    std::cout << "begin" << std::endl;
    {
        //This string is going to be deleted, but `MyHolder` will still hold
        //its buffer
        std::string s("hello");
        auto h = new MyHolder<std::string>(s);
    
        auto b = Buffer<const char*, MyHolder<std::string>>::fromObject(s.c_str(),s.size(), h);
        queue.emplace(b);
    }
    {
        auto b = queue.front();
        //We try to print the buffer from a deleted string, segfault
        printf(b.data());
        printf("\n");
    }
    std::cout << "end" << std::endl;
}

Как видите, строка s копируется внутри объекта держатель, но разрушается сразу после него. Поэтому, когда я пытаюсь получить доступ к необработанному буферу, принадлежащему buffer, я получаю segfault.

Конечно, я мог бы просто скопировать буфер из строки s в новый буфер внутри моего объекта, но это ' d быть бесполезным.

Может быть, есть лучший способ сделать это, или, может быть, есть даже что-то готовое в C ++, которое делает то, что мне нужно.

PS: string - это просто пример. На практике я мог иметь дело с любым типом объекта, которому принадлежит буфер char*.

Живой пример: https://repl.it/repls/IncredibleHomelySdk

Ответы [ 3 ]

6 голосов
/ 10 июля 2020

Ваша основная проблема в том, что вы хотите, чтобы ваш Держатель был подвижным. Но когда объект Owner перемещается, буферный объект также может перемещаться. Это сделает ваш указатель недействительным. Вы можете избежать этого, поместив владельца в фиксированное место в куче через unique_ptr:

#include <string>
#include <memory>
#include <queue>
#include <functional>

template <class B, class Owner>
class Buffer
{
public:
    Buffer(std::unique_ptr<Owner>&& owner, B buf, size_t size) :
      _owner(std::move(owner)), _buf(std::move(buf)), _size(size)
    {}

    B data() { return _buf; }
    size_t size() { return _size; }

private:
    std::unique_ptr<Owner> _owner;
    B _buf;
    size_t _size;
};

//Allocates a new buffer with a size
template<typename T>
Buffer<T*, T[]> alloc_buffer(size_t size) {
    auto buf = std::make_unique<T[]>(size);
    return {std::move(buf), buf.get(), size};
}

Вот ссылка для ответа: https://repl.it/repls/TemporalFreshApi

Если вы хотите иметь буфер со стиранием типа, вы можете сделать это следующим образом:

template <class B>
class Buffer
{
public:
    virtual ~Buffer() = default;
    B* data() { return _buf; }
    size_t size() { return _size; }

protected:
    Buffer(B* buf, size_t size) : 
      _buf(buf), _size(size) {};
    B* _buf;
    size_t _size;
};


template <class B, class Owner>
class BufferImpl : public Buffer<B>
{
public:
    BufferImpl(std::unique_ptr<Owner>&& owner, B* buf, size_t size) :
      Buffer<B>(buf, size), _owner(std::move(owner))
    {}
    
private:
    std::unique_ptr<Owner> _owner;
};

//Allocates a new buffer with a size
template<typename T>
std::unique_ptr<Buffer<T>> alloc_buffer(size_t size) {
    auto buf = std::make_unique<T[]>(size);
    return std::make_unique<BufferImpl<T, T[]>>(std::move(buf), buf.get(), size);
}

Опять же, ответная ссылка: https://repl.it/repls/YouthfulBoringSoftware#main. cpp

0 голосов
/ 12 июля 2020

Класс std :: unique_ptr уже является «держателем буфера», который «гарантирует удаление», без копий строк, без висящих ссылок и без ошибок сегментов:

#include <iostream>
#include <queue>
#include <memory>

int main()
{
    std::queue<std::unique_ptr<std::string>> queue;
    std::cout << "begin" << std::endl;
    {
        auto h = std::make_unique<std::string>("Hello");
        queue.emplace( std::move(h) ); // move into the queue without copy
    }
    {
        auto b = std::move(queue.front());  // move out from queue without copy
        std::cout << *b << std::endl;
    }  // when b goes out of scope it delete the string
    std::cout << "end" << std::endl;
}

https://godbolt.org/z/neP838

0 голосов
/ 10 июля 2020

Вы написали:

Бывают ситуации, когда мне нужно передавать буфер char * туда и обратно.

и

Так что я считаю то, что я сделал, плохим, и, возможно, есть более безопасный способ сделать это на C ++.

Не совсем понятно, кто вы стремясь, но когда мне это нужно, я иногда использую std::vector<char> - std::vectorstd::string) - это всего лишь управляемый буфер. Вызов data() для вектора даст вам необработанный указатель на буфер, который будет передаваться на устаревшие интерфейсы et c. или по какой-то причине вам просто нужен буфер, которым вы управляете сами. Подсказка: используйте resize() или конструктор для выделения буфера.

Итак, вы видите, что в вашем примере нет необходимости хранить внутренний указатель std::string. Вместо этого просто позвоните по номеру data() при необходимости.

Похоже, вас беспокоят копии и эффективность. Если вы используете объекты, поддерживающие семантику перемещения и , вы используете семейство emplace функций, то не должно происходить никакого копирования, по крайней мере, в . Все / большинство контейнеров также поддерживает перемещение.

...