Я сделал простую программу кодирования Хаффмана для вывода индивидуальных кодировок символов и сохранения закодированного файла. Это было для задания, и мне сказали, что использование const_cast
на heap.top()
считается неопределенным поведением, если мы heap.pop()
потом, но я не уверен, что понимаю, почему.
Я прочитал cppreference относительно std::pop_heap
, которая является базовой функцией, вызываемой при вызове heap.pop()
, и я считаю, что nullptr
в сравнении все еще определен и понятен. Мне кажется, что когда я его отлаживал, он не работал ненормально.
Вот пример
#include <functional>
#include <queue>
#include <vector>
#include <iostream>
#include <memory>
template<typename T> void print_queue_constcast(T& q) {
while(!q.empty()) {
auto temp = std::move(const_cast<int&>(q.top()));
std::cout << temp << " ";
q.pop();
}
std::cout << '\n';
}
template<typename T> void print_queue(T& q) {
while(!q.empty()) {
std::cout << q.top() << " ";
q.pop();
}
std::cout << '\n';
}
int main() {
std::priority_queue<int> q1;
std::priority_queue<int> q2;
for(int n : {1,8,5,6,3,4,0,9,7,2}){
q1.push(n);
q2.push(n);
}
print_queue(q1);
print_queue_constcast(q2);
}
Может ли кто-нибудь объяснить, что на самом деле происходит в фоновом режиме, что было бы неопределенным поведением или что могло бы привести к сбою при определенных обстоятельствах?
Сделайте минимальный воспроизводимый пример пожалуйста...
Соответственно отредактировал вопрос. Я не думал, что это вопрос, требующий запуска кода, поскольку код работает правильно. Это скорее общий вопрос о методах кодирования и неопределенном поведении, поэтому я включил только пример, чтобы показать, что я имею в виду.
На каждый вопрос о коде должен быть минимальный воспроизводимый пример. Как человек, задающий вопрос, вы, как правило, не имеете права знать, что важно, а что нет. Давайте примем это решение, увидев ваш воспроизводимый код! В противном случае вы заставляете нас тратить больше времени на догадки :(
Понял и буду иметь это в виду, извиняюсь за путаницу.
Отлично, не беспокойтесь
Вы должны обменять int
на shared_ptr
, хотя ответ будет другим. Итак, вы изменили вопрос
tl;dr: Может быть; возможно, нет.
Как и набор, priority_queue отвечает за упорядочение своих элементов. Любая модификация элемента потенциально «нарушит» порядок, поэтому единственный безопасный способ сделать это — использовать собственные методы мутации контейнера. (На самом деле ни один из них не предоставляет такой возможности.) Непосредственное изменение элементов опасно. Чтобы обеспечить это, эти контейнеры предоставляют только const
доступ к вашим элементам.
Теперь на уровне языка объекты не будут иметь статического типа const T
; скорее всего они просто T
s. Таким образом, их изменение (после const_cast
для обмана системы типов) не имеет неопределенного поведения в этом смысле.
Однако вы потенциально нарушаете условие использования контейнера. Правила для priority_queue на самом деле никогда не говорят об этом, но поскольку его изменяющие операции определяются в терминах таких функций, как push_heap и pop_heap, использование таких операций нарушит предварительные условия этих функций, если порядок контейнера больше не удовлетворяет после вашей прямой мутации.
Таким образом, ваша программа будет иметь неопределенное поведение, если вы нарушите порядок, а затем измените priority_queue таким образом, чтобы это зависело от сохранения порядка. Если вы этого не сделаете, технически поведение вашей программы четко определено; однако в целом вы все равно будете играть с огнем. const_cast
должен быть крайней мерой.
Вопрос: вы нарушили порядок? Каково состояние элемента после перемещения из него, и удовлетворяется ли порядок, если объект в этом состоянии находится в начале очереди?
В исходном примере используется shared_ptr
s, и из документации мы знаем, что перемещенный shared_ptr
безопасно превращается в нулевой указатель.
Порядок priority_queue
по умолчанию определяется std::less
, что дает строгий общий порядок необработанных указателей; std::less
на shared_ptr
фактически вызовет его базовый случай operator<
, но , который, в свою очередь, определен для вызова std::less на его эквивалент необработанного указателя .
К сожалению, это не означает, что нулевой shared_ptr упорядочивается «первым»: , хотя порядок указателей std::less является строгим и полным, где нулевые указатели приземляются в этом порядке, не указано .
Таким образом, не указано, нарушит ли ваша мутация порядок, и, следовательно, не указано, будет ли ваш pop()
иметь неопределенное поведение.
(Пример MCVE с int
безопасен, потому что std::move
на int
нечего делать: он просто скопирует int
. Таким образом, порядок остается неизменным.)
Я бы согласился с тем, что предположительно было вашим мотивом, что, к сожалению, pop()
не возвращает вам выскочившую вещь, которую вы могли бы тогда move
из. Подобные ограничения с наборами и картами — вот почему теперь у нас есть функции сплайсинга узлов для этих контейнеров. Для Priority_queue такого нет, это просто оболочка вокруг другого контейнера, такого как вектор. Если вам нужен более детальный контроль, вы можете заменить его собственным, который имеет нужные вам функции.
В любом случае, ради приращения shared_ptr
(как в вашем исходном коде), я бы, вероятно, просто взял копию, если у вас нет действительно экстремальных требований к производительности. Таким образом, вы знаете, что все будет четко определено.
Конечно, ради int
копии (как в вашем MCVE) std::move
совершенно бессмысленно (нет косвенных ресурсов, которые можно было бы украсть!) и вы все равно делаете копию, так что вопрос довольно спорный и все, что вам нужно' мы сделали, чтобы создать более сложный код без всякой причины.
Я бы также рекомендовал не писать код, где вы должны спросить, хорошо ли он определен, даже если окажется, что это так. Это не идеально для удобочитаемости или ремонтопригодности.
@Jack Пожалуйста, просмотрите мое редактирование - мне пришлось несколько изменить свой ответ
Все хорошо! Спасибо за обширный ответ, я понимаю, где я сейчас нахожусь с кодом, как в отношении более простого примера int, так и в отношении примера с shared_ptr. В моем случае это было фактически неопределенное поведение, поскольку я использовал собственный компаратор, который не учитывал nullptr.
Хех, еще одна скрытая грань. Таким образом, прямой ответ на ваш вопрос мог быть даже неверным, потому что вы упустили еще одну важную деталь! Посмотрите, что я имею в виду о MCVE....
Ссылка на другой сайт SO не так хороша, как размещение вашего кода в вопросе.