Pull to refresh

Comments 54

На сколько я в курсе, атрибут noreturn означает, что функция никогда не возвращает управление, а не результат.
Да именно так, спасибо, исправил. Всякие exit(), terminate() или безусловно выбрасывающие исключение.
UFO just landed and posted this here
По слухам, потому что MS не хочет их реализовывать, но хочет получить conformity со стандартом для C11.

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

а в чем концептуальная сложность реализации?
>И в Си их размер (sizeof) действительно равен нулю, в отличие от С++ где это почему-то 1 байт.
В C++ у этого достаточно простая мотивация — стандарт требует, чтобы различные однотипные объекты имели строго различные адреса. Если допустить «пустые» объекты вообще (а не в специфичных ситуациях типа empty base class optimization, где от них нет вреда), то может возникнуть проблема, например, когда в массиве таких объектов все элементы будут иметь совпадающие типы и адреса. Тогда не получится проверять их на тождественность простым сравнением адресов, не будет отношения порядка для таких указателей, в общем, такие объекты все равно будут ущербными, без идентичности и с кучей оговорок, а стандарт усложнится.
Десигнаторы в списках инициализации, насколько я помню, входят в стандарт C99. Непонятно, почему они отнесены к расширениям.
У них расширения в том числе и для режима компиляции C90 (многие расширения в этом списке именно такие). И кроме того я хотел упомянуть про десигнаторы потому, что они не включены в расширения С++, а хотелось бы :)
Кайф. Спасибо!
Некоторые годные фичи не знал.
Немного не в тему, C++ так же как и Си состоит из указателей, дефолтных типов и условных переходов, все остальное — обертка на уровне синтаксиса с минимальным количеством лишних сущностей в результирующем коде, в том смысле, что существенные нагромождения программист пишет сам(/использует библиотеки), я правильно понимаю?
Это к тому, что в недавней теме selgjos заявлял, что с++ = си[++], и мне показалось странным, что его не поняли. Заглядывать же смотреть «под капот» самому сейчас желания не достаточно много.
UFO just landed and posted this here
Да, эквивалентны. И при этом являются языками низкого уровня (одинаково низкого уровня), хотя есть выверенные «мудрецами» шаблоны создания сущностей, которые кажутся высокоуровневыми, но только в рамках наших абстракций. С++ почти не ограничивает программиста и все внутренности торчат наружу, поэтому не могу его считать языком высокого уровня и видить в нем принципиальные отличия от си (речь не про полноту).
В отличие от какой-нибудь джавы, где сразу в усмирительной рубашке рождаешься :)]-<
UFO just landed and posted this here
В фортране много встроенных функций математических (с++ функции уровня синтаксиса типа typeof — совсем другое), есть комплексные переменные и 2d-массивы, строки какие-то вроде бы были.
В паскале были строки точно, хоть и байтовой длины, это уже высоко.
c# — достаточно того, что там память не ручная.
Маплы всякие…

В c/c++ нет вообще ничего высокоуровнего, встроенного в язык(реализация компилятора не имеет отношения к стандарту языка). Или что-то упускаю?
UFO just landed and posted this here
Это замечательно, с++ очень мощный. В совокупности с библиотеками им можно пользоваться как сколь угодно высокоуровневым, с минимальными потерями в скорости. По-моему в этом и заключается вся прелесть языка с++. Вседозволенность компенсируется богатыми средствами создания ограничений, но это на уровне синтаксиса и возлагается на пользователя. Сам язык при этом низкоуровневый и позволяет очень многое.
В c/c++ нет вообще ничего высокоуровнего, встроенного в язык(реализация компилятора не имеет отношения к стандарту языка). Или что-то упускаю?

