[C/C++] Undefined behavior [из Какому языку учить в школе?
Лучше опустить С и сразу начинать с С++ - так граблей поменьше и наступаешь на них только в профессиональной разработке.Прикольно набрасываешь, в компиляторе, написанном на C++, не смогли корректно реализовать простейшую оптимизацию. Как я понимаю, поэтому нужно учить С++ и фиксить баги.
PS, ну и разыменовывать поинтер в никуда ужасно, классическая ошибка.
PS, ну и разыменовывать поинтер в никуда ужасно, классическая ошибка.может и ошибка, но поведение компилятора контр-интутитивно. Видишь ошибку - выбрасывай ошибку компиляции.
И при этом что возникает проблема локальности. Это объявление realloc'а могло быть в совсем другом куске программы. А если посмотреть в отрыве от него, то код абсолютно валиден. Подставь туда любые два пойнтера, и станет ясно, что ситуация когда пойнтеры равны, а их значения - нет, не возможна.
В плюсах есть практически все из сей и много-много дополнительных, своих собсвенных "фишек".
Кстати о плюсах, кто-нить может пояснить следующее:
$ cat x.cc
#include <iostream>
#include <sstream>
struct Log
{
std::ostringstream buffer_;
std::ostringstream& stream { return buffer_; }
~Log { std::cout << buffer_.str; }
};
int main
{
Log.buffer_ << "First" << " line\n";
Log.stream << "Second" << " line\n";
return 0;
}
$ g++ -std=c++03 x.cc ; ./a.out
0x400df8 line
Second line
$ g++ -std=c++11 x.cc ; ./a.out
First line
Second line
$
what's the fucking going on?
может и ошибка, но поведение компилятора контр-интутитивно. Видишь ошибку - выбрасывай ошибку компиляции.
И при этом что возникает проблема локальности. Это объявление realloc'а могло быть в совсем другом куске программы. А если посмотреть в отрыве от него, то код абсолютно валиден. Подставь туда любые два пойнтера, и станет ясно, что ситуация когда пойнтеры равны, а их значения - нет, не возможна.
#include <iostream>
using namespace std;
int main
{
int *p = new int;
delete p;
int *q = new int;
*p = 1;
*q = 2;
if (p == q)
cout << *p << " " << *q << "\n";
return 0;
}
Ну и чем C++ лучше-то? Где ошибка компилятора? Это
#include <stdio.h>
#include <stdlib.h>
int main {
int *p = (int*)malloc(sizeof(int;
int *q = (int*)realloc(p, sizeof(int;
*p = 1;
*q = 2;
if ( p == q) {
*p = 1;
*q = 2;
printf("%d %d\n", *p, *q);
};
}
$ clang -O realloc.c; ./a.out
1 2
Ничем не лучше. Плюсы имеют ту же самую проблему.
Ну и ладненько.
Си - это язык, где есть адресная арифметика. И если два адреса равны между собой, не важно как именно они получены, хоть сложением, хоть умножением, хоть сложной функций вроде realloc, то в них должны храниться одинаковые значения.Ты ожидаешь слишком многого от undefined behaviour.
Вот ты берёшь код
if ( p == q) {
*p = 1;
*q = 2;
printf("%d %d\n", *p, *q);
};
Какого фига он должен показывать разные значения внутри p и q? Всё дело в том, что строчкой раньше встретилось *p = 1, и после этого в undefined behaviour попала вся остальная программа. Нормальные компиляторы, встретив выражение, после которого остальная программа теряет смысл, просто выдают ошибку и не пытаются собрать ложную программу. А clang поступает по-скотски, совсем как php, где чтобы программист не написал, всё будет интерпретировано, даже явная ошибка.
Я ожидаю предсказуемости.Погоди-погоди. Ты хочешь чтобы undefined behavior был undefined но при этом предсказуем? Тут как-бы содержится противоречие.
Нормальные компиляторы, встретив выражение, после которого остальная программа теряет смысл, просто выдают ошибку и не пытаются собрать ложную программу.Для этого компилятор должен со 100% точностью выявлять все такие случаи на этапе компиляции. Этого пока никто не научился делать.
Ну блин пофикси. Чего разнылся то. Или зарепорть багу. Бага или в конст фолдинге/пропагаторе или где-то в алиас анализе. Контекст содержит редандант стор(первый стор поэтому в тестсьюте его просто нет. Зарепортишь багу, добавят.
А то, что программист написал некорректный код и потом жалуется, что тот работает не так, как ему кажется что он должен - проблема программиста.
Вот вам еще пример говнокода, который работает "странно", причем даже без оптимизаций. Правда он только для i386 c cdecl (gcc -m32 в помощь).
#include <stdio.h>
void *create_ptr(void)
{
char c;
return &c + 11;
}
int main(void)
{
int *p = create_ptr;
*p = 0eadbeef;
printf("%x\n", *p);
printf("%x\n", *p);
return 0;
}
[-Wreturn-stack-address]
Нормально он работает. Если знать calling conventions можно даже использовать. Только стэки приватные для тредов и с многопоточкой могут быть проблемы. Вопрос только зачем.
Upd: хотя нет. Забыл про эксепшны.
Чувак, а ты точно в своем изначальном высказывании не перепутал С++ с Java'ой какой-нибудь?нет, в с++ все же не принято использовать malloc realloc и иже с ними, да и голыми указателями не рекомендуется пользоваться
Погоди-погоди. Ты хочешь чтобы undefined behavior был undefined но при этом предсказуем? Тут как-бы содержится противоречие.Да, я хочу, чтобы он не портил всю программу, а только локальную часть.
Потому что внутри блока if (p == q) уже никакой неопределённости нет. Известно что эти указатели равны, и раз один из них валиден, то валиден и второй.
так как один из них валидный указатель, а другой нетА не проще тогда сразу выдавать ошибку "невалидный указатель", чем вводить поведение "о, невалидный указатель, давайте я выкину каждую чётную строчку после него, ведь UB - имею право". Как-это по-свински.
Однопоточном мире в живет подаван юный.
if ( p == q) {
*p = 1;
*q = 2;
printf("%d %d\n", *p, *q);
};
Расскажи юному подавану, что здесь меняется в случае многопоточности?
if ( p == q) {
*p = 1;
// приешл другой поток и сделала q = malloc(sizeof(int;
*q = 2;
printf("%d %d\n", *p, *q);
};
Понятно. Я думал, что какая-то оптимизация может распараллелить автоматически код, а имелось в виду возможность явного обращения в коде к чужим данным. Увы, си от этого не застрахован, встроенного в язык механизма параллелизма нет, так что приходится смотреть за ним вручную. Расстановка синхронизации между потоками - это отдельная проблема, не относящаяся к обсуждаемой
Потому что внутри блока if (p == q) уже никакой неопределённости нет. Известно что эти указатели равны, и раз один из них валиден, то валиден и второй.Я тут немного почитал стандарт, и понял, что пример несколько более интересный, чем я думал. Видишь ли, следующий код тоже дает UB:
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *p = (int*)malloc(sizeof(int;
int *q = (int*)realloc(p, sizeof(int;
if (p == q) { // <- Shit happens here
*p = 1;
*q = 2;
printf("%d %d\n", *p, *q);
}
return 0;
}
Дело в том, что "The realloc function deallocates the old object pointed to by ptr and returns a pointer to a new object that has the size specified by size." Далее "The lifetime of an allocated object extends from the allocation until the deallocation." И наконец "If an object is referred to outside of its lifetime, the behavior is undefined. The value of a pointer becomes indeterminate when the object it points to reaches the end of its lifetime."
То есть после realloc само значение указателя p становится неопределенным, и любые попытки его использовать дают UB, в том числе сравнение с q на равенство.
А если я сделаю for (int * z = p; z != q; z++) и попробую перебором нащупать q, это тоже будет UB? То есть мне казалось логично, что значение сравнение может быть либо позитивным, либо негативным, но не суперпозицией. И UB означает, что компилятор волен выбрать любой вариант, он может полностью выкинуть if, если решит, что указатели разные. Но если решил, что одинаковые, то неопределённость должна исчезнуть.
1) для того, чтобы можно было писать оптимизации без оглядки на гвонокод, использующий соответствующие конструкции,
2) чтобы можно было генерировать эффективный код для разных архитектур, а не только для какой-то одной.
Иногда это может приводить к неинтуитивным последствиям, но что делать.
но что делать.Отлавливать такие ситуации на этапе компиляции?
Реаллок может быть control flow sensitive, тоесть стоять под предикатом. Рекурсивный вызов, принимающий аргументом указатель, передающийся в реаллок, может не доминировать/постдоминировать реаллок. В общем случае поинтерный анализ может иметь экспоненциальное время работы.
в с++ все же, стараются писать немного по-другому:
#include <iostream>
using namespace std;
int main
{
int *p = new int;
delete p;
int *q = new int;
*p = 1;
*q = 2;
if (p == q)
cout << *p << " " << *q << "\n";
return 0;
}
Ну и чем C++ лучше-то? Где ошибка компилятора? Это объявление realloc'а "delete p" могло быть в совсем другом куске программы.
auto p = std::make_shared<int>
auto q = p;
p.reset;
if(p)
*p = 1;
if(q)
*q = 2;
if(p == q)
cout << *p << " " << *q << "\n";
тут все гладенько и работает без WTF моментов
вот только твоё "гладенько" это семантически совсем другой код, тут даже нет использования указателя на удалённый объект
вот только твоё "гладенько" это семантически совсем другой код, тут даже нет использования указателя на удалённый объектну так смысл моего сообщения в том и заключается, что в с++ надо писать на с++ и не надо будет париться над указателями в никуда
все, что требует использования низкоуровневых вещей - обкладываем тестами, ассертами и изолируем последствия
иначе ССЗБ
если правильно проектировать, то становится понятно, что указатель на удаленный объект - это нерешенный вопрос о владении этим объектом (ну или просто тупой баг).
Ну и бездумное использование shared_ptr налево-направо (особенно если "забыть" об этом через использование удобненьких auto) вполне может спровоцировать утечки памяти, так что тогда уж и weak_ptr и unique_ptr стоит расчехлить - и в результате сложность просто перекочует из одного места программы в другое. И не факт, что станет безопасней. Ружжо всё равно нацелено на ногу!
Но это я так, не в качестве возражения)
и в результате сложность просто перекочует из одного места программы в другое.
ну типа сложность в принципе не уменьшается, борьба идет за ее явность и возможность эту сложность разделит на несколько частей
хотя, конечно, для этого требуется осознанное программирование, что не всегда возможно или целесообразно
в принципе, помогает простой принцип: когда создаешь объект, сразу думаешь когда он должен быть уничтожен, кто им владеет и кто от него зависит, если в этих вопросах нету ясности, то стоит отложить кодинг и задуматься
дальше уже идут принципы ооап
сложность в принципе не уменьшаетсяну это очень сильно сказано, хотя от принципа зависит
когда создаешь объект, сразу думаешь когда он должен быть уничтожен, кто им владеет и кто от него зависит, если в этих вопросах нету ясности, то стоит отложить кодинг и задуматьсяну вот ты отложишь и задумаешься, а некоторые решат что "умный указатель не зря умный, что-нибудь придумает" и наговнокодят прям на месте. Подозреваю, что исторически ты учил плюсы не с make_shared и auto?
одно время даже буст было сложно вставить - какие там умные указатели - auto_ptr только
Оставить комментарий
PooH
после такого:иногда хочется забиться в угол и плакать
сам столкнулся на своем проекте с похожим багом (UB, который оптимизировался в собачий бред)