2014 dxdy logo

Научный форум dxdy

Математика, Физика, Computer Science, Machine Learning, LaTeX, Механика и Техника, Химия,
Биология и Медицина, Экономика и Финансовая Математика, Гуманитарные науки




 
 Неопределенное поведение
Сообщение29.01.2014, 14:23 
Аватара пользователя
Всем привет, имеется структура типа "очередь" . И небольшая программа с использованием буфера как раз типа "очередь". Программа работает, однако компилятор gcc объявляет один warning, который мне совершенно не понятен.
В сухом остатке мне нужно показать вам следующий код. Вы можете его скомпилировать (на gcc с ключом -Wall) и получить то же предупреждение, что и я.
Используется синтаксис C
struct q
{
    char *s;
    int num;
};
void add(struct q *buf, char c)
{
    *((buf->s=realloc(buf->s, ++buf->num*sizeof(char)))+buf->num-1)=c;
}
 

Функция add добавляет в буфер один символ, изменяет размер буфера, увеличивает счётчик символов в буфере на единицу.
Да, я люблю однострочники и считаю их допустимыми в программах "для себя" :D Кому трудно прочитать выражение, дальше есть дерево.
А warning вот какой:
Используется синтаксис Bash
test.c: В функции «add»:
test.c:12:31: предупреждение: операция над «buf->num» может дать неопределенный результат [-Wsequence-point]


В warning'е речь идёт о точках следования, и для лучшего понимания я нарисовал дерево этого выражения, отметив на нём sequence points как SP (не считая самого внешнего на точке с запятой). Мог кстати и не правильно отметить, но дерево вроде бы верное.
Изображение

Так вот, почему тут возникает это предупреждение и какая именно есть неоднозначность в операции над buf->num? Я её в упор не вижу.

 
 
 
 Re: Неопределенное поведение
Сообщение29.01.2014, 14:49 
Вся проблема в инкременте. ++buf->num и потом buf->num-1. По стандарту очередность выполнения инкремента не определена. Поэтому значение выражения buf->num-1 не определено, может использоваться значение buf->num до инкремента, а может использоваться значение после инкремента.

 
 
 
 Re: Неопределенное поведение
Сообщение29.01.2014, 18:40 
Legioner93 в сообщении #820303 писал(а):
Да, я люблю однострочники и считаю их допустимыми в программах "для себя" :D

Хотите интересное упражнение? Соберите программу с "однострочниками", потом перепишите ее в человеческом виде, соберите снова и сравните два исполняемых файла. Объясните результат.

Ну и да, mustitz сказал все верно — у вас под правой sequence point один раз разыменовывается buff, а другой раз — ++buff.

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

 
 
 
 Re: Неопределенное поведение
Сообщение29.01.2014, 20:01 
Joker_vD в сообщении #820382 писал(а):
Ну и да, mustitz сказал все верно — у вас под правой sequence point один раз разыменовывается buff, а другой раз — ++buff.
Не так. В другой раз также разыменовывается buf, а ++ относится к (buf->num).

-- Ср янв 29, 2014 12:03:41 --

Legioner93 в сообщении #820303 писал(а):
Да, я люблю однострочники
Кстати, если вы так любите короткий код, то зачем умножаете на sizeof(char)? Это ведь равно единице по определению оператора sizeof().

 
 
 
 Re: Неопределенное поведение
Сообщение30.01.2014, 01:46 
venco в сообщении #820426 писал(а):
Joker_vD в сообщении #820382 писал(а):
Ну и да, mustitz сказал все верно — у вас под правой sequence point один раз разыменовывается buff, а другой раз — ++buff.
Не так. В другой раз также разыменовывается buf, а ++ относится к (buf->num).


Правильно, ++ относится к buf->num. Что делает выражение buf->num неопределенным, если оно встретится еще где либо в этом же выражении. А оно встречается чуть далее по тексту. Поэтому компилятор может выбирать один из двух вариантов:

Используется синтаксис C
void add1(struct q *buf, char c)
{
    auto new_buf_num = buf_num + 1;
    *((buf->s=realloc(buf->s, ++buf->num*sizeof(char)))+new_buf_num-1)=c;
}