в вашем понимании, каким требованиям должен удовлетворять яп, чтобы считаться высокоуровневым?
Наличие сущностей в языке, которые не элементарно записываются на машинном языке, то есть «настоящих» абстракций, которые введены в язык не потому что их легко построить для архитектуры процессора, а потому что их посчитал удобными создатель языка (который, возможно, вообще, с другой планеты).
Абстракции же языка с/с++ намеренно максимально ограничены архитектурой процессора, что естественно для языка низкого уровня. Хотя в с/с++ можно записывать арифметические выражения одной строкой. Это все-таки высокий уровень, но такая возможность слишком популярна в яп, поэтому не учитываю. Те же виртуальные функции доступны и на ассемблере как массив адресов, с++ позволяет этим пользоваться в удобной форме.

Короче ладно, не важно это все.
Наличие сущностей в языке, которые не элементарно записываются на машинном языке

std::sort
Все, что в stl можно вообще не использовать и не линковаться с этим всем. Это не является элементом языка. В этом большой плюс С++.
А «элементарно» — это как? Вот тот же сишный for — он элементарно записывается, или нет? Где граница?
UFO just landed and posted this here
Элементарно — условный переход с примочками. Не знаю где граница. В моих гуманитарных высерах отсутствует строгость, одни эмоции.
Я просто представляю себе мощный макроассемблер, тот же Fasm — в нем есть if then else, например. Но это — ассемблер, не ЯВУ.

условный переход с примочками

Ха, так условный переход — это JZ или там LOOP, если уж говорить о цикле. Но это только маленькая часть, самое главное — expression evaluation.
Парсер — это высокоуровневая возможность, но есть почти во всех ЯП, поэтому проигнорил ее. Только не пойму как устройство цикла for связано с парсером выражений. Анализ того, как именно реализовать цикл, перевычислять ли значения и т.д. — это уже оптимизация и относится к реализации компилятора, а не к ЯП.
Это отдельные высказывания, последняя фраза — самостоятельна.

Конструкция сишного цикла не ложится элементарно на машинный язык. while — да, это один в один JZ, а for — нет.
Спасибо за статью, очень интересно
Фактически эта возможность существует также только в Ассемблере, где метка — лишь адрес в коде

Фактически эта возможность существует в Фортране чуть с самого начала (в стандарте 66 точно была) и называется «вычисляемый goto», так что это не только низкоуровневый прием.

Ну то есть просто тупо перевели статью из доков GCC…


Массивы run-time размера, надо заметить, являются расширением только в С++. В языке С это — стандартная фича начиная с С99.


Отдельно можно заметить, что "метки как значения" присутствовали на заре зарождения языка С — в CRM С.

Оно даже в Бейсике есть.
> А между тем еще в Турбо Паскале была возможность вкладывать одни функции в другие.
Как уточнение: вложенные функции были в Паскале с самого начала и, вообще, присутствуют почти во всех языках, происходящих от Алгола-60, C здесь как-раз исключение.

> Однако, это является неопределенным поведением по стандарту Си (из-за разыменования нуля)
offsetof() NULL не разыменовывает, он выполняет арифметические операции над NULL-pointer, что совершенно законно.

Во-первых, применять адресную арифметику к null-указателям в С и С++ строжайше запрещено (undefined behavior). Поэтому о каком "совершенно законно" вы говорите и откуда вы это взяли — совершенно не ясно.


Во-вторых, "традиционная" реализация offsetof действительно делает разыменование null-указателя. Однако придираться к этому бесполезно. Как реализовано offsetof никого интересовать не должно. На реализацию стандартной библиотеки требования и правила языка С не распространяются. Спецификация языка гарантирует, что это стандартное макро работает — значит оно работает. Формально, стандартная библиотека не реализована на языке С вообще, а реализована на некоем платформенно-зависимом псевдокоде, любые сходства которого с языком С ограничены лишь интерфейсной совместимостью.

&(*((type *)NULL)) — это вполне законный код, C99 явно оговаривает в сноске к 6.5.3.2. В начале 6.6 также говорится, что для создания адресной константы должен быть использован & и могут быть использованы [], ., ->, & (унарный) и * (унарная), также как и приведения, но при этом к значению объекта не будет производится обращений. Остаётся только вопрос о том, что должно давать приведение адреса к числу, я сейчас не скажу, где это описано.

