Как стать автором
Обновить

Комментарии 98

НЛО прилетело и опубликовало эту надпись здесь
Не «зачем», а «как так получилось».

Сначала был написан очень низкоуровневый язык, который был чуть выше ассемблера. Там было несколько дырявых абстракций (те же указатели), но работать с ними было приятнее, чем с машинным кодом. А ещё он был настолько низкоуровневым, что на нём удалось написать ОС. Первую ОС не на ассемблере.

Потом язык пошёл в массы и появились разные компиляторы. Пришлось придумать стандарт, который бы позволил разным компиляторам давать такой же результат на таком же коде. В процессе разработки стандарта опирались на существующую (-ие?) реализацию (-ии) и попытались дать полное описание для дырявых абстракций.

Получилось как получилось.
На самом деле, как ни странно, ответ на этот вопрос есть — просто людям он часто не нравится.

Язык C изначально создан для написания одной-единственной вещи: операционной систему UNIX.

А она, как известно, является переносимой.

Сделать же язык, на котором можно написать эффективную переносимую программу — не так-то просто.

Это сейчас — все процессоры похожи. А в те времена, когда C создавался — между ними была масса различий. И можно было либо делать так, как в Java — регламентировать, что получится, скажем в результате операции «1<<256», и наплевать на эффективности… или запретить программистам использовать «неправильные» конструкции.

Что и было проделано: «неправильные» конструкции, которые могут вести себя по разному на разном железе и в разных ОС в C — запрещены. Вот только создание и полное описание того, что такое «неправильная» конструкция — оказалось не так-то просто сделать
НЛО прилетело и опубликовало эту надпись здесь

Потому что указатели не обязательно имеют численное представление, для которого арифметические операции имеют смысл. Представим себе такую архитектуру, где объекты разного типа имеют независимую систему адресации и может быть бесконечное количество указателей со значением "42", которые не равны друг другу (т.к. указывают на данные разных типов). Это вполне легально по стандарту С.

НЛО прилетело и опубликовало эту надпись здесь

Да, про упоротые случаи, конечно же :) Но компилятор обязан гарантировать одинаковое поведение кода строго соответствующего стандарту С на любых, даже самых упоротой платформе. Поэтому стандарт написан так, чтобы быть своего рода общим знаменателем всех возможных платформ.

Мы не про «упоротые сущности». Мы про «упоротые архитектуры». В XXI веке — да, их мало, а вот в XX — чуть не каждый первый процессор изобретал какой-нибудь особый способ адресовать больше 64K при использовании только 16-битных регистров.

А современная культура, когда можно было потратить 90% ресурсов только на то, чтобы облегчить жизнь программисту — ещё не овладела массами (на самом деле первая попытка уже тогда была… но не сложилась… дикие люди — не понимали что для их же блага лучше то, что за что можно заплатить два рубля лучше покупать за сто).

В результате — писать нужно было так, чтобы на всех платформах (а среди них, напомним, «плоская» память была скорее исключением, чем правилом!) код работал…

Ну а потом — да, пришли люди, пытающиеся использовать C/C++ как замену для ассемблера… и начались беды с «undefined behavior», да…
Это вполне легально по стандарту С.
Это не только вполне легально. Это, чёрт побери, банальный Turbo C! Указатели могут указывать в одно (физически) место в памяти, но быть, при этом, разными.
«либо один указатель ссылается на позицию за последним элементом массива, а другой — на начало другого массива, следующего сразу за первым в том же адресном пространстве.»
— но в первом примере никто не может гарантировать, что a и b расположены в одном адресном пространстве, поэтому gcc и считает, что указатели не могут быть равны.
Что значит «не может гарантировать» когда на платформе всего одно адресное пространство?

В стандарте нет требования только одного адресного пространства.
Возможна архитектура, в которой адреса возврата из функций хранятся в одном стеке, а локальные данные функции — в другом.
Это исключает один из векторов взлома приложений.

Не просто возможна, а прямо таки норма для embedded процессоров, где программа хранится и выполняется из flash памяти, а данные хранятся в оперативной памяти.

И есть вариант, когда переменная объявлена как неизменяемая — тогда она хранится в программной памяти, и указатель тоже показывает на программную память.

