Интеллектуальный указатель с локальной оптимизацией буфера

#c #optimization #design-patterns #smart-pointers

Вопрос:

Это пример интеллектуального указателя с удаленным удалителем типов и включенной оптимизацией локального буфера. Это из книги https://books.google.am/books/about/Hands_On_Design_Patterns_with_C .html?id=iQGGDwAAQBAJamp;printsec=frontcoveramp;source=kp_read_buttonamp;redir_esc=y#v=onepageamp;qamp;f=false

 #include <iostream>

template <typename T>
class smart_ptr
{
    struct deleter_base 
    {
        virtual ~deleter_base() {};
        virtual void apply(T* p) = 0;
    };
    
    template <typename Deleter>
    struct deleter : public deleter_base
    {
        deleter(Deleter d): m_d(d) {}
        void apply(T* p) override 
        {
            m_d(p);
        }
        Deleter m_d;
    };

    deleter_base* m_db;
    T* m_p;
    char m_buf[16];
    
public:
    template <typename Deleter>
    smart_ptr(T* p, Deleter d)
        : m_p(p)
        , m_db( sizeof(Deleter) > sizeof(m_buf) ? new deleter<Deleter>(d) : new (m_buf) deleter<Deleter>(d))
    {
    }
    
    ~smart_ptr() 
    {
        m_db->apply(m_p);
        if (static_cast<void*>(m_db) == static_cast<void*>(m_buf))
        {
            m_db->~deleter_base();
        }
        else 
        {
            delete m_db;
        }
    }
    
    Tamp; operator*() {return *m_p;}
    const Tamp; operator*() const {return *m_p;}
    T* operator->() { return m_p; }
    const T* operator->() const { return m_p; }
};

struct Test 
{
    Test(double d): m_d(d) { std::cout << "Test::Test" << std::endl; }
    ~Test() { std::cout << "Test::~Test" << std::endl; }
    
    double m_d;
};

int main()
{
    auto deleter = [](Test * t) {delete t;};
    smart_ptr<Test> spd(new Test(3.6), deleter);
    
    std::cout << spd->m_d << std::endl;
    
}
 

Один вопрос. Когда удалитель не помещается в локальный буфер, мы выделяем для него память и оставляем m_buf неинициализированным. Это ошибка или я что-то пропустил?

В деструкторе мы сравниваем m_buf , какое значение имеет случайное значение, со значением, возвращаемым new deleter<Deleter>(d) . Не исключено, что эти значения совпадают, верно?

Комментарии:

1. m_buf обеспечивает хранение (используется с размещением нового). До тех пор, пока реализация всегда полагается на m_db ссылку на удалитель, независимо от того, является ли m_buf он неинициализированным, если он не используется. Вы можете войти. с { } тем, чтобы сделать это более надежным, но это можно рассматривать как пессимизацию.

2. Ошибка-это что-то (не) работающее неправильно. Как содержимое буфера «неправильно», если мы его не используем?

3. Но мы используем его внутри деструктора smart_ptr. Мы читаем там из неинициализированной памяти, нет ?

4. Мы сравниваем адрес буфера с чем-то. До этого мы не получаем доступ к его содержимому.

5. …но с явной проверкой перед: if (static_cast<void*>(m_db) == static_cast<void*>(m_buf)) что защищает случай, когда он не является неинициализированным.

Ответ №1:

Это не ошибка — допустимо иметь неинициализированную память. У вас есть неопределенное поведение (UB) только при чтении из неинициализированной памяти.

Это имеет смысл — если просто наличие неинициализированной памяти уже было бы ошибкой, то размещение нового, например, в smart_ptr описании выше, было бы бессмысленным. Но это безопасно, потому что все начинается с записи.

Комментарии:

1. m_buf будет содержать случайное значение, что делать, если случайно содержимое m_db совпадает с этим случайным значением. Разве это не невозможно, верно?

2.@Ashot: Он действительно будет содержать случайное значение, если не используется для Deleter . Но m_buf адрес не будет случайным. Это будет немного больше, чем this из-за предыдущего m_db и m_p .

3. m_buf адрес не имеет отношения к делу. Мы не сравниваем адрес m_buf , но это ценность. Он сравнивается со значением, возвращаемым new deleter<Deleter>(d) . Я не вижу причины, по которой эти ценности всегда должны быть разными.

4. @Ашот: Это неправильно. m_buf используется как static_cast<void*>(m_buf) . Теперь m_buf это массив, поэтому это приведение вызывает неявное преобразование из char[16] в char* , а затем явное преобразование из char* в void* . Это char* адрес m_buf[0] начала массива.