К чему вы это приплели? Что такое &(*((type *)NULL)) и к чему это здесь?


Напоминаю, что речь идет о выражении (size_t)&(((s *)0)-›m), которое, во-первых, содержит запрещенное разыменование нулевого указателя оператором -> и, во-вторых, завязано на физическое значение нулевого указателя на данной платформе.


Именно по этим двум причинам подобная реализация offsetof допустима только в рамках стандартной библиотеки конкретной платформы. Это не язык С.


Отдельно стоит заметить, что "создание адресной константы" обязано подчиняться всем остальным требованиям языка. Никакого отдельного разрешения на разыменование null-указателя в контексте "создания адресной константы" язык С не дает.


Однако средства статического анализа этого обычно "не знают" и начинают орать во все воронье горло на попытку разыменования null-указателя в offsetof. Именно поэтому в GCC приняли решение заменить реализацию offsetof не непрозрачный builtin.

Во-первых, применять адресную арифметику к null pointer стандартом C явно разрешено (см. C99, 6.5.3.2, особенно примечание 74, где явно указано, что даже разыменование нулевого указателя бывает законно). Во-вторых «разыменование null pointer» это применение операции разыменования (unary * operator) к нулевому указателю, а в макросе offsetof() такая операция просто-напросто отсутствует: этот макрос ничего не разыменовывает.

Во-первых, повторяю еще раз: применять адресную арифметику к null-указателю в С запрещено. Где вы увидели в 6.5.3.2 хоть малейшее упоминание адресной арифметики — мне в упор не ясно. Адресная арифметика и ее правила в С определны в 6.5.6.


Во-вторых, разыменовывать null-указатель в С запрещено. Пункт 6.5.3.2, на который вы сослались, нигде не содрежит разрешения на разыменование null-указателя. Наоборот 6.5.3.2/1 это запрещает. Пункт 6.5.3.2 лишь говорит, что комбинация операторов & и * может "взаимно аннигилироваться" и тем самым предотвращать запрещенное разыменование.


В-третьих, "традиционная" хак-реализация offsetof(size_t)&(((s *)0)-›m) — построена на применении оператора -> к null-указателю. Оператор -> — это разыменование указателя.


В-четветых, работоспособность выражения (size_t)&(((s *)0)-›m) в качестве вычислителя смещения держится еще на том, что нулевой указатель соответствует физическому адресу 0. Такого язык С никогда не гарантировал. Физический адрес, соответствующий нулевому указателю — зависит от платформы. На некоей платформе этоможет быть 0xFFFFFFFF или 0xBAADF00D. На такой платформе такая реализация offsetof работать не будет.

> Наоборот 6.5.3.2/1 это запрещает.
Где конкретно? Там явно написано «The operand of the unary & operator shall be [...] unary * operator» и никаких ограничений на аргумент * нет, т.е. явно разрешается &(*(X)) для произвольного X.

Что значит «предотвращает запрещенное разыменование»? Стандарт просто отмечает, примечанием, что результат &(*0) должен быть 0, там ничего не говорится ни про какую «аннигиляцию».

> Оператор -> — это разыменование указателя.
Смешались в кучу кони, люди. :-)

С точки зрения стандарта, -> это не разыменование, семантика -> описана специальным образом (6.5.2.3), но стандарт гарантирует, что при некоторых условиях (X)->Y эквивалентно (*(X)).Y (примечание 69).

С неформальной точки зрения, offsetof(A, B) это просто константа компиляции, и никакого разыменования, т.е. обращения к памяти, ее вычисление не требует.

> На такой платформе такая реализация offsetof работать не будет.
С этим никто не спорит, у offset() есть и гораздо менее экзотические проблемы. И также понятно, что традиционная реализация вызывает undefined behaviour. Но вовсе не из-за отсутствующего там разыменования, а потому, что семантика A->B стандартом не сводится к арифметике над указателями (в отличие от семантики A[B]).