Все это, разумеется, для экономии ресурсов. Нежно любимый мной PIC16F84A имел 2048 слов кода и 68 байт оперативной памяти…
www.microchip.com/wwwproducts/en/PIC16F84A
Вряд ли автор поста компилировал код для embedded процессора…
Полностью с вами согласен, но беседа шла о стандарте на язык Си, и он такое допускает.
Небольшой комментарий к замечанию mikelavr. Цепочка рассуждений следующая:
0. Язык C предназначен для написания переносимых программ.
0a. Никто и никогда не должен писать непереносимый код на C.
0b. Задача программиста — написать корректную и переносимую программу.
1. Соотвественно любая конструкция рассматривается исключительно в разрезе исполнения соотвествующего кода на всех известных науке процессорах и со всеми известными науке операционками.
2. Если какой-то код хоть где-то может вести себя странно — то это значит, что программист сделает так, чтобы этот код не вызвался. Как — его проблемы.

Очень много разработчиков этим подходом недовольны и им очень не нравится такое «своевольничание», но… если мы откажемся от этого подхода — то это будет уже другой язык, предназначенный для других целей.
НЛО прилетело и опубликовало эту надпись здесь
То бишь человек представляет, как выглядит машинный код x86 и пишет код на C, ожидая, что он будет компилироваться очевидным образом в этот код.
Вот только очевидность — она у всех разная.

Вот, например:
#include <stdio.h>

struct A {
  int x;
  int y;
};

int foo() {
  struct A a;
  a.x = 2;
}

int bar() {
  struct A a;
  a.y = 2;
}

int baz() {
  struct A a;
  return a.x + a.y;
}

int main() {
  foo();
  bar();
  printf("%d\n", baz());
}

$ gcc test.c -O0 -o test && ./test
4
Вполне себе такой код в духе Fortran-66. И если код на C будет «очевидным образом» компилироватья в машинный код — даже корректный. А с -O0 он даже на современных компиляторах работает.

Но вы действительно хотите, чтобы оптимизаторы подобные трюки поддерживали? Вы хотя бы представляете — насколько это замедлит типичную программу?

Призывы к тому, чтобы сделать «лучшую», более безопасную спецификацию C — звучат давно. Но при попытки их воплотить в жизнь мы неизменно напарываемся на неочевидность очевидности…
а как это работает? структуры же у каждой функции свои. Или здесь работает то, что они остаются лежать в стеке с прошлого вызова?
Или здесь работает то, что они остаются лежать в стеке с прошлого вызова?

Да, именно этот фактор и срабатывает.

А что программист должен ожидать при выводе значения неинициализированной переменной?

А почему это она неинциализирована? В другой функции в это место в стеке данные записаны.

Почему вдруг вы вот в этом конкретном месте вдруг вспонинаете такие «мелочи», как то, что говорит стандарт по этому поводу, а других местах — считаете, что должно происходить то, что делает процессор?

Вы меня с кем-то путаете.

Ладно, минусовальщики, уговорили — молчу.

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

Если бы в стандарте было написано что-то вроде «нельзя сравнивать указатели полученные из разных объектов не являющихся частями одного объекта» — я бы с вами согласился. Но я вижу совсем другие цитаты.

Любая свобода в стандарте — это простор для оптимизации компилятора, чем компиляторы и пользуются. Полагаю, что тут вне зависимости от целевой архитектуры (где-нибудь на уровне middle level intermediate language) компилятор заметил, что указатели основаны на разных объектах, а значит, легально заменить выражение сравнения на константу 0. Кодогенератор целевой платформы в бэкенде компилятора уже не видит сравнения, а видит просто 0. Возможна и более агрессивная оптимизация, когда этот ноль прямо подставится в строку формата "%p %p 0" на этапе компиляции.

Механику произошедшего я понимаю. А вот отношение стандарта к происходящему остается для меня загадкой.

Вообще-то приведенная в статье цитата из 6.5.8 говорит об этом прямым текстом — "Во всех остальных случаях поведение не определено" (после списка валидных сравнений).

Вы пост-то читали? Случай — не остальной, а перечисленный:


либо один указатель ссылается на позицию за последним элементом массива, а другой — на начало другого массива, следующего сразу за первым в том же адресном пространстве

