[C/C++] Undefined behavior [из Какому языку учить в школе?

PooH


Лучше опустить С и сразу начинать с С++ - так граблей поменьше и наступаешь на них только в профессиональной разработке.
тостовато набрасываешь
после такого:
 

#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)
printf("%d %d\n", *p, *q);
}
$ clang -O realloc.c ; ./a.out
1 2

иногда хочется забиться в угол и плакать
сам столкнулся на своем проекте с похожим багом (UB, который оптимизировался в собачий бред)

Anturag

Лучше опустить С и сразу начинать с С++ - так граблей поменьше и наступаешь на них только в профессиональной разработке.
Прикольно набрасываешь, в компиляторе, написанном на C++, не смогли корректно реализовать простейшую оптимизацию. Как я понимаю, поэтому нужно учить С++ и фиксить баги.
PS, ну и разыменовывать поинтер в никуда ужасно, классическая ошибка.

yroslavasako

PS, ну и разыменовывать поинтер в никуда ужасно, классическая ошибка.
может и ошибка, но поведение компилятора контр-интутитивно. Видишь ошибку - выбрасывай ошибку компиляции.
И при этом что возникает проблема локальности. Это объявление realloc'а могло быть в совсем другом куске программы. А если посмотреть в отрыве от него, то код абсолютно валиден. Подставь туда любые два пойнтера, и станет ясно, что ситуация когда пойнтеры равны, а их значения - нет, не возможна.

evolet

Чувак, а ты точно в своем изначальном высказывании не перепутал С++ с Java'ой какой-нибудь?
В плюсах есть практически все из сей и много-много дополнительных, своих собсвенных "фишек".
Кстати о плюсах, кто-нить может пояснить следующее:
 

$ 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?

Anturag

может и ошибка, но поведение компилятора контр-интутитивно. Видишь ошибку - выбрасывай ошибку компиляции.
 
И при этом что возникает проблема локальности. Это объявление 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++ лучше-то? Где ошибка компилятора? Это объявление realloc'а "delete p" могло быть в совсем другом куске программы.

yroslavasako

Ничем не лучше. Плюсы имеют ту же самую проблему. Си - это не ява, где есть автоменеджмент памяти. Си - это язык, где есть адресная арифметика. И если два адреса равны между собой, не важно как именно они получены, хоть сложением, хоть умножением, хоть сложной функций вроде realloc, то в них должны храниться одинаковые значения. Я вот тут даже усложнил несколько оригинальный пример, и всё равно получаю бред:

#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

Anturag

Ничем не лучше. Плюсы имеют ту же самую проблему.

Ну и ладненько.
Си - это язык, где есть адресная арифметика. И если два адреса равны между собой, не важно как именно они получены, хоть сложением, хоть умножением, хоть сложной функций вроде realloc, то в них должны храниться одинаковые значения.
Ты ожидаешь слишком многого от undefined behaviour.

yroslavasako

Я ожидаю предсказуемости.
Вот ты берёшь код

if ( p == q) {
*p = 1;
*q = 2;
printf("%d %d\n", *p, *q);
};

Какого фига он должен показывать разные значения внутри p и q? Всё дело в том, что строчкой раньше встретилось *p = 1, и после этого в undefined behaviour попала вся остальная программа. Нормальные компиляторы, встретив выражение, после которого остальная программа теряет смысл, просто выдают ошибку и не пытаются собрать ложную программу. А clang поступает по-скотски, совсем как php, где чтобы программист не написал, всё будет интерпретировано, даже явная ошибка.

salamander

Я ожидаю предсказуемости.
Погоди-погоди. Ты хочешь чтобы undefined behavior был undefined но при этом предсказуем? Тут как-бы содержится противоречие.
Нормальные компиляторы, встретив выражение, после которого остальная программа теряет смысл, просто выдают ошибку и не пытаются собрать ложную программу.
Для этого компилятор должен со 100% точностью выявлять все такие случаи на этапе компиляции. Этого пока никто не научился делать.

podluchaya

Ну блин пофикси. Чего разнылся то. Или зарепорть багу. Бага или в конст фолдинге/пропагаторе или где-то в алиас анализе. Контекст содержит редандант стор(первый стор поэтому в тестсьюте его просто нет. Зарепортишь багу, добавят.

salamander

Да вроде нет баги. clang посчитал что *p и *q не могут алиаситься (так как один из них валидный указатель, а другой нет). В результате он дотащил константы 1 и 2 до вызова printf. Все шаги корректны с точки зрения семантики Си.
А то, что программист написал некорректный код и потом жалуется, что тот работает не так, как ему кажется что он должен - проблема программиста.
Вот вам еще пример говнокода, который работает "странно", причем даже без оптимизаций. Правда он только для 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;
}

podluchaya

print.c:6:13: warning: address of stack memory associated with local variable 'c' returned
     [-Wreturn-stack-address]
Нормально он работает. Если знать calling conventions можно даже использовать. Только стэки приватные для тредов и с многопоточкой могут быть проблемы. Вопрос только зачем.
Upd: хотя нет. Забыл про эксепшны.

PooH

