Комментарии 54
Secure Coding Standard
Это было сделано потому, что в компиляторах для embedded платформ эта фича была практически не востребованной, но в тоже время требовала заметных усилий в реализации. Именно для того, чтобы не тратить усилия реализацию ненужной фичи только ради того, чтобы компилятор стал conforming, VLA сделали опциональными.
В C++ у этого достаточно простая мотивация — стандарт требует, чтобы различные однотипные объекты имели строго различные адреса. Если допустить «пустые» объекты вообще (а не в специфичных ситуациях типа empty base class optimization, где от них нет вреда), то может возникнуть проблема, например, когда в массиве таких объектов все элементы будут иметь совпадающие типы и адреса. Тогда не получится проверять их на тождественность простым сравнением адресов, не будет отношения порядка для таких указателей, в общем, такие объекты все равно будут ущербными, без идентичности и с кучей оговорок, а стандарт усложнится.
Некоторые годные фичи не знал.
Это к тому, что в недавней теме selgjos заявлял, что с++ = си[++], и мне показалось странным, что его не поняли. Заглядывать же смотреть «под капот» самому сейчас желания не достаточно много.
В отличие от какой-нибудь джавы, где сразу в усмирительной рубашке рождаешься :)]-<
В паскале были строки точно, хоть и байтовой длины, это уже высоко.
c# — достаточно того, что там память не ручная.
Маплы всякие…
В c/c++ нет вообще ничего высокоуровнего, встроенного в язык(реализация компилятора не имеет отношения к стандарту языка). Или что-то упускаю?
В c/c++ нет вообще ничего высокоуровнего, встроенного в язык(реализация компилятора не имеет отношения к стандарту языка). Или что-то упускаю?
в вашем понимании, каким требованиям должен удовлетворять яп, чтобы считаться высокоуровневым?
Абстракции же языка с/с++ намеренно максимально ограничены архитектурой процессора, что естественно для языка низкого уровня. Хотя в с/с++ можно записывать арифметические выражения одной строкой. Это все-таки высокий уровень, но такая возможность слишком популярна в яп, поэтому не учитываю. Те же виртуальные функции доступны и на ассемблере как массив адресов, с++ позволяет этим пользоваться в удобной форме.
Короче ладно, не важно это все.
Наличие сущностей в языке, которые не элементарно записываются на машинном языке
std::sort
условный переход с примочками
Ха, так условный переход — это JZ или там LOOP, если уж говорить о цикле. Но это только маленькая часть, самое главное — expression evaluation.
Фактически эта возможность существует также только в Ассемблере, где метка — лишь адрес в коде
Фактически эта возможность существует в Фортране чуть с самого начала (в стандарте 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-указателю в С запрещено. Где вы увидели в 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
работать не будет.
Где конкретно? Там явно написано «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 toE
(even ifE
is a null pointer), and&(E1[E2])
to((E1)+(E2))
. It is
always true that ifE
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 toE
. If*P
is an lvalue andT
is the name of
an object pointer type,*(T)P
is an lvalue that has a type compatible with that to whichT
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
тут вообще собоку припеку. И этк конструкция — нелегальна, ибо содержит применение оператора ->
к указателю, не являющемуся указателем на объект.
Очень жаль, что вы все время об этом разговаривали, потому что я, как как и было написано в моем первом сообщении, говорю о том, что в этой конструкции нет разыменования (т.е. применения 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
никакой аннигиляции нет и такое применение &
— нелегально.
Не надо пытаться непрерывно повторять одни и те же домыслы — они от этого правильнее не станут.
Если спецификация языка говорит, что поведение не определено — то поведение не определено. И в данном случае оно именно не определено. Конец дискуссии.
А то, что у какого-нибудь "васипупкина" это "всегда работало" — это не аргумент для данного разговора.
Где‐то в блоге PVS‐Studio эта проблема обсуждалась: авторы данного статического анализатора считают, что, во‐первых, неопределённое поведение есть, во‐вторых, вы вряд ли найдёте более‐менее популярный компилятор, где реализованный через такое неопределённое поведение offsetof
выдаст неверный результат. Авторы GCC тоже ведь зачем‐то сделали __builtin_offsetof
, а не оставили макрос с NULL, как, к примеру, авторы tcc
или pcc
. clang
тоже имеет __builtin_offsetof
, но они, скорее, сделали всё для совместимости.
Ещё: не знаю, почему, но Vim вместо offsetof
или NULL
хака использовал глобальную переменную. Вероятно, «неопределённое поведение» где‐то всё же создавало проблемы, а Bram не любит расставаться с поддержкой морально устаревших компиляторов (т.е. тех, что не имеют offsetof
макроса).
Где‐то в блоге PVS‐Studio эта проблема обсуждалась: авторы данного статического анализатора считают, что, во‐первых, неопределённое поведение есть
Неопределенное поведение в таком коде, если он будет написан в пользовательском пространстве — есть. Этот вопрос не обсуждается.
Другое дело, что, как я уже сказал выше, этот критерий неприменим к коду, написанному на территории стандартной библиотеки, о чем было в свое время ясно сказано в DR#44 (http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_044.html).
Понятно, что в случае макроподстановок в общем случае трудно/невозможно отличить, откуда "пришел" код, по каковой причине такая реализация действительно будет отлавливаться анализаторами.
Реализация через глобальную переменную поможет решить обе проблемы оригинального offsetof
: и устранит разыменование null-указателя, и устранит зависимость от физического значения null-указателя.
$ 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
Расширения языков C и C++. Часть 1