При использовании с этими операторами указатель на объект, не являющийся элементом массива, ведёт себя, как указатель на начало массива длиной в один элемент, тип которого соответствует типу исходного объекта.

Все дело в формулировке "следующего сразу за первым в том же адресном пространстве".


Есть не так много ситуаций, когда стандарт требует определённого размещения в адресном пространстве, и локальные переменные на стеке — не тот случай. Компилятор волен расположить их произвольным образом, следовательно, этот случай не применим. Во всяком случае именно так я понимаю позицию разработчиков gcc.

Почитайте тот же раздел в C++17.

Думаю что разработчики компиляторов справедливо полагают, что это ошибка в стандарте, которая в C22 (или когда у нас там следующий по плану?) будет исправлена.
Тогда ок, принимается.
Если бы в стандарте было написано что-то вроде «нельзя сравнивать указатели полученные из разных объектов не являющихся частями одного объекта» — я бы с вами согласился.
В C++17 так и написно. А в C11 — в обном месте поправили, в другом — забыли…
Да нет, единое адресное пространство как раз гарантируется. Просто потому что у наиболее распространенной платформы всего одно адресное пространство.
Почему нельзя, например, разместить a в регистре, а b — в оперативке?
Компилятор вполне может свернуть все указатели на регистр в нужные инструкции.
Просто потому что у наиболее распространенной платформы всего одно адресное пространство.
Языки, которые используются для написания кода только и исключительно для «наиболее распространённых платформ» не называются С и C++. Это могут быть C++/CLI или там F# — но не C и не C++.

Код на C должен работать на всех платформах — а не только на «наиболее распространённых».
Так он и работает, только выдает разные результаты (и это ожидаемо!)

Вопрос лишь в том почему конкретно в этом случае он выдал такой результат.

PS если я случайно отправил комментарий два раза, вовсе не обязательно отвечать на каждый :-)
Так он и работает, только выдает разные результаты (и это ожидаемо!)
Нет. Это не ожидаемо. Ожидаемо — это когда результат в стандарте описан как implementation defined. Таких места там очень мало.

Во всех остальных случаях считается, что нас устраивает любой результат — и компилятор его и обеспечивает. Скажем участок кода в 0 байт длиной обеспечивает какой-то результат, исполняется быстро и вообще хорошо… вот его вы и получите.
Но ведь стандарт С не говорит, что оно «должно» быть едино. Поэтому гарантия, на которой Вы настаиваете, скорее вопрос среды. И даже если, допустим, сегодня такую гарантию можно дать, чисто теоретически, никто не может гарантировать что завтра ничего не изменится.

И потом, даже в среде, которую Вы имеете ввиду, единое адресное пространство гарантировано для виртуальной памяти. А С он, говоря по — Английски «promiscuous» и может, на той же платформе, с той же оболочкой, выполняться внутри некоего физического устройства, с собственной архитектурой физической памяти, которая может быть разделена на секторы, с непреодолимыми барьерами, и С будет в этом устройстве выполняться, по совершенно другим правилам, и здесь гарантии единого адресного пространства совершенно неприменимы.

Но не в этом дело. Мне кажется С был создан для того, чтобы некоторые устройства работали. Он не создавался для того, чтобы были правила. И поэтому он так горячо любим. Язык сорванец :)
Спасибо за интересный обзор. Действительно понимание таких тонкостей разработчикам дается достаточно тяжело. Кстати, на базе вашего первого примера можно вообще сделать нечто, ломающее мозг. Например:
#include <stdio.h>

int main(void) {
    int a, c, b;

    (void)c;

    int *p = &a;
    int *q = &b + 1;
    printf("%p %p %d\n", (void *)p, (void *)q, p == q);
    return 0;
}
Результат:
0xffffcbec 0xffffcbec 1
gcc version 6.4.0 x86_64-pc-cygwin (-O0 -Wall -Wextra -Werror -std=c11)
А можете обьяснить почему так?
Рискну предположить, что раз мы явно указываем компилятору что переменную с мы не используем, то он под нее память на стеке и не выделяет.

Дело не в третьей переменной, а именно в -O0. С -O1 будет как в примере в статье.


