Почему const_casting heap.top() из priority_queue имеет неопределенное поведение?

Я сделал простую программу кодирования Хаффмана для вывода индивидуальных кодировок символов и сохранения закодированного файла. Это было для задания, и мне сказали, что использование 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);
    
    
}

Может ли кто-нибудь объяснить, что на самом деле происходит в фоновом режиме, что было бы неопределенным поведением или что могло бы привести к сбою при определенных обстоятельствах?

Ссылка на другой сайт SO не так хороша, как размещение вашего кода в вопросе.

Nicol Bolas 12.12.2020 17:14

Сделайте минимальный воспроизводимый пример пожалуйста...

Asteroids With Wings 12.12.2020 17:17

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

Jack Avante 12.12.2020 17:17

На каждый вопрос о коде должен быть минимальный воспроизводимый пример. Как человек, задающий вопрос, вы, как правило, не имеете права знать, что важно, а что нет. Давайте примем это решение, увидев ваш воспроизводимый код! В противном случае вы заставляете нас тратить больше времени на догадки :(

Asteroids With Wings 12.12.2020 17:34

Понял и буду иметь это в виду, извиняюсь за путаницу.

Jack Avante 12.12.2020 17:38

Отлично, не беспокойтесь

Asteroids With Wings 12.12.2020 17:40

Вы должны обменять int на shared_ptr, хотя ответ будет другим. Итак, вы изменили вопрос

Asteroids With Wings 12.12.2020 17:44
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
7
156
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

tl;dr: Может быть; возможно, нет.


Безопасность на уровне языка

Как и набор, priority_queue отвечает за упорядочение своих элементов. Любая модификация элемента потенциально «нарушит» порядок, поэтому единственный безопасный способ сделать это — использовать собственные методы мутации контейнера. (На самом деле ни один из них не предоставляет такой возможности.) Непосредственное изменение элементов опасно. Чтобы обеспечить это, эти контейнеры предоставляют только const доступ к вашим элементам.

Теперь на уровне языка объекты не будут иметь статического типа const T; скорее всего они просто Ts. Таким образом, их изменение (после const_cast для обмана системы типов) не имеет неопределенного поведения в этом смысле.


Безопасность на уровне библиотеки

Однако вы потенциально нарушаете условие использования контейнера. Правила для priority_queue на самом деле никогда не говорят об этом, но поскольку его изменяющие операции определяются в терминах таких функций, как push_heap и pop_heap, использование таких операций нарушит предварительные условия этих функций, если порядок контейнера больше не удовлетворяет после вашей прямой мутации.

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


Итак, где мы находимся?

Вопрос: вы нарушили порядок? Каково состояние элемента после перемещения из него, и удовлетворяется ли порядок, если объект в этом состоянии находится в начале очереди?

  • В исходном примере используется shared_ptrs, и из документации мы знаем, что перемещенный 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 Пожалуйста, просмотрите мое редактирование - мне пришлось несколько изменить свой ответ

Asteroids With Wings 12.12.2020 17:52

Все хорошо! Спасибо за обширный ответ, я понимаю, где я сейчас нахожусь с кодом, как в отношении более простого примера int, так и в отношении примера с shared_ptr. В моем случае это было фактически неопределенное поведение, поскольку я использовал собственный компаратор, который не учитывал nullptr.

Jack Avante 12.12.2020 18:41

Хех, еще одна скрытая грань. Таким образом, прямой ответ на ваш вопрос мог быть даже неверным, потому что вы упустили еще одну важную деталь! Посмотрите, что я имею в виду о MCVE....

Asteroids With Wings 12.12.2020 22:20

Другие вопросы по теме