У C репутация негибкого языка. Но вы знаете, что вы можете изменить порядок аргументов функции в C, если он вам не нравится?
#include <math.h>
#include <stdio.h>
double DoubleToTheInt(double base, int power) {
return pow(base, power);
}
int main() {
// приводим к указателю на функуцию с обратным порядком аргументов
double (*IntPowerOfDouble)(int, double) =
(double (*)(int, double))&DoubleToTheInt;
printf("(0.99)^100: %lf \n", DoubleToTheInt(0.99, 100));
printf("(0.99)^100: %lf \n", IntPowerOfDouble(100, 0.99));
}
Этот код на самом деле никогда не определяет функцию IntPowerOfDouble
— потому что функции IntPowerOfDouble
не существует. Это переменная, указывающая на DoubleToTheInt
, но с типом, который говорит, что ему хочется, чтобы аргумент типа int
шел перед аргументом типа double
.
Вы могли бы ожидать, что IntPowerOfDouble
примет аргументы в том же порядке, что и DoubleToTheInt
, но приведет аргументы к другим типам, или что-то типа того. Но это не то, что происходит.
Попробуйте — вы увидите одинаковый результат в обоих строчках.
emiller@gibbon ~> clang something.c
emiller@gibbon ~> ./a.out
(0.99)^100: 0.366032
(0.99)^100: 0.366032
Теперь попробуйте изменить все int
на float
— вы увидите, что FloatPowerOfDouble
делает что-то ещё более странное. Да,
double DoubleToTheFloat(double base, float power) {
return pow(base, power);
}
int main() {
double (*FloatPowerOfDouble)(float, double) =
(double (*)(float, double))&DoubleToTheFloat;
printf("(0.99)^100: %lf \n", DoubleToTheFloat(0.99, 100)); // OK
printf("(0.99)^100: %lf \n", FloatPowerOfDouble(100, 0.99)); // Упс...
}
выдает:
(0.99)^100: 0.366032
(0.99)^100: 0.000000
Значение во второй строке "даже не ошибочное" — если бы проблема была в перестановке аргументов, мы бы ожидали, что ответ будет 100^0.99 = 95.5 а не 0. Что происходит?
Примеры кода выше представляют каламбуры типизации функций(type punning of functions) — опасную форму "ассемблера без ассемблера" который никогда не должен использоваться на работе, рядом с тяжелой техникой или в сочетании с отпускаемыми по рецепту лекарствами. Эти примеры абсолютно логичны для тех, кто понимает код на уровне ассемблера — но, скорее всего, запутает всех остальных.
Я немного смухлевал — предположил, что вы запустите код на 64-битном x86 компьютере. На другой архитектуре этот фокус может не сработать. Хоть и считается, что у C бесконечное количество темных углов, поведение с аргументами int и double точно не является частью стандарта C. Это результат того, как вызываются функции на современных x86 машинах, и может быть использовано для изящных программистских трюков.
Это не моя сигнатура
Если вы изучали C в университете, вы может быть помните, что аргументы передаются функции на стек. Вызывающий кладет аргументы на стек в обратном порядке, а функция считывает аргументы со стека.
По крайней мере, мне объяснили это именно так, но большинство компьютеров сегодня передают первые несколько аргументов прямо в регистры CPU. Таким образом функции не понадобится читать из стека, что гораздо медленнее регистров.
Количество и расположение регистров, используемых для аргументов функций зависит от соглашения о вызовах(calling convention). У Windows одно соглашение — четыре регистра для значений с плавающей точкой и четыре регистра для указателей и целых чисел. у Unix другое соглашение, называющееся соглашение System V. В нём для аргументов с плавающей точкой предназначено восемь регистров и еще шесть — для указателей и целых чисел. (Если аргументы не влазят в регистры, то они отправляются по старому на стек.)
В C, заголовочные файлы существуют только чтобы сказать компилятору, куда класть аргументы функции, зачастую комбинируя регистры и стек. У каждого соглашения о вызовах есть свой алгоритм для расположения этих аргументов в регистрах и на стеке. Unix, например, очень агрессивен насчет разбивания структур и попыток уместить все поля в регистрах, в то время как Windows немного ленивее и просто передает указатель на большую структуру-параметр.
Но и в Windows, и в Unix, базовый алгоритм работает так:
- Аргументы с плавающей точкой расположены, по порядку, в регистрах SSE, обозначенных XMM0, XMM1 и т.д.
- Целые и указатели расположены, по порядку, в регистрах общего назначения, обозначенных RDX, RCX и т.д.
Давайте посмотрим, как передаются аргументы функции DoubleToTheInt
.
Сигнатура функции такова:
double DoubleToTheInt(double base, int power);
Когда компилятор встречает DoubleToTheInt(0.99, 100)
, он располагает регистры так:
RDX | RCX | R8 | R9 |
---|---|---|---|
100 | ??? | ??? | ??? |
XMM0 | XMM1 | XMM2 | XMM3 |
0.99 | ??? | ??? | ??? |
(Для простоты, я использую соглашение о вызовах Windows.) Если бы взамен была такая функция:
double DoubleToTheDouble(double base, double power);
Аргументы были бы расположены так:
RDX | RCX | R8 | R9 |
---|---|---|---|
??? | ??? | ??? | ??? |
XMM0 | XMM1 | XMM2 | XMM3 |
0.99 | 100 | ??? | ??? |
Теперь вы, возможно, догадались, почему маленьких фокус из начала статьи работает. Рассмотрим следующую сигнатуру функции:
double IntPowerOfDouble(int y, double x);
Вызывая IntPowerOfDouble(100, 0.99)
, компилятор расположит регистры так:
RDX | RCX | R8 | R9 |
---|---|---|---|
100 | ??? | ??? | ??? |
XMM0 | XMM1 | XMM2 | XMM3 |
0.99 | ??? | ??? | ??? |
Другими словами, точно так же, как в DoubleToTheInt(0.99, 100)
!
Из-за того, что скомпилированная функция понятия не имеет, как она была вызвана — только где в регистрах и на стеке ожидать свои аргументы — мы можем вызвать функцию с другим порядком аргументов приведя указатель на функцию к неверной (но ABI-совместимой) сигнатуре функции.
Фактически, пока целые аргументы и аргументы с плавающей точкой сохраняют порядок, мы можем перемешивать их как угодно, и расположение регистров будет одинаковым. То есть, у
double functionA(double a, double b, float c, int x, int y, int z);
будет такое же расположение регистров, как и у:
double functionB(int x, double a, int y, double b, int z, float c);
и такое же, как у:
double functionC(int x, int y, int z, double a, double b, float c);
Во всех трех случаях в регистрах будет:
RDX | RCX | R8 | R9 |
---|---|---|---|
int x |
int y |
int z |
??? |
XMM0 | XMM1 | XMM2 | XMM3 |
double a |
double b |
double c |
??? |
Обратите внимание, что и аргументы двойной, и аргументы одинарной точности занимают регистры XMM — но они не ABI-совместимы друг с другом. Поэтому, если вы помните второй пример кода в самом начале, причина по которой FloatPowerOfDouble
вернул ноль (а не 95.5) в том, что компилятор расположил значение одинарной точности (32-битное) 100.0 в XMM0, и значение двойной точности (64-битное) 0.99 в XMM1 — но вызываемая функция ожидала число двойной-точности в XMM0 и одинарной в XMM1. Из-за этого, экспонента притворилась мантиссой, биты мантиссы были обрезаны или приняты за экспоненту, и функция FloatPowerOfDouble
возвела Очень Маленькое Число в степень Очень Большого Числа, получив ноль. Загадка решена.
Обратите внимание на ??? в таблицах выше. Значения этих регистров не определено — там может быть любое значение из предыдущих вычислений. Вызываемой функции не важно, что в них, и она может перезаписывать их во время исполнения.
Это создает интересную возможность — вдобавок к вызову функции с другим порядком аргументов, также можно вызвать функцию с другим количеством аргументов. Есть несколько причин, по которым можно захотеть сделать что-то настолько сумасшедшее.
Наберите 1-800-I-Really-Enjoy-Type-Punning
Попробуйте это:
#include <math.h>
#include <stdio.h>
double DoubleToTheInt(double x, int y) {
return pow(x, y);
}
int main() {
double (*DoubleToTheIntVerbose)(
double, double, double, double, int, int, int, int) =
(double (*)(double, double, double, double, int, int, int, int))&DoubleToTheInt;
printf("(0.99)^100: %lf \n", DoubleToTheIntVerbose(
0.99, 0.0, 0.0, 0.0, 100, 0, 0, 0));
printf("(0.99)^100: %lf \n", DoubleToTheInt(0.99, 100));
}
Неудивительно, что в обеих строках одинаковый результат — все аргументы помещаются в регистры, и расположение регистров одинаковое.
Теперь начинается веселье. Мы можем определить новый "многословный" тип функции который может вызывать много разных типов функций, при условии что аргументы влазят в регистры и функцию возвращают один и тот же тип.
#include <math.h>
#include <stdio.h>
typedef double (*verbose_func_t)(double, double, double, double, int, int, int, int);
int main() {
verbose_func_t verboseSin = (verbose_func_t)&sin;
verbose_func_t verboseCos = (verbose_func_t)&cos;
verbose_func_t verbosePow = (verbose_func_t)&pow;
verbose_func_t verboseLDExp = (verbose_func_t)&ldexp;
printf("Sin(0.5) = %lf\n",
verboseSin(0.5, 0.0, 0.0, 0.0, 0, 0, 0, 0));
printf("Cos(0.5) = %lf\n",
verboseCos(0.5, 0.0, 0.0, 0.0, 0, 0, 0, 0));
printf("Pow(0.99, 100) = %lf\n",
verbosePow(0.99, 100.0, 0.0, 0.0, 0, 0, 0, 0));
printf("0.99 * 2^12 = %lf\n",
verboseLDExp(0.99, 0.0, 0.0, 0.0, 12, 0, 0, 0));
}
Такая совместимость типов удобна потому что мы можем, например, создать простой калькулятор, который отсылает к любой функции, которая принимает и возвращает числа двойной точности:
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef double (*four_arg_func_t)(double, double, double, double);
int main(int argc, char **argv) {
four_arg_func_t verboseFunction = NULL;
if (strcmp(argv[1], "sin") == 0) {
verboseFunction = (four_arg_func_t)&sin;
} else if (strcmp(argv[1], "cos") == 0) {
verboseFunction = (four_arg_func_t)&cos;
} else if (strcmp(argv[1], "pow") == 0) {
verboseFunction = (four_arg_func_t)&pow;
} else {
return 1;
}
double xmm[4];
int i;
for (i=2; i<argc; i++) {
xmm[i-2] = strtod(argv[i], NULL);
}
printf("%lf\n", verboseFunction(xmm[0], xmm[1], xmm[2], xmm[3]));
return 0;
}
Проверяем:
emiller@gibbon ~> clang calc.c
emiller@gibbon ~> ./a.out pow 0.99 100
0.366032
emiller@gibbon ~> ./a.out sin 0.5
0.479426
emiller@gibbon ~> ./a.out cos 0.5
0.877583
Не совсем конкурент Mathematica, но можно представить более сложную версию с таблицей имен функций и соответсвующих им указателей на функцию — для добавления новой функции достаточно обновить таблицу, а не явно вызывать новую функцию в коде.
Другое применение включает JIT компиляторы. Если вы когда-нибудь занимались по туториалу LLVM, вы могли неожиданно встретить сообщение:
"Full-featured argument passing not supported yet!"
LLVM искусно превращает код в машинные коды и загружает машинные коды в память, но не очень гибок, если нужно вызвать загруженную в память функцию. С помощью LLVMRunFunction
, вы можете вызывать main()
-подобные функции (целый аргумент, аргумент-указатель, аргумент-указатель, возвращает целое), но не многое другое. Большинство туториалов рекомендует обернуть вашу функцию компилятора функцией похожей на main()
, пряча все ваши аргументы за аргументом-указателем, и использовать обертку чтобы вытянуть аргументы из указателя и вызвать настоящую функцию.
Но с нашими новыми знаниями о регистрах X86, мы можем упростить церемонию, избавившись от функции-обертки во многих случаях. Вместо того, чтобы проверять, что функция пренадлежит к ограниченному списку C-callable сигнатур функций (int main()
, int main(int)
, int main(int, void *)
и т.д.), мы можем создать указатель, сигнатура которго заполняет все регистры параметров и, следовантельно, совместима со всеми функциями, которые передают аргументы только через регистры, и вызывать их, передавая ноль (или что угодно) для неиспользуемых аргументов. Нам надо всего лишь определить отдельный тип для каждого возвращаемого типа, а не для каждой возможной сигнатуры функции, и более гибко вызывать функции с помощью способа, который в другом случае потребовал бы использование ассемблера.
Я покажу вам последний фокус перед тем, как закрыть лавочку. Попробуйте разобраться как работает это код:
double NoOp(double a) {
return a;
}
int main() {
double (*ReturnLastReturnValue)() = (double (*)())&NoOp;
double value = pow(0.99, 100.0);
double other_value = ReturnLastReturnValue();
printf("Value: %lf Other value: %lf\n" value, other_value);
}
(Вам стоит для начала прочитать ваше соглашение о вызовах...)
Функция возвращает результат через XMM0. Между двумя функциями ничего не происходит, и в XMM0 остается результат последней функции, который NoOp
подхватывает как аргумент и возвращает.
Требуется немного ассемблера
Если вы когда-нибудь спросите на форуме программистов об ассемблере, обычным ответом будет: Тебе не нужен ассемблер — оставь его для гениальных докторов наук, которые пишут компиляторы. Да, держи пожалуйста руки на виду.
Писатели компиляторов умные люди, но я думаю, что ошибочно считать, что все остальные должны скрупулезно избегать ассемблер. В коротком набеге на каламбуры типизации мы увидели как расположение регистров и соглашение о вызовах — якобы исключительная забота занимающихся ассемблером писателей компиляторов — время от времени всплывает в C, и как использовать эти знания чтобы делать вещи которые обычные программисты C посчитали бы невозможными.
Но это лишь самая вершина айсберга программирования на ассемблере — специально представленная без единой строчки кода на ассемблере — и я советую всем, у кого есть время, поглубже окунуться в эту тему. Ассемблер — ключ к пониманию, как CPU занимается исполнением инструкций — что такое счетчик команд, что такое указатель фрейма, что такое указатель стека, что делают регистры — и позволяет вам посмотреть на программы в другом (более ярком) свете. Даже базовые знания могут помочь вам придумать решения, которые в ином случае даже не пришли бы вам в голову и понять что к чему, когда вы проскользнете мимо тюремных надзирателей своего любимого языка высокого уровня и будете щуриться на суровое, прекрасное солнце.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Комментарии (38)
QtRoS
14.08.2016 23:12Уточню — на x86 в 32-битной ОС поведения из первого примера нельзя наблюдать?
Я редко занимаюсь низкоуровневым программированием сейчас, но в старые времена и стек приходилось раскручивать самому в некоторых ситуациях, все было канонично в стековом фрейме: аргументы в обратном порядке, EIP, EBP, локальные переменные.qw1
14.08.2016 23:25+5На x86 было несколько ABI: __cdecl, __stdcall и __fastcall. Последний передавал два аргумента в регистрах, а остальные через стек. Так что зависело от опций компилятора, многие проекты в Release ставили __fastcall, чтобы оптимизировать код.
Randl
14.08.2016 23:44Уточню — на x86 в 32-битной ОС поведения из первого примера нельзя наблюдать?
Судя по беглому взгляду на calling conventions, нет. Даже там, где что-то передается через регистры, видимо, их слишком мало, чтобы разделять floating point и integer.
qw1
15.08.2016 00:38+7В __fastcall int-аргументы передаются черех ecx, edx, а floating-point — всегда через стек.
Поэтому трюк работает, код
выводит#include <stdio.h> void __fastcall test(double x, int a, int b) { printf("%f %d %d\n", x, a, b); } int main() { void (__fastcall *runme)(int, int, double) = (void (__fastcall *)(int, int, double))&test; runme(3, 5, 3.14); }
3.140000 3 5
tvi
15.08.2016 07:54-6Это платформозависимые пляски. Почему нельзя объявить inline функцию, в которой «поправить» расположение: и код будет переносим, а главное понятен и компилятору и программеру?
hdfan2
15.08.2016 07:57-2Очень вредная статья, я так считаю. Какой-нибудь джуниор запилит такое (они любят всякие прикольные штуки использовать), а потом кто-нибудь (скорее всего, вообще не он) поимеет несколько восхитительных часов отладки. Такие штуки, как и оружие, нужно выдавать только после длительных тренировок и после медкомиссии. И сажать за нелегальное использование.
Randl
15.08.2016 08:02+7Ножи очень вредные, потому что какой-нибудь ребенок может ими порезаться (они любят со всякими прикольными штуками играться).
Halt
15.08.2016 08:10+5Джуниор натворит гораздо больше бед, если будет свято уверен, что программа работает так, как он написал.
Подобные примеры позволяют заглянуть под капот иужаснутьсяпопытатья понять, как оно работает. После осознания того, насколько хрупок баланс костылей, желание трюкачить должно поубавиться.
P.S.: Если кому интересно, type punning в другом своем проявлении (и другие пляски) затрагиваются в докладе, запись которого можно найти у меня в публикациях.
mayorovp
15.08.2016 09:37+3Будет куда хуже, если джуниор найдет такую возможность "методом тыка" и начнет применять, даже не зная про хрупкость подобных конструкций.
gaki
16.08.2016 08:11В статье несколько раз прямым текстом говорится, что так делать _не надо_. Если этот джуниор настолько упорот, что всё равно не понимает, то он не на этом, так на чём другом рано или поздно отстрелит себе голову и всех вокруг забрызгает.
HighPredator
15.08.2016 09:44+9Это не каламбур. Это неопределенное поведение. Дело в том, что осуществив каст к указателю на функцию с обратным порядком аргументов, вы получаете UB согласно стандарта языка (
C11, раздел 6.3.2.3, параграф 8A pointer to a function of one type may be converted to a pointer to a function of another
type and back again; the result shall compare equal to the original pointer. If a converted
pointer is used to call a function whose type is not compatible with the referenced type,
the behavior is undefined.Randl
15.08.2016 09:47+4Термин каламбур типизации (англ. type punning) используется в информатике для обозначения различных техник нарушения или обмана системы типов некоторого языка программирования, имеющих эффект, который было бы затруднительно или невозможно обеспечить в рамках формального языка.
mayorovp
15.08.2016 09:49Тем не менее, стандарт не запрещает экспорт функции в одном модуле с последующим импортом в другом модуле с другим порядком аргументов.
GarryC
15.08.2016 10:31+1А мы не должны получить предупреждение? Если пишем поперек стандарта? Или С думает, что мы понимаем, что делаем, раз есть явное приведение?
tyomitch
15.08.2016 11:35«Должны ли?» — в смысле, «требует ли этого стандарт?» (нет, не требует) или «не лучше ли было бы, если бы выдавалось предупреждение?» (да, было бы лучше. Если вы предложите соответствующий патч для clang, его бы наверняка приняли.)
wrfsh
15.08.2016 11:41В стандарте описаны ситуации когда «реализация языка» должна генерировать «диагностические сообщения», а когда нет. По стандарту при использовании explicit casts предупреждение не обязательно.
Но это только из-за explicit каста. Если убрать каст в самом начале статьи и просто взять указатель на функцию то сообщение о несовпадении типов будет.
iyemelyanov
15.08.2016 11:36+1Спасибо за статью! )
Если скомпилировать крайний пример c -O, то результатом other_value будет 0.000000, и это вполне логично в виду оптимизации.
tyomitch
15.08.2016 11:39+1Функция возвращает результат через XMM0. Между двумя функциями ничего не происходит, и в XMM0 остается результат последней функции, который NoOp подхватывает как аргумент и возвращает.
Маленькая поправка: «Функция возвращает результат через XMM0. Между двумя функциями ничего не происходит, и в XMM0 остается результат последней функции. NoOp совсем ничего не делает (потому так и называется), так что это значение остаётся в XMM0, и принимается вызывающей стороной за результат NoOp.»tyomitch
15.08.2016 12:17Добавлю ещё, что объявление
полностью равнозначно приведённому в том примере.void NoOp() {}
fsmoke
15.08.2016 11:42+6Статья интересная, хоть и немного неграмотная, например фразы:
«большинство компьютеров сегодня передают первые несколько аргументов прямо в регистры»
«У Windows одно соглашение....Unix другое соглашение»
«Unix, например, очень агрессивен насчет разбивания структур...Windows немного ленивее»
это явная глупость, у начинающих разработчиков может сложиться впечатление, что передача аргументов может зависеть от ОС или ещё абсурднее от компьютера(я имею ввиду в рамках CISC архитектуры)
Если смотреть с точки зрения низкого уровня: Call инструкция — (грубо) это всего лишь переход с возможностью возврата. Как я буду передавать аргументы — это сугубо моё личное дело, я могу написать свой компилятор С, который будет поддерживать какую нибудь извратную конвенцию вызова и это будет работать и под виндой и под никсами! И никто мне этого не запретит — ни ОС ни тем более процессор, который об этом вообще ничего не знает. Другое дело, что если мне потребуется сделать системный вызов к ОС — я вынужден буду использовать конвенцию навязанную мне разработчиками ОС в рамках конкретного ABI — но это совсем уже другая история.
Т.е. и под никсами и под виндой я могу написать и собрать исполняемый бинарник и какие нибудь библиотеки(dll/so) — между которыми будут происходить вызовы как угодно, хоть через стек, хоть через регистры, через память, вперед, назад, из центра(шутка) — как мне захочется. Тем более если у меня монолитный исполняемый файл(как примеры из статьи) — я вообще могу делать, что захочу с моим кодом. Плюс существуют оптимизации, которые при любом удобном случае скорее всего будут пытаться передать аргументы наиболее оптимально..., но это тоже относится лишь к компилятору.
И ограничен я лишь возможностями компилятора, естественно на свете уже есть ряд общепринятых CC, ещё раз повторюсь — если я в компиляторе поддержу какую нибудь свою конвенцию, никакие ОС меня не остановят и вообще по большому счету не будут знать, что там у меня происходит.
PS
Предлагаю переводчику, сделать ремарки, о том что все вызовы зависят от компилятора, а не от ОС и тем более «компьютеров». Я прекрасно могу иcпользовать ABI никсовых компиляторов в Windows, если это не затрагивает системные вызовы. Но даже если брать вызовы WinApi — они происходят в рамках stdcall конвенции, в отличии от всех остальных вызовов внутри приложения, короче говоря в статье идет смесь понятий CC и ABI, одна и та же СС может в разных ABI выглядеть по разному. И это всё относится лишь к компиляторам…mayorovp
15.08.2016 12:11Но соглашения о вызовах и правда зависят от ОС. Например, соглашение thiscall отличается в linux g++ и mingw g++.
fsmoke
15.08.2016 12:21не знаю зачем это было сделано, возможно thiscall отличается специально, для поддержки каких нибудь COM интерфейсов Windows или ещё чего нибудь, чтобы было проще, хотя врать не буду — это просто предположение
mayorovp
15.08.2016 12:27Я тоже не знаю зачем это сделано. Но факт остается фактом — соглашения о вызовах зависят от ОС.
fsmoke
15.08.2016 12:36Да не от ОС это зависит… Вы хотите сказать, что если я сейчас возьму исходники mingw, поправлю их, чтобы thiscall соответствовал никсовому gcc — у меня перестанет работать какое нибудь консольное приложение, которое особо ничего не дергает? Не будет такого.
Вернее сказать, конечно ОС стала предпосылкой каких-то изменений — но слово «зависит» тут явно не подходит.mayorovp
15.08.2016 12:43Работать-то ничего не перестанет — но собранное новым компилятором консольное приложение перестанет быть бинарно совместимым со старым.
tyomitch
15.08.2016 12:12+1Вы мыслите, как computer scientist (сферический компилятор в вакууме может передавать параметры как угодно), а автор статьи — как software engineer (компиляторы, практически применяемые в софтописании, передают параметры вполне определённым образом).
Т.е. у автора, действительно, есть небрежности в формулировках, но на смысл текста они не влияют вообще никак.
Анекдот в тему:Едут по Австралии биолог, физик и математик, и из окна видят на лугу черную овцу.
Биолог: --Смотрите, в Австралии обитают черные овцы.
Физик: --Нет, мы можем сказать лишь то, что в Австралии обитает как минимум одна черная овца.
Математик: --Нет, господа. Мы можем сказать лишь то, что в Австралии обитает как минимум одна овца, чёрная по крайней мере с одной стороны.fsmoke
15.08.2016 12:28В целом согласен, но фраза «вы можете изменить порядок аргументов функции в C» имхо слишком небрежна. Автор порой выдает UB за какие-то «фичи». Хотя эти самые фичи — и являются сферическим чем-то там, т.к. работают только в определенном компиляторе, определенной версии, под определенную разрядность определенной архитектуры.
tyomitch
15.08.2016 13:02+1То, что с точки зрения стандарта является UB, очень часто с точки зрения компилятора является фичей этого компилятора.
Автор называет «фичи наиболее распространённых компиляторов Си» просто «фичами Си», потому что для 99% программистов на Си разницы между этими формулировками нет: стандарт Си они никогда не читали, и незачем им его читать. Для них язык Си и компилятор Си — одно и то же самое.
Randl
15.08.2016 13:04Ну естественно, соглашение это часть ABI, если вам не нужна бинарная совместимость ни с чем, можно делать что угодно.
dkv
15.08.2016 14:19+3Век живи, век учись эффективно стрелять себе в ногу с рикошетом в голову. Спасибо за статью.
ChachaB
15.08.2016 19:16+2Хм, при чем тут С? Похоже автор путает термины компилятор и язык.
pc:/tmp> gcc -O2 -o a something.c -lm pc:/tmp> ./a (0.99)^100: 0.366032 (0.99)^100: inf
Что я делаю не так? :)Randl
16.08.2016 05:02Без листинга ассемблера это не определить. Впрочем, лично я (как и большинство здесь) даже с листингом не уверен что разберусь.
Sliver
17.08.2016 15:58+1Эта "загадка" может быть поводом к тому, чтобы заинтересовать людей покопаться в том, как работает компилятор.
Но вот переход к разделу "как это применять на практике" меня просто убил.
Ещё и с примерами! (facepalm)
iCpu
Изящный способ отстрелить себе
головуногу по самую голову.Спасибо!