Если посмотреть дизассемблер, с оптимизацией p == q предподсчитывается как 0.


    printf("%p %p %d\n", (void *)p, (void *)q, p == q);
 6b4:   48 8d 54 24 0c          lea    0xc(%rsp),%rdx
 6b9:   48 89 d6                mov    %rdx,%rsi
 6bc:   b9 00 00 00 00          mov    $0x0,%ecx # Вот здесь просто пишется 0 в ecx
 6c1:   48 8d 3d 9c 00 00 00    lea    0x9c(%rip),%rdi 
 6c8:   b8 00 00 00 00          mov    $0x0,%eax
 6cd:   e8 8e fe ff ff          callq  560 <printf@plt>

Без оптимизации делается честный cmp.


    printf("%p %p %d\n", (void *)p, (void *)q, p == q);
 6cc:   48 8b 45 f8             mov    -0x8(%rbp),%rax
 6d0:   48 3b 45 f0             cmp    -0x10(%rbp),%rax # Тот самый cmp
 6d4:   0f 94 c0                sete   %al
 6d7:   0f b6 c8                movzbl %al,%ecx
 6da:   48 8b 55 f0             mov    -0x10(%rbp),%rdx
 6de:   48 8b 45 f8             mov    -0x8(%rbp),%rax
 6e2:   48 89 c6                mov    %rax,%rsi
 6e5:   48 8d 3d 98 00 00 00    lea    0x98(%rip),%rdi  
 6ec:   b8 00 00 00 00          mov    $0x0,%eax
 6f1:   e8 6a fe ff ff          callq  560 <printf@plt>
Значит я не правильно понял вопрос khabib, подумал вопрос был «почему &b + 1 == &a».
А не все что угодно может быть? Кажется только структуры гарантируют последовательное расположение переменных. И то с выравниванием.
А тут «так совпало», пусть даже ожидаемо.

Объяснить легко — переменные внутри функций размещаются в стеке, а стек обычно растет "вниз" (на некоторых архитектурах возможно и нет, но на x86/x64 — точно). То есть следующий помещаемый элемент будет иметь адрес меньше предыущего, что мы и видим в примере. Если объявить эти переменные вне функции (глобальными), фокус может и не сработать — все зависит от реализации компилятора.
Насчет сравнения указателей — это, как уже сказали, козни оптимизации.

Вот, пример, когда переменные a1, b1 глобальные, те не помещены в стек. Адреса назначаются в другом порядке.
#include <stdio.h>
int a1;
int b1;

int main() {
int a;
int b;

int *p = &a;
int *q = &b + 1;

int *p1 = &a1;
int *q1 = &b1 + 1;

printf("%p %p %d\n", (void *)p, (void *)q, p == q);
printf("%p %p %d\n", (void *)p1, (void *)q1, p1 == q1);
return 0;
}

Компилируем без оптимизации
$ gcc 1.c
$ ./a.out
0xbedd7614 0xbedd7614 1
0x205c8 0x205d0 0
Проверил на Linux x86_64(GСС), Linux armhf (GCC), а также на ARM Cortex-M0 (компилятор ARMCC) — результаты аналогичны, не стал копипастить.

Вы подвесили меня на минуту. Как???? Почему???
В переводе с первым примером упущена существенная деталь.

If compiled with and optimization level 1, then a run of the program on a x86-64 Linux system prints:

Без оптимизации результат другой.

> gcc main.c
> ./a.out
0x7ffd9dad19fc 0x7ffd9dad19fc 1
> gcc main.c -O1
> ./a.out
0x7ffe4b876ebc 0x7ffe4b876ebc 0

gcc version 6.3.0 20170516 (Debian 6.3.0-18+deb9u1)
Добавил.
C11 § 6.5.9 пункт 6

Два указателя равны тогда и только тогда…