Я веду речь именно и только о языке С, который определяет унарный * только для случаев, когда операнд указывает на объект или функцию.


The unary * operator denotes indirection. If the operand points to a function, the result is
a function designator; if it points to an object, the result is an lvalue designating the
object. If the operand has type ‘‘pointer to type’’, the result has type ‘‘type’’. If an
invalid value has been assigned to the pointer, the behavior of the unary * operator is
undefined.102)

Сноска 102 однозначно поясняет:


Thus, &*E is equivalent to E (even if E is a null pointer), and &(E1[E2]) to ((E1)+(E2)). It is
always true that if E is a function designator or an lvalue that is a valid operand of the unary &
operator, *&E is a function designator or an lvalue equal to E. If *P is an lvalue and T is the name of
an object pointer type, *(T)P is an lvalue that has a type compatible with that to which T points.

Among the invalid values for dereferencing a pointer by the unary * operator are a null pointer, an address inappropriately aligned for the type of object pointed to, and the address of an object after the
end of its lifetime.

Как вы умудрились не увидеть "аннигиляцию" мне не ясно, ибо в стандарте ясно сказано


If the operand is the result of a unary * operator, neither that operator nor the & operator is evaluated and the result is as if both were omitted

Это именно та самая аннигиляция. Опять же та же самая сноска 102 в первой своей части ясно говорит то же самое.


Где вы выкопали "семантика -> описана специальным образом" мне в упор не ясно. Даю под запись: семантика -> в С описана тем же образом, что и семантика *: указатель должен указывать на объект.


The value is that of the named member of the object to which the first expression points

Null-указатель не является указателем на объект.


примечание 69)

Что? Примечание 69 не имеет к этому никакого отношения. Что за "стандарт" вы там читаете?


С неформальной точки зрения, offsetof(A, B) это просто константа компиляции, и никакого разыменования, т.е. обращения к памяти, ее вычисление не требует.

В огороде бузина, а в Киеве дядька...


Никто не говорит, что offsetof(A, B) вдруг как-то требует обращения к памяти. Речь в данном разговоре идет о формальной легальности конструкции (size_t)&(((s *)0)-›m) в пространстве пользовательского кода. Сам offsetof тут вообще собоку припеку. И этк конструкция — нелегальна, ибо содержит применение оператора -> к указателю, не являющемуся указателем на объект.

> Речь в данном разговоре идет о формальной легальности конструкции (size_t)&(((s *)0)-›m)
Очень жаль, что вы все время об этом разговаривали, потому что я, как как и было написано в моем первом сообщении, говорю о том, что в этой конструкции нет разыменования (т.е. применения unary *), вне зависимости от того, легальна ли вся конструкция.

Например (приходится повторить), в конструкции A[B] есть разыменование, потому что стандарт явно определяет ее как (*(A+B)). Для конструкции A->B такого сведения нет. Вы, кажется, пользуетесь каким-то своим неформальным пониманием разыменования, не определив его явно.

> И этк конструкция — нелегальна, ибо содержит применение оператора -> к указателю, не являющемуся указателем на объект.
Вы можете процитировать соответствующий пункт из списка undefined behaviours в конце стандарта?
Для конструкции A->B такого сведения нет. Вы, кажется, пользуетесь каким-то своим неформальным пониманием разыменования, не определив его явно.

Вы, похоже, пытаетесь выкрутиться из ситуации, придираясь к слову "разыменование". Давайте забудем об этом слове. Применение оператора -> к нулевому указателю запрещено и без этого слова. Спецификация оператора -> ясно говорит, что указатель слева от -> должен указывать на объект.


Вы можете процитировать соответствующий пункт из списка undefined behaviours в конце стандарта?

Нет, навскидку не вижу там такого пункта. Но это не так важно, ибо соответствующий раздел — "informative", а не "normative".

> Вы, похоже, пытаетесь выкрутиться из ситуации, придираясь к слову «разыменование».
В моих комментариях, которые вам необходимо перечитать, с самого начала и неоднократно было указано, что я понимаю «разыменование», как применение unary operator *. Каким образом вы его понимаете — до сих пор непонятно.

> Но это не так важно, ибо соответствующий раздел — «informative», а не «normative».
Есть другие примеры undefined behaviour, которые авторы решили не включать в этот список, или это удивительное исключение?

Отдельно, кстати, стоит заметить, что и на операнд унарного & тоже налагаются строгие требования


The operand of the unary & operator shall be either a function designator, the result of a
[] or unary * operator, or an lvalue that designates an object that is not a bit-field and is
not declared with the register storage-class specifier.

Lvalue, полученное через ->, примененный к нулевому указателю, не "designates an object", по каковой причине такое применение унарного & вызывает неопределенное поведение.


В случае &*E для нулевого указателя E ситуацию спасает взаимная аннигиляция & и *, как уже говорилось выше. Но в случае &E->member никакой аннигиляции нет и такое применение & — нелегально.

UFO just landed and posted this here

Не надо пытаться непрерывно повторять одни и те же домыслы — они от этого правильнее не станут.


Если спецификация языка говорит, что поведение не определено — то поведение не определено. И в данном случае оно именно не определено. Конец дискуссии.


А то, что у какого-нибудь "васипупкина" это "всегда работало" — это не аргумент для данного разговора.

Где‐то в блоге PVS‐Studio эта проблема обсуждалась: авторы данного статического анализатора считают, что, во‐первых, неопределённое поведение есть, во‐вторых, вы вряд ли найдёте более‐менее популярный компилятор, где реализованный через такое неопределённое поведение offsetof выдаст неверный результат. Авторы GCC тоже ведь зачем‐то сделали __builtin_offsetof, а не оставили макрос с NULL, как, к примеру, авторы tcc или pcc. clang тоже имеет __builtin_offsetof, но они, скорее, сделали всё для совместимости.


Ещё: не знаю, почему, но Vim вместо offsetof или NULL хака использовал глобальную переменную. Вероятно, «неопределённое поведение» где‐то всё же создавало проблемы, а Bram не любит расставаться с поддержкой морально устаревших компиляторов (т.е. тех, что не имеют offsetof макроса).

Обычный offsetof() может привести к undefined behaviour, когда применяется как offsetof(struct foo, array[n]), где n — больше числа задекларированных элементов в поле-массиве. Типичная ситуация: массив нулевой длины в конце структуры, фактически память под массив выделяется при аллокации.
Где‐то в блоге PVS‐Studio эта проблема обсуждалась: авторы данного статического анализатора считают, что, во‐первых, неопределённое поведение есть

Неопределенное поведение в таком коде, если он будет написан в пользовательском пространстве — есть. Этот вопрос не обсуждается.


Другое дело, что, как я уже сказал выше, этот критерий неприменим к коду, написанному на территории стандартной библиотеки, о чем было в свое время ясно сказано в DR#44 (http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_044.html).


Понятно, что в случае макроподстановок в общем случае трудно/невозможно отличить, откуда "пришел" код, по каковой причине такая реализация действительно будет отлавливаться анализаторами.


Реализация через глобальную переменную поможет решить обе проблемы оригинального offsetof: и устранит разыменование null-указателя, и устранит зависимость от физического значения null-указателя.

Кстати, насчет редкоиспользуемости goto. Возьмем, наприемр, mysql:

$ apt-get source mysql-5.6
/tmp/1/mysql-5.6-5.6.33$ grep -R goto --include=*.cc --include=*.c --include=*.cpp |wc -l
6966
Ну так в си (за неимением деструкторов и raii) ничего лучше goto exit; где после exit очистка ресурсов перед выходом, не придумали
Sign up to leave a comment.

Articles

Change theme settings