Понимать C ++ указатель времени жизни / зомби указатели - PullRequest
4 голосов
/ 16 октября 2019

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

Сначала немного базового понимания. Пожалуйста, исправьте меня, если какие-либо комментарии неправильны:

int* p = new int(1);
int* q = p;
// 1) p and q are valid and one can do *p, *q
delete q;
// 2) both are invalid and cannot be dereferenced. Also the value of both is unspecified
q = new int(42); // assume it returns the same memory
// 3) Both pointers are valid again and can be dereferenced

Я озадачен 2). Очевидно, что они не могут быть разыменованы, но почему нельзя использовать их значения (например, сравнивать одно с другим, даже несвязанный и действительный указатель?). Это указано примерно в 25: 38 . Я ничего не могу найти по этому поводу в cppreference , откуда я и получил 3).

Примечание. Предположение о том, что возвращается та же память, не может быть обобщено, как можетили не может случиться. Для этого примера следует принять как должное то, что он «случайно» возвратил ту же память, что и в примере с видео, и (возможно?), Необходимый для разрыва приведенного ниже кода.

Многопоточный примеркод из списка LIFO может быть смоделирован в одном потоке как:

Node* top = ... //NodeA allocated before;
Node* newnodeC = new Node(v); 
newnodeC->next = top;
delete top; top = nullptr;
// newnodeC->next is a zombie pointer
Node* newnodeD = new Node(u); // assume same memory as NodeA is returned
top = newnodeD;
if(top == newnodeC->next) // true
  top = newnodeC;
// Now top->next is (still) a zombie pointer

Это должно быть допустимо, если только Node не содержит нестатических константных членов или ссылок в соответствии с правилами в

Если новый объект создается по адресу, который был занят другим объектом, то все указатели, ссылки и имя исходного объекта будут автоматически ссылаться на новый объект и, как только начнется время жизни нового объекта,может использоваться для манипулирования новым объектом, но только если выполняются следующие условия [которыми они являются]

Так почему же это указатель зомби и предположительно UB?

Может ли(однопоточный) сжатый код, указанный выше, должен быть исправлен (при наличии констант): newnodeC->next = std::launder(newnodeC->next) с

Если tЕсли вышеперечисленные условия не выполнены, действительный указатель на новый объект все еще может быть получен путем применения барьера оптимизации указателя std :: launder

Я ожидаю, что это исправит «указатель зомби»и компиляторы, чтобы они не выдавали инструкции для назначения, а просто воспринимали их как барьер оптимизации (например, когда функция встроена и снова вызывается константный член)

Итак, в общем: я не слышал о "зомби указатели "раньше. Я прав, что любой указатель на уничтоженный / удаленный объект не может использоваться (для чтения [значения указателей] и разыменования [чтения указателя]), если указатель не переназначен или память не перераспределена с тем же типом объекта, воссозданным там (ибез постоянных / референтных членов)? Разве это не может быть исправлено C ++ 17 std::launder уже? (исключая многопоточные проблемы)

Также: при 3) в первом коде if(p==q) будет вообще вообще допустимым? Поскольку из моего понимания (второй части) видео недопустимо читать p.

Редактировать: В качестве объяснения, где я почти уверен, что происходит UB: Опять предположим, что по чистой случайностита же память возвращается с new:

// Global
struct Node{
  const int value;
};
Node* globalPtr = nullptr;
// In some func
Node* ptr = new Node{42};
globalPtr = ptr;
const int value = ptr->value;
foo(value);
// Possibly on another thread (if multi-threaded assume proper synchronisation so that this scenario happens)
delete globalPtr;
globalPtr = new Node{1337}; // Assume same memory returned
// First thread again (and maybe on serial code too)
if(ptr == globalPtr)
  foo(ptr->value);
else
  foo(globalPtr->value);

Согласно видео, после delete globalPtr также ptr является «указателем зомби» и не может быть использован (иначе «будет UB»)). Достаточно оптимизирующий компилятор может использовать это и предполагать, что pointee никогда не освобождается (особенно, когда удаление / новое происходит в другой функции / потоке / ...), и оптимизировать от foo(ptr->value) до foo(42)

Примечание. также упомянутый отчет о дефектах 260 :