сакральные тексты перевели на русский?! Где смотреть?
такого добра я богато читал…
Если PVS-Studio действительно хочет прославится могли бы перевести С11 на русский. Все бы сишные программисты рассказывали какая PVS-Studio крутая компания и какие у нее крутые продукты.
Был у меня перевод С89 выполненный советской властью, отпечатанный на ЦПУ на бумаге с дырочками по бокам…
Тут пару лет назад господа Зуев и Чупринов сделали перевод плюсового стандарта, проделав колоссальный объем работы. Совокупное мнение получилось неоднозначным. Тут специфика в том, что это не просто техническая литература, а еще и ISO-шная. И тут две проблемы. Во-первых, по моему глубокому убеждению, именно такую литературу нужно переводить по ISO-шным же стандартам переводов для максимальной точности. Во-вторых, чисто стандарта недостаточно. Надо как у юристов: условно том закона + том комментариев. С учетом всего, думаю PVS-Studio не захочет. Полезность сомнительна, затраты большие, покупать результат (а он выйдет по опыту книги выше недешевым) будут плохо.
Интересная тема, как минимум направление.
У меня есть собственный зуд на уровне чистого как слеза Си, без плюсов.
Хотелось сделать универсальные функции под большинство типов, одна общая декларация и масса вариантов решений под каждый тип. Да, всё это можно сделать на Си++, но зуд!!!
Получилось три универсальных заготовки определения типов, в комбинации которых можно распознать float/uint(8~32)_t /int(8~32)_t. С небольшими дополнениями будут распознаваться 64-128бит числа.
#define ICE_TYPE(type_x) (__typeof__(type_x))
#define ICE_i(var) (ICE_TYPE (var) (0) > -1? 0: 1 )
#define ICE_F(var) (ICE_TYPE (var) (0.1f) == 0? 0: 1 )
bitbucket.org/AVI-crak/sprint/src/default/sPrint.h

Но вот чего я не смог найти — так это решения по поводу указателей.
На словах всё просто, но написать работающий макрос не получается.
Собственно — каким образом силами Си отличить тип указателя от всего остального.?
Так вы ж уже всё равно вышли за рамки C! Вызовите __builtin_classify_type(p) и сравните с пятёркой… делов-то.

P.S. Для clang'а лучше делать через __overloadable__.

P.P.S. Что делают другие компиляторы — не знаю. Но любой C99-совместимый компилятор должен такие трюки уметь. Ибо tgmath.h. Проблема в том, что это стандартная библиотека такое должна уметь — а людям такие ручки давать не положено.
Спасибо за направление.
Поиском по __builtin_classify_type(p) нашлось много интересного.
И да, основная идея доработать math. Буквально переписать все стандартные функции под актуальные для мк типы, на три вида результата: точный, оптимальный и быстрый. С быстрым переключением режима прямо в коде пользователя.
__overloadable__ — согласен, для моего варианта это оптимально.

В первом примере по моему все четко, правильно и логично,- сравниваются два разных указателя, а не то на что они указывают. А вот так p == q доджно быть true.

Заезды с телевона не вставились почему-то перед p и q

Это не звезды не вставились, это Markdown включился. Там пара звезд работает как тэг <i>


Используйте знак `для выделения фрагментов как кода, там внутри форматирование выключается.

Проверка *p ==*q
использовал тег

`

Невозможно напечатать вместо этой закарючки ‘ тег <code> </code>
:)

Два разных указателя на один и тот же адрес в памяти, о чем наглядно свидетельствует вывод print-а где печатаются адреса указателей а не то на что они указывают.
А если так:
printf("%p %p %d\n", (void *)p, (void *)q, (void *)p == (void *)q);
а?
Обратите внимание, что указатели p и q ссылаются на один и тот же адрес

int a, b;   
int *p = &a;
int *q = &b + 1;


я один не понимаю зачем это сделано и почему p и q ссылаются на один и тот же адрес?

и далее почему то пытаются сравнить

printf("%p %p %d\n", (void *)p, (void *)q, p == q);


и почему вообще должно давать положительный результат p == q, если мы сделали

int *q = &b + 1
,

и вообще что сейчас происходить с q ??

У меня вопрос: зачем так делать и кто так делает?

И отсебятины ответ: если вы так делаете, в архитектуре вашей программы чтото не так…

И вообще, странный тон указатели на адреса сравнивать, в одному можеть бить целое число, в другом — ссылка на ссылку на ссылку на ссылку структуры которая может иметь в свойстве лишь целое число, с одной стороны сущности разные, но с другой, если структура пустая и сравнивоемое пустое — то почему не должны быть равны?..

И вообще2: почему не делать программы явными… если сравнение указателей вызывает подозрения, вместо описания и добавление возможный опций компиляций — инным способом реализовать эту часть логики программы?
При выделении памяти компилятор обычно разместит a и b сразу друг за другом