void add2(struct q *buf, char c)
{
    auto old_buf_num = buf->num;
    *((buf->s=realloc(buf->s, ++buf->num*sizeof(char)))+old_buf_num-1)=c;
}


Но даже если инкремент обернуть в функцию, то это не избавит от UB. На скане приведено красивое дерево. По стандарту языка C (C++), оператор сложения + не определяет порядок вычисления аргументов, поэтому в выражении f1()+f2() возможно, что любая из функций может быть вычислена первой. В нашем случае, если на скане для узла + вначале вычислить левое поддерево, то мы получим add1, а если вначале вычислить правое поддерево, то add2. Другой пример:

Используется синтаксис C
static inline increment(int * value)
{
    return ++*value;
}

int x = 0;
int a = increment(&x) + x; // UB
int b = increment(&x) && x; // 1
 


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

 
 
 
 Re: Неопределенное поведение
Сообщение30.01.2014, 09:46 
Аватара пользователя
mustitz в сообщении #820314 писал(а):
Вся проблема в инкременте. ++buf->num и потом buf->num-1. По стандарту очередность выполнения инкремента не определена.

Спасибо, вы совершенно правы! Меня немного запутали эти sequence points (на выходе и входе из realloc). Сейчас побольше почитал про них, и стало всё ясно. Действительно, никакие sequence points не помешают компилятору вычислить то дерево в угодном ему порядке.
mustitz в сообщении #820568 писал(а):
Ну а так в каждом языке свои правила, поэтому, чтобы не забиваться себе голову, я взял себе за правило не писать код, который содержит выражения, значения которых зависят от порядка вычисления аргументов функций/операторов. Имхо, так получается и более читабельно, и надо меньше ячеек в мозгу занято хранением деталей.

Я предполагаю (сам никогда не участвовал ещё), что в реальных проектах намного эффективнее по совокупному времени, потраченному на код, писать в более читабельном стиле. Потому что там код не только пишут, но ещё и читают, причем много-много раз.
Однако во время изучения языка (чем я сейчас и занимаюсь) полезно покопаться во всех этих тонкостях. Хотя бы чтобы лучше понять, почему так всё-таки не надо писать :D
Joker_vD в сообщении #820382 писал(а):
Хотите интересное упражнение? Соберите программу с "однострочниками", потом перепишите ее в человеческом виде, соберите снова и сравните два исполняемых файла. Объясните результат.

Предполагаю, что существенной разницы не будет. Но мне нравится писать такие выражения, потому что их писать быстрее! В смысле набора с клавиатуры. Да и какая-то своя эстетика у них есть :-)
И да, я знаю, что пока время, затраченное например на эту тему с лихвой перебило все эти 2-3 секундные выигрыши :mrgreen:
Joker_vD в сообщении #820382 писал(а):
Пожалуйста, не пишите запутанный код, если вы видите меньше UB, чем компилятор видит (а он их видеть абсолютно не обязан, кстати, в отличие от вас).

Ну, это только пока. Как говорится, догоним и перегоним компилятор. Я вот только недавно сел серьезно изучать C, поэтому и создаю подобные темы, дабы изучить язык на хорошем уровне.
Тут же как, я считаю: если программист "на ты" с этими sequence points, undefined behaviour, unspecified behavior и прочими хаками, то он прекрасно будет понимать, как эффективно и правильно написать и без них, когда это нужно.
venco в сообщении #820426 писал(а):
Кстати, если вы так любите короткий код, то зачем умножаете на sizeof(char)? Это ведь равно единице по определению оператора sizeof().

Согласен, спасибо! Теперь буду заменять на 1. Как-то в памяти просто засело, что на некоторых системах $char$ может быть и 2 октета, и 0.5, вот и писал по привычке. Но это никакого отношения не имеет к sizeof(char), который всегда 1 по стандарту языка, как только что выяснилось :-)

 
 
 [ Сообщений: 6 ] 


Powered by phpBB © 2000, 2002, 2005, 2007 phpBB Group