Если значение указателя становится неопределенным, поскольку объект, на который указывает указатель, достиг конца своего времени жизни, все объекты, чей эффективный тип является указателем, иэта точка на один и тот же объект приобретает неопределенное значение. Таким образом, p в точке X и p, q и r в точке Z могут изменить свое значение.

Я думаю, что это является окончательным объяснением: после delete globalPtr значение ptrтакже является неопределенным. Но как это можно совместить с

Если новый объект создается по адресу, который был занят другим объектом, то все указатели [...] исходного объекта будут автоматически ссылаться на новый объект и [...] могут использоваться для манипулированияновый объект

Ответы [ 3 ]

1 голос
/ 16 октября 2019

Начиная с устройства, описанного в вопросе, уже выделенные int* p и:

int* q = p;
q = new int(42); // assume it returns the same memory

Теперь у нас есть сценарий, который идентичен этому:

int* p = new int(42);
int* q = p;

Потому что этосоответствует предварительным условиям, которые мы можем предположить, p и q указывают на одну и ту же область памяти;в этом случае тот факт, что это место было выделено, затем удалено, а затем снова выделено, не имеет значения. Ничто из того, что произошло до этого момента, не имеет значения, поскольку мы предполагаем, что два указателя находятся в состоянии, идентичном только что описанному.

Что касается "значение обоих не определено" на шаге # 2, я бы сказал, что значение q не определено в этой точке, потому что оно было передано delete, но значение p не изменяется.

Поведение delete здесь на самом деле не определено в C ++ 14, это , определенная реализация ;немного из некоторой документации delete:

Любое использование указателя, который стал недействительным таким образом, даже копирование значения указателя в другую переменную, является неопределенным поведением. (до C ++ 14)

Переадресация через указатель, который таким образом стал недействительным, и передача его в функцию освобождения (двойное удаление) - неопределенное поведение. Любое другое использование определяется реализацией. (начиная с C ++ 14)

https://en.cppreference.com/w/cpp/memory/new/operator_delete

Итак, чтобы ответить на то, что я считаю вашим вопросом, в этих обстоятельствах, то нет, ни то, ни другоена это указатель зомби.

Так почему же это указатель зомби и предположительно UB? или дезинформация.

1 голос
/ 24 октября 2019

Удаленное значение указателя имеет недопустимое значение указателя , а не неопределенное значение.

Начиная с C ++ 17 поведение недопустимых значений указателя определено в [basic.stc]/ 4:

Направление через недопустимое значение указателя и передача недопустимого значения указателя в функцию освобождения имеют неопределенное поведение. Любое другое использование недопустимого значения указателя имеет поведение, определяемое реализацией.

Таким образом, попытка сравнить два недопустимых указателя имеет поведение, определяемое реализацией. Есть сноска, поясняющая, что это может включать в себя ошибку времени выполнения.

В вашем первом фрагменте p имеет недопустимое значение указателя после удаления;все, что вы можете сделать с q, не имеет к этому никакого отношения. Неверные значения указателя не могут снова волшебным образом стать действительными. Невозможно (в рамках стандарта C ++) определить, находится ли новое распределение в «том же месте», что и предыдущее.

std::launder не поможет;снова вы используете недопустимое значение указателя и, следовательно, запускаете поведение, определяемое реализацией.

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

В своем вопросе вы упоминаетеC DR 260, однако это не имеет значения. C отличается от языка C ++. В C ++ удаленные указатели имеют недопустимое значение указателя , а не неопределенное значение .

1 голос
/ 16 октября 2019

Я бы не согласился с этим:

// 3) Both pointers are valid again and can be dereferenced

На самом деле, вас обманули, потому что на втором new программа обычно перераспределяет то же самоеблок памяти у него только что стал доступен, но на это нельзя полагаться (не гарантируется, что этот же блок памяти будет использоваться повторно).

Например, если вы используете хорошую практику для установки удаленных указателей на nullptr, следующая программа:

int main()
{
    int * p = new int(1);
    int * q = p;

    std::cout << (p == q) << std::endl;

    delete q;
    q = nullptr;
    p = nullptr;

    q = new int(42);

    std::cout << (p == q) << std::endl;

    delete q;

    return 0;
}

приведет к:

10

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...