То же самое он бы сделал если бы мы попросили вместо int a,b выделить int a[2]. То есть пример в каком-то смысле эквивалентен
int main(void) {
int mem[2];
int *b = mem[0];
int *a = mem[1];
int *p = a;
int *q = b + 1;
printf("%p %p %d\n", (void *)p, (void *)q, p == q);
return 0;
}

Конкретно в этом модельном примере конечно подобная операция выглядит бессмысленно, но в тексте приведен пример когда совершенно нормально и безобидно выглядящий код из-за той же самой особенности компилируется неправильно

extern int _start[];
extern int _end[];

void foo(void) {
for (int *i = _start; i != _end; ++i) { /* ... */ }
}

В этом безобидно выглядящем примере ряд компиляторов с включенными оптимизациями автоматически и зачастую молча убирают проверку на (_start==_end) в сгенерированном коде со всеми вытекающими последствиями
по моему, часто в казино нужно выигрывать, чтобы рассчитывать на последовательное выделение адресов, мне казалось, этими делами вовсе не компилятор занимается (порядок выделения адресов и т.п.), это дело ОС и архитектуры, какой-то блок новый, какой-то переиспользованный, вплоть до получить для «а» один из первых адресов, для «б» — последний после последнего использованного, или вообще где-то в конце… почему компилятор должен отвечать за глупости «int *q = &b + 1»?

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

просто нужно делать, просто
Выделением памяти под глобальные переменные и переменные на стеке занимается именно компилятор. И да, гарантий там никаких нет. Но речь не об этих гарантиях а о том что два указателя на одну и ту же область памяти могут с точки зрения компилятора давать при сравнении false. Пример который вас так пугает просто один из способов получить подобные указатели, а приведенный пример пониже — типовой способ нарваться на такую проблему в реальной программе.
этими делами вовсе не компилятор занимается (порядок выделения адресов и т.п.), это дело ОС и архитектуры

Компилятор под конкретную архитектуру этим занимается. И на x86 такое выделение вполне себе нормальное, и будет работать ну очень часто. Это ж не куча, а стек, тут все предсказуемо.

там можно делать странные вещи, которые вроде бы должны вести себя непредсказуемо — но работают

О, в Си можно делать очень много странных вещей. Например, на x86 из-за выравнивая адресов в памяти, malloc всегда возвращает указатель (а указатель это число, как мы помним) с нулевыми младшими битами (минимум 2 бита будут нулевыми). Следовательно, мы можем в эти два бита положить какую-нибудь свою информацию! Естественно, для использования указателя нужно будет эти биты сбрасывать. Итого, фича должна себя вести непредсказуемо, но она работает.

И как тогда выполнять сравнение указателей в примере с циклом?
Приведение типа к какому-нибудь int с целью арифметического сравнения — не по стандарту, получается.
Сравнение их как указатели на объекты — работает не так как ожидается.
И как написать стандартный, переносимый код перебора элементов в духе итераторов (begin, end и всё такое)? Никак что-ли? Надо работать с массивами только через индексы? А то ж найдётся какая-нибудь извращённая платформа, где элементы через один лежат в двух банках памяти с одинаковой адресацией, а каждый третий элемент вообще в регистре хранится...

А то ж найдётся какая-нибудь извращённая платформа, где элементы через один лежат в двух банках памяти с одинаковой адресацией, а каждый третий элемент вообще в регистре хранится...
И вот в этом случае — это уже обязанность компилятора с этим разбираться.

В описание C++17 посмотрите — там чётко описано какие указатели можно сравнивать на больше/меньше, какие — нельзя.

Почему c++? Мы же про c говорим.


А так — да, если абстрагироваться от непрерывной памяти и всё такое, то — как прогнать цикл в стиле итераторов? Где сравниваются указатели на целое. Сравниваются на равенство. Не больше/меньше.

Потому что стандарт C++ обновлялся после 2011го года и в нём это место прописали более чётко (было добавлено пояснение: Note: A pointer past the end of an object (8.5.6) is not considered to point to an unrelated object of the object’s type that might be located at that address. A pointer value becomes invalid when the storage it denotes reaches the end of its storage duration; see 6.6.4. ) — а стандарт C не обновлялся, но все разработчики компиляторов считают, что то, что прописано в C11 реализовать эффективно нельзя.