Чувак, а ты точно в своем изначальном высказывании не перепутал С++ с Java'ой какой-нибудь?
нет, в с++ все же не принято использовать malloc realloc и иже с ними, да и голыми указателями не рекомендуется пользоваться

yroslavasako

Погоди-погоди. Ты хочешь чтобы undefined behavior был undefined но при этом предсказуем? Тут как-бы содержится противоречие.
Да, я хочу, чтобы он не портил всю программу, а только локальную часть.
Потому что внутри блока if (p == q) уже никакой неопределённости нет. Известно что эти указатели равны, и раз один из них валиден, то валиден и второй.

yroslavasako

так как один из них валидный указатель, а другой нет
А не проще тогда сразу выдавать ошибку "невалидный указатель", чем вводить поведение "о, невалидный указатель, давайте я выкину каждую чётную строчку после него, ведь UB - имею право". Как-это по-свински.

serega1604

>Потому что внутри блока if (p == q) уже никакой неопределённости нет.
Однопоточном мире в живет подаван юный.

yroslavasako


if ( p == q) {
*p = 1;
*q = 2;
printf("%d %d\n", *p, *q);
};

Расскажи юному подавану, что здесь меняется в случае многопоточности?

salamander

if ( p == q) {
*p = 1;
// приешл другой поток и сделала q = malloc(sizeof(int;
*q = 2;
printf("%d %d\n", *p, *q);
};

yroslavasako

Понятно. Я думал, что какая-то оптимизация может распараллелить автоматически код, а имелось в виду возможность явного обращения в коде к чужим данным. Увы, си от этого не застрахован, встроенного в язык механизма параллелизма нет, так что приходится смотреть за ним вручную. Расстановка синхронизации между потоками - это отдельная проблема, не относящаяся к обсуждаемой

salamander

Потому что внутри блока 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 на равенство.

yroslavasako

А если я сделаю for (int * z = p; z != q; z++) и попробую перебором нащупать q, это тоже будет UB? То есть мне казалось логично, что значение сравнение может быть либо позитивным, либо негативным, но не суперпозицией. И UB означает, что компилятор волен выбрать любой вариант, он может полностью выкинуть if, если решит, что указатели разные. Но если решил, что одинаковые, то неопределённость должна исчезнуть.

salamander

UB буквально означает, что может произойти что угодно. Нужно это
1) для того, чтобы можно было писать оптимизации без оглядки на гвонокод, использующий соответствующие конструкции,
2) чтобы можно было генерировать эффективный код для разных архитектур, а не только для какой-то одной.
Иногда это может приводить к неинтуитивным последствиям, но что делать.

stm5872449

но что делать.
Отлавливать такие ситуации на этапе компиляции?

podluchaya

Реаллок может быть control flow sensitive, тоесть стоять под предикатом. Рекурсивный вызов, принимающий аргументом указатель, передающийся в реаллок, может не доминировать/постдоминировать реаллок. В общем случае поинтерный анализ может иметь экспоненциальное время работы.

PooH

 
#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 моментов

margadon

вот только твоё "гладенько" это семантически совсем другой код, тут даже нет использования указателя на удалённый объект

PooH

вот только твоё "гладенько" это семантически совсем другой код, тут даже нет использования указателя на удалённый объект
ну так смысл моего сообщения в том и заключается, что в с++ надо писать на с++ и не надо будет париться над указателями в никуда
все, что требует использования низкоуровневых вещей - обкладываем тестами, ассертами и изолируем последствия
иначе ССЗБ
если правильно проектировать, то становится понятно, что указатель на удаленный объект - это нерешенный вопрос о владении этим объектом (ну или просто тупой баг).

margadon

Я с тобой согласен, но с другой стороны вышеуказанная ситуация должна быть до мелочей осознана, если хочешь не только свой корректный код писать, но и чужой читать и править, не переписывая.
Ну и бездумное использование shared_ptr налево-направо (особенно если "забыть" об этом через использование удобненьких auto) вполне может спровоцировать утечки памяти, так что тогда уж и weak_ptr и unique_ptr стоит расчехлить - и в результате сложность просто перекочует из одного места программы в другое. И не факт, что станет безопасней. Ружжо всё равно нацелено на ногу!
Но это я так, не в качестве возражения)

PooH

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

margadon

сложность в принципе не уменьшается
ну это очень сильно сказано, хотя от принципа зависит :grin:
когда создаешь объект, сразу думаешь когда он должен быть уничтожен, кто им владеет и кто от него зависит, если в этих вопросах нету ясности, то стоит отложить кодинг и задуматься
ну вот ты отложишь и задумаешься, а некоторые решат что "умный указатель не зря умный, что-нибудь придумает" и наговнокодят прям на месте. Подозреваю, что исторически ты учил плюсы не с make_shared и auto? :)

PooH

так сложилось, что некоторые инструменты в той области, в которой я работаю (геймдев на мобильных платформах) стали поддерживать С++11 относительно недавно
одно время даже буст было сложно вставить - какие там умные указатели - auto_ptr только
Оставить комментарий
Имя или ник:
Комментарий: