Не определено ли поведение для освобождения указателя, возвращаемого глобальным оператором замены new, без вызова оператора замены delete? (C ++ 17) - PullRequest
3 голосов
/ 20 апреля 2020

Считается неопределенным поведением вызывать SL operator delete для ненулевого указателя, который не был возвращен SL operator new, как описано здесь для (1) и (2): https://en.cppreference.com/w/cpp/memory/new/operator_delete

Поведение реализации этой функции стандартной библиотеки не определено, если ptr не является нулевым указателем или указателем, ранее полученным из реализации стандартной библиотеки operator new(size_t) или operator new(size_t, std::nothrow_t).

Поэтому также неопределенное поведение смешивать использования operator new, operator delete и operator new[], operator delete[]. Я не могу найти ничего в стандарте, утверждая, что это также относится к замене operator new и operator delete, которые вызывают метод распределения пользователя. Как пример:

void* operator new(std::size_t p_size)
{
    void *ptr = UserImplementedAlloc(p_size);
    return ptr;
}

void* operator new[](std::size_t p_size)
{
    void *ptr = UserImplementedAlloc(p_size);
    return ptr;
}

void operator delete(void* p_ptr)
{
    UserImplementedFree(p_ptr);
}

void operator delete[](void* p_ptr)
{
    UserImplementedFree(p_ptr);
}

Будет ли следующее неопределенным? Предполагая, что UserImplementedAlloc всегда возвращает правильный адрес и никогда не nullptr.

struct Simple
{
    explicit Simple(); //Allocates m_bytes
    ~Simple(); //Frees m_bytes
    char * m_bytes;    
};

/*Placement new is not replaced or overridden for these examples.*/

//Example A
{
    //Allocates and invokes constructor
    Simple* a = new Simple();
    //Invokes destructor
    a->~Simple();
    //Deallocates
    UserImplementedFree(static_cast<void*>(a));
}

//Example B
{
    //Allocates
    void* addr = UserImplementedAlloc(sizeof(Simple));
    //Invokes constructor
    Simple* b = new (addr) Simple();
    //Invokes destructor and deallocates
    delete b;
}

Я не ищу лекции о том, является ли это плохой практикой, я просто пытаюсь определить, является ли это определенным поведением или нет.

Ответы [ 2 ]

0 голосов
/ 24 апреля 2020

Оба примера имеют неопределенное поведение. Теперь, когда я потратил время на просмотр стандартного окончательного варианта C ++ 17 , я нашел необходимые мне доказательства.


Пример A

Относительно operator new:

Функции распределения - § 6.7.4.1.2

Если запрос выполнен успешно, возвращается значение должно быть ненулевым значением указателя (7.11) p0, отличным от любого ранее возвращенного значения p1, если только это значение p1 не было впоследствии передано operator delete

В примере A мы вызываем new-expression , Simple* a = new Simple(), которое внутренне будет вызывать соответствующий operator new. Мы обходим operator delete, когда мы звоним UserImplementedFree(static_cast<void*>(a)). Даже если operator delete вызовет эту функцию и, вероятно, сделает то же самое освобождение, подвох в том, что любые последующие вызовы operator new теперь могут потенциально возвращать указатель, который соответствует адресу, который имел a. И a никогда не передавался operator delete. Итак, мы нарушили указанное выше правило.


Пример B

delete-expression - § 8.3.5.2

... значение операнда удаления может быть значением нулевого указателя, указателем на объект, не являющийся массивом, созданным предыдущим новым выражением, или указателем на подобъект (4.5), представляющий базовый класс такой объект (пункт 13). Если нет, поведение не определено. Во второй альтернативе (массив удаления) значением операнда удаления может быть нулевое значение указателя или значение указателя, которое возникло из предыдущего массива new-expression. 83 Если нет, поведение не определено.

В примере B мы не выделяем addr через new-expression . И затем мы пытаемся использовать delete-expression для его освобождения. Что нарушает вышеприведенное правило.


Как бы выглядело определенное поведение?

Основной особенностью этих примеров является отделение конструкции от распределения и отделение разрушения от освобождения. Стандарт гласит следующее:

new-expression - § 8.3.4.11

Для массивов char, unsigned char и std::byte разница между результатом выражения new-expression и адресом, возвращаемым функцией выделения, должна быть целым кратным самого строгого требования фундаментального выравнивания (6.11) любого типа объекта, размер которого не превышает размер создаваемого массива. [Примечание: поскольку предполагается, что функции выделения возвращают указатели на хранилище, которые соответствующим образом выровнены для объектов любого типа с фундаментальным выравниванием, это ограничение на накладные расходы на выделение массивов допускает общую идиому распределения массивов символов, в которую будут поступать объекты других типов позже будет размещен . - конец примечания]

Таким образом, определенное поведение потенциально может выглядеть так:

{
    //Allocates bytes
    char* bytes = new char[sizeof(Simple)];
    //Invokes constructor
    Simple* a = new ((void *)bytes) Simple();
    //Invokes destructor
    a->~Simple();
    //Deallocates
    delete[] bytes;
}

Еще раз, не обязательно хорошая практика, но определенное поведение.

0 голосов
/ 20 апреля 2020

Версия удаления вашего компилятора может знать что-то, скрытое в реализации, об удаленном указателе, который зависит от типа.

Сначала вызывать деструктор вручную, а затем удалять пустоту * (потому что в противном случае вы вызываете деструктор дважды), это небезопасно. Вы не удаляете тот же самый указатель в семантике C ++. Это один и тот же адрес на уровне сборки, и он может освободить тот же объем памяти - или вы действительно это знаете? Вы обманули компилятор, чтобы удалить void * вместо фактического типа.

...