С «циклом же в стиле итераторов» проблем нет: все стандарты разрешают без ограничений сравнивать указатели, относящиеся к одному объекту и указывающие на его начало, в середину или конец (байт, находящийся стразу за концов объекта).

Это, кстати, приводит к забавному следствию: адресоваться к самому последнему байту в адресном пространстве средствами C/C++ нельзя…

"С «циклом же в стиле итераторов» проблем нет"
Т.е. пример из статьи — липа? Всё работает и ничего не зацикливается?

Возможно, у меня что-то со зрением, но в примере из статьи я вижу два массива, а не один.
Более того — как раз комментарий из C++17 объясняет почему в примере из статьи — могут быть проблемы.

Но если «искать знакомые слова», а не думать — то этого можно и не заметить.

Стандарт C тоже обновился. Планировался в 2017, на вышел только в 2018. https://en.wikipedia.org/wiki/C18_(C_standard_revision)


Исправили ли там сравнение указателей, я не смотрел.

Господа минусующие (просто за вопрос! o_O ) — может хоть ответите на него!
Поясните: вы не хотите перепаковывать или изменять массивы, так, чтобы был маркер конца, но все равно хотите итератор?

Я задаюсь простым вопросом:
если пример с циклом — вполне себе жизненный, — работает не так как ожидается (бесконечно зацикливается), то перебирать массивы, получается, можно только по индексу? Квадратными скобками?
Или всё-таки и указателями, в духе итераторов — тоже можно пробежаться по массиву?

Чем плохо решение в разделе «Дополнение» статьи? На голых указателях реализованы iter.next() и iter.last(). Ещё я советую посмотреть, как реализованы контейнеры в GLib, особенно GVariant и GArray — там есть готовые итераторы, но вроде бы в контейнере есть служебная информация.

"Дополнение" к статье, не совсем о том же о чём был изначальный цикл.
Изначально: есть два указателя и надо пробежаться от одного до другого.
А в "дополнении" — есть один массив, длина которого известна и вместо того, чтобы перебрать его элементы по индексу — наводят тень на плетень указателями.


Получить просто два адреса (один это массив или не один — не знаю, просто адрес начала и адрес конца) и пробежаться от одного до другого — нельзя, получается? Некорректно с точки зрения стандарта? Ну или не столько некорректно, сколько — UB?

один это массив или не один — не знаю, просто адрес начала и адрес конца
А вот так — C/C++ не работают. Должны знать — иначе ответить на этот вопрос нельзя.
В принципе есть такие способы сравнить два указателя
if (p == q) ... // may fail if p := ptr + offset
if ((intptr_t)p == (intptr_t)q) ... // should work if intptr_t is available
if (memcmp(&p, &q, sizeof(p)) == 0) ... // will either work always or not compile at all

Но вообще да, в C предполагается что массивы всегда задаются началом и длиной а не двумя указателями. Перебор «в духе итераторов» тоже работает, но при условии что указатели на начало и конец явным образом формируются из одного указателя непосредственно перед использованием в той же функции где находится цикл.

Получается, "перебор в духе итераторов" можно применять только когда инициализация этих "итераторов" (указателей на начало/конец) происходит в той же единице трансляции, где и использование. Где компилятор видит, что они относятся к одному массиву.

Перебор «в духе итераторов» тоже работает, но при условии что указатели на начало и конец явным образом формируются из одного указателя непосредственно перед использованием в той же функции где находится цикл.
Извините, но всё с точностью до наоборот. «Явно формировать» указатели не нужно — наоборот, компилятор может что-то предполагать только если он видит, что указатели указывают в разные объекты.

Проблема с __start_builtin_fw/__end_builtin_fw, про которую говорилось в LKML была как раз в том, что они были описаны как массивы, а не как указатели.
Мда, ещё пример легкого попадалова:
если в тексте тестовой программки из статьи заменить
printf("%p %p %d\n", (void *)p, (void *)q, p == q);
на
printf("%p %p %d\n", (void *)p, (void *)q, (intptr_t)p == (intptr_t)q);
то на -O1 выводится 1, но на -O2 уже всё-равно 0.
Переход с -O0 на -O1 включает оптимизацию, и ни одним из доступных флагов оптимизации, которые можно выключить при переходе на -O1 сравнение p==q не делается true (т.е. реально GCC умён нереально и не собирается делиться привилегиями с глупыми программистами, не знающим закоулки реализации GCC C). При переходе на уровень -O2 проверил с -fno-strict-aliasing: (intptr_t)p == (intptr_t)q — всё равно 0. Отключение других опций оптимизации не стал проверять, т.к. печально…
При переходе на уровень -O2 проверил с -fno-strict-aliasing: (intptr_t)p == (intptr_t)q — всё равно 0. Отключение других опций оптимизации не стал проверять, т.к. печально…
Поправка. Не заметил: на -O2
int a, b;
не располагает их рядом.
НЛО прилетело и опубликовало эту надпись здесь

Почему так происходит уже ответил выше. А делать так, конечно же не стоит. Просто С допускает такие вольности, но это на некоторых архитектурах может дать совсем другие результаты.

НЛО прилетело и опубликовало эту надпись здесь
test.sh
#!/bin/sh

echo >wat.c "#include <stdio.h>
int main(void) {
int a, b, *p = &a, *q = &b + 1;
printf(\"%p %p %d\\n\", (void *)p, (void *)q, p == q);
return (0);
}
"

gcc --version
for opt in 0 1 2 3 s; do
gcc -Werror -Wall -Wextra -Wpedantic -O${opt} wat.c
echo -n "-O${opt}: "
./a.out
done

Выхлоп на ARM
gcc (GCC) 4.9.3
Copyright © 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

-O0: 0x7eeaeccc 0x7eeaeccc 1
-O1: 0x7e959cd4 0x7e959cd4 0
-O2: 0x7ee59cd0 0x7ee59cd8 0
-O3: 0x7eb11cd0 0x7eb11cd8 0
-Os: 0x7ec91cd0 0x7ec91cd8 0

Еще выхлоп на ARM
gcc (GCC) 6.3.0
Copyright © 2016 Free Software Foundation, Inc.
Это свободно распространяемое программное обеспечение. Условия копирования
приведены в исходных текстах. Без гарантии каких-либо качеств, включая
коммерческую ценность и применимость для каких-либо целей.

-O0: 0xffffcbec 0xffffcbec 1
-O1: 0xffffcbfc 0xffffcbfc 0
-O2: 0xffffcbf8 0xffffcc00 0
-O3: 0xffffcbf8 0xffffcc00 0
-Os: 0xffffcbf8 0xffffcc00 0

Под Ubuntu результат аналогичен вашему.
Для GCC на Вашей машине надо в примере поменять в декларации a и b местами
int b,a;
В правильной версии кода первые два числа (адреса) должны быть одинаковы. Если они разные — то пример сломан. Как этого добиться — зависит от компилятора. Суть проблемы показывает лишь третье число при условии что первые два равны.

Выше к слову есть отличная ссылка из которой следует что пример можно изящно расширить
#include <stdio.h>

int main(void) {
int a = 1, b = 2;
int *p = &a;
int *q = &b + 1;
*p = 11;
printf("%p %p %d %d %d\n", (void *)p, (void *)q, p == q, a, b);
return 0;
}

Вы в test.fish в командную строку компилятора не добавили опцию -O, только вывели её на экран.

НЛО прилетело и опубликовало эту надпись здесь
> int *p = malloc(64 * sizeof(int));
> int *q = malloc(64 * sizeof(int));
> if (p < q) // неопределённое поведение
> foo();

То есть теоретически выполнение такого сравнения может отформатировать жёсткий диск, или сделать всё остальное, чем обычно пугают при рассказах о UB? Как страшно программировать на Си…
Примечание. Через некоторое время, уже после публикации перевода на Хабре автор внёс большие модификации в текст статьи. Обновлять перевод на Хабре не очень хорошая идея, так как некоторые комментарии потеряют смысл или будут смотреться неуместно. Публиковать текст, как новую статью, тоже не хочется. Поэтому мы просто обновили перевод статьи на сайте viva64.com, а здесь оставили всё как есть. Если Вы новый читатель, то предлагаю читать более свежий перевод на нашем сайте, перейдя по приведённой выше ссылке.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий