Pull to refresh

Comments 61

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

Так нет же, деструктор по умолчанию noexcept, если вылетает исключение, то terminate. Если поставил явно noexcept(false), то всё ещё никакого уб нет, откуда бы ему взяться? Ну вылетит исключение вот и всё. Если во время анвиндига, то опять же в стандарте написано, что вызывается terminate

https://godbolt.org/z/vxPaWdzcb

Спасибо за комментарий. Да деструктор по умолчанию noexcept, как я описал в своей статье.

Ваш пример кода на gobolt это UB код

Подробнее здесь

Это потому что не смотря на то, что вы за счет noexcept(false) может развернуть стек и даже обработать исключение в некоторых случаях (которые, кстати, указаны в статье). Это не значит что он будет работать корректно во всех случаях. Если например вы будете дорабатывать такой код и у вас между созданием и уничтожение такого экземпляра появится какой-то вызов функции, который в теории может бросить исключение ваш код может перестать работать, и вообще может случиться что угодно.

Приведу пример.

Вот код https://godbolt.org/z/hnqTaE7jG. Здесь мы создаем два экземпляра двух типов. Из деструктора B вылетает исключение и он успешно разворачивает стек и ловится (за счет noexcept(false)).

Потом пришел другой программист и начал дорабатывать бизнес логику продукта в этом UB коде и добавил вызов функции после создания экземпляра B. И он это сделал абсолютно легально. Его функция в некоторых случаях может бросать исключение, что также абсолютно легально. Он написал полностью корректный C++ код.

Запустите этот доработанный код https://godbolt.org/z/3YavK5Eah и вы увидите, что он перестал работать. Вряд ли другой программист, мог знать, что Вы кидаете исключение из деструктора. И здесь прав он, его исключение должно было быть перехвачено по правилам языка C++ и обработано. Но вместо этого деструктор A не был вызван, а процесс упал.

Естественно это UB, я не могу дорабатывать и вообще никто не может дорабатывать такой код, в котором оставлены такие мины. При том такие мины бахнут на продакшене уже, в каких-то рандомных кейсах. Это поведение невозможно предсказать. Оно на данном примере ведет себя по разному как мы можем видеть, даже в рамках одного компилятора (то экспешн из деструктора поймался, то не поймался).

Нет, это НЕ уб, в такой ситуации по стандарту поведение четко определенно - вызов терминэйт.

Вы пишете, что не существует случаев когда из деструктора имеет смысл кидать исключения - вы слишком высокого о себе мнения и берете на себя больше чем можете вынести (на данный момент). По теме исключений вам конечно незачет, приходите с другой статьей на пересдачу. Хинт гуглите std::uncaught_exceptions(), эс на конце очень важна, можете ещё в строку поиска добавить Александреску.

Задание на следующую статью - напишите класс который может кинуть исключение из деструктора и НИКОГДА не привидет этим к вызову терминэйт.

Вопрос со зведочкой - зачем это может понадобиться в реальном коде. Одного примера достаточно.

напишите класс который может кинуть исключение из деструктора и НИКОГДА не привидет этим к вызову терминэйт.

Кинуть из деструктора - имеется в виду, что исключение выходит за пределы деструктора?

Вставлю свои 5 копеек.

Назовем "корректное поведение", в стандарте написано "в случае stack unwinding если выбрасывается исключение - вызывается terminate" - все знают что так и будет и оно так и будет.

Implementation defined - компилятор A говорит что в упомянутом случае вызовется какой кастомный abort(), компилятор B говорит что что диск будет отформатирован.

Unspecified behavior - по сути предыдущее, но не документрованно и фактически observable behavior, но не приводит к UB (см порядок эвалюации аргументов функции).

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

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

говорит что что диск будет отформатирован

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

Зачем вы транслируете этот форс абуза UB? Да и не только вы.

Если компилятор при UB ломает программу или делает явно нелогичное поведение это баг/ошибка (как было с дебильной ситуацией с циклом у clang который растранслировался в "мем"), а если он при этом ещё и занимается явным вредительством доводя концепцию UB до абсурда это саботаж.

Дополню: UB это далеко не всегда ошибка, как например чтение за границей массива, а вполне возможно законная оптимизация.

Зачем вы транслируете этот форс абуза UB? Да и не только вы.

Потому-что UB это буквально "неопределенное" поведение - оно по определению ничем не определено. Если у вас кастомная ОС без пейджинга/защиты и разрешает вам лезть в любую память вы можете записывая вне границ массива иногда ничего не получать, иногда переписывать другие процессы, иногда переписывать ОС с реальной возможностью когда-нибудь таки отформатировать диск.

Если компилятор при UB ломает программу

Компилятор оптимизирует код на основании того что UB в программе нет, он может реогранизовывать код исходя из этого, может выкидывать куски когда исходя и этого ("а зачем тут нужна эта проверка? она может пригодиться только если выше будет UB, его выше быть не может - выкидываем проверку"). Это не проблема компилятора что человек пишет не валидный код. Некоторые компиляторы некоторые UB могут апнуть до Unspecified Behaviour, но это их личное дело и полагаться на это глупо, так как в след версии все может поменяться.

UB это далеко не всегда ошибка, как например чтение за границей массива

Не очень если честно представляю как это будет оптимизацией учитывая что даже если в 99.9% случаев все будет хорошо и "более оптимально" в остальных случаях процесс может полезть в чужие пэйджи и закономерно получить по рукам от ОС, которые видя такие выкрутасы просто его прибьет долго не разбираясь.

Да, в реальной жизни почти нереально найти серьезную программу без UB, тот же Strict Aliasing нарушают все кому не лень, но факт остается фактом - UB надо избегать.

Ну я могу рассказать о такой оптимизации, которая формально УБ, но на практике всегда ок. Расклад такой, у вас есть большой массив структур, ну например массив ргб цветов по байту на цвет. И так вы можете все это хранить либо в интах и просерать 25% памяти или хранить цвета в 3 байтах. Так а где же здесь УБ? Ну оно возникает когда вам надо все это обрабатывать используя симд. В симде у вас будет 0 в четвертом компоненте, а читать вы будете из памяти по началу по байтово. А потом поймёте что не выровненное чтение быстрее чем побоайтовая загрузка и таки начнёте читать 3х компонентные структуры как будто они 4х компонентные. Вот тут у вас формально УБ, но на практике всегда ок. А что бы не читать за пределами пеэйджа вы алоцируете немного больше памяти под весь массив что бы вот этот фэйковый 4 компонент не вылез за границы буфера. Я тут мог что то не точно описать про 1 байт, но суть более менее передал, а смотреть код мне в лом.

В своей "бесконечной гениальности" создатели стандарта пытались учесть все возможные случаи. В т.ч. и случаи машин, которые не умеют читать невыровненно. А страдают потом все, ещё и компиляторы подгаживают :)

Зачем вы транслируете этот форс абуза UB? Да и не только вы.

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

Вставлю свои 2 копейки

  1. restrict - единственное ключевое слово из C, которое не поддерживается в C++

  2. Есть заголовок <setjmp.h>, который (добавляет функции, возволяющие) позволяет делать нелокальные переходы. Т.е. можно вернуться вверх по стеку через несколько функций без явного return. В C++ она хоть есть, но если использовать, то можно забыть о всех деструкторах - они вызваны не будут.

Большое спасибо, за полезный комментарий! Сразу вспомнился API libpng, в частности то как там обрабатываются ошибки с setjmp

Странно говорить о каких-то свойствах языков и не упоминать версии их стандартов. Я уверен, что ни _Bool, ни _Complex ни _Generic из C11 никакая из версий c++ тоже не поддерживает.

С ключевым словами - неверно. В С есть _Bool, _Complex, _Imaginary и т.д.

Что касается longjmpто различие между С и С++ тут не настолько сильны, как может показаться. Во-первых, в С менеджмент ресурсов делается рукописным эпилог-кодом. И он точно так же "не выполнится" при выходе из функции по longjmp. Во-вторых, в С уже тоже появились [опциональные] неявные конструкторы и деструкторы - это внутренний код, обслуживающий VLA. Именно поэтому описанию longjmp в совокупности с VLA уделено отдельное внимание в стандарте языка.

В C и C++ есть особенности, о которых вас вряд ли спросят на собеседовании (вернее, не спросили бы до этого момента). Почему не спросят? Потому что такие аспекты имеют мало практического значения в повседневной работе или попросту малоизвестны.

Здрасьте, varargs не спросят даже на позицию по компиляторам или software stack в процессорные компании? Да в MIPS Technologies сидел специальный человек, которому зарплату платили за то, что он был экспертом по calling conventions. Наверняка такие же есть и в ARM, Intel, SiFive и других таких компаниях.

Вы абсолютно правы в своих суждениях.

Но я никого не хотел раздражать таким введением или устанавливать правила проведения собеседований.

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

под Windows компилятор MSVC считает, что char — это unsigned char

А разве оно не определяется параметрвми компилятора? Не могу сказать уверенно про MSVC (хотя память упорно подсказывает, что такой параметр есть или был когда-то), а в других компилятораз такое точно было.

так для архитектуры x64 ясное дело __stdcall и __cdecl будут проигнорированы.

Не совсем так. Наличие или отсутствие cdecl в объявлении экспортируемой функции даже для архитектуры x64 как минимум указывает на особенности ее именования в разделяемой библиотеке (dll или so). В случае cdecl это будет имя функции, иначе к имени функции будут добавлены символы, кодирующие типы параметров и возвращаемого значения (причем для разных компиляторов этот код может отличвться, что может приводить к интересным ситуациям при попытке, например, подключить dll функциями LoadLibrary и GetProcAddress).

>> под Windows компилятор MSVC считает, что char — это unsigned char

Определённо автор что-то путает. В MSVC signed char (знаковый), постоянно приходится это учитывать при операциях сравнения и сдвига.

chanav, Вы правы, в моей статье ошибка, большое спасибо за данный комментарий!

geher, большое спасибо за комментарий про char

Можете уточнить про имена символов или, если есть такая возможность, привести дизасм C кода сбилженого под архитектуру x64 с разными колконвеншенами функций: cdecl, stdcall, fastcall и т.д., где будет видно что имена символов изменились?

Не совсем так. Наличие или отсутствие cdecl в объявлении экспортируемой функции даже для архитектуры x64 как минимум указывает на особенности ее именования в разделяемой библиотеке (dll или so).

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

Мои обоснования такие

Потому что в имени как раз и декорируется информация о колконвешене (т.н. декорирование имен). Чтобы во время связывания кода линковщик знал как вызывать функцию и передавать в нее аргументы, а также кто должен сместить адрес стека.

Например линковщик видит символ с таким именем:

@AddSubFc@16

И он понимает из названия (по символу @), что речь идет о конвенции fastcall:

extern "C" int _fastcall AddSubFc(int i1, int i2, int i3, int i4);

Или например видит такой символ:

_AddInts@8

Линковщик понимает что речь идет о такой сигнатуре stdcall:

extern "C" int _stdcall AddInts(int i1, int i2);

Это примеры для MSVC x86.

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

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

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

Сейчас попробовал в Qt, подвернувшемся под руку (mingw 64 бит 14.1).

Виноват, был не прав. cdecl ничего не меняет, как дорисовывал к имени символы, так и дорисовывает. Похоже бес (какой-то очень древний левый компилятор, в котором оно таки работало именно так) попутал.

Так-то описанное мной поведение (выключение декорирования имени) по человечески выполняется через определение extern "C" (которое всегда использовал) или def файл (там можно вообще любое имя нарисовать), но в памяти почему-то сидела и неправильная альтернатива, которую очень давно не пробовал (ну не было никогда необходимости объявлять явно cdecl в программах на С и С++).

Для меня, как исключительно С++ разработчика, тыжсишник==тыжпрограммист, Си и С++ - это разные языки с различными парадигмами и подходами к разработке. Я не знаю Си, о чем открыто заявляю, когда на меня пытаются повесить подобные проекты. Разумеется я его знаю, на каком-то уровне, но не смогу на нем так же продуктивно работать. Т.е. С++ основной язык разработки, а python, Си, php и пр. примерно в одном ряду, как бы парадоксально это ни казалось.

А что на счет строковых литералов, например "string literal"? Это будет char*

Тут стоит заострить внимание на то, что именно char *, а не char const *. Почему это важно?
Случай из практики (писали мы на C тогда). Была у нас функция, которая принимала на вход строку char * и изменяла ее. Долго она у нас была, пока однажды мы не обнаружили место, где в функцию передается... строковый литерал. Функция бодро писала в литерал и всё работало, потому что в данном случае изменения заключались в удалении пробелов (так называемый trimming). То есть за пределы литерала ничего не писалось.
Но это открытие нас так напугало (мы то были абсолютно уверены, что литералы конечно же char const *, ну кому в здравом уме может прийти в голову сделать литералы изменяемыми?), что мы добавили в наш кодстайл обязательное требование все строковые литералы приводить к char const *

В C++ как раз типом будет char const [N] как и ожидается.

Пишите на C++ 😀

Что-то я уже начинаю путаться чем отличается const char * от char const *. Первое постоянно использую на C++ использовал в стиле (const char)*, т.е. указатель на неизменяемый char, а второе как-то не замечал. Надо мне перекурить параграф "приоритеты и порядок выполнения операций".

Нет разницы

char const* == const char*

Указатель на константный символ

Адрес менять можно содержимое нельзя

char * const

Тут уже будет константный указатель на символ

Адрес менять нельзя , а содержимое можно.

Чаще всего используют const char* называя это константный указателем, что ещё больше вносит путаницы.

Это очередное тяжёлое наследие C. Типы читаются справа налево, но для const сделали исключение. Из-за этого все путаются и ничего не понимают.

https://mariusbancila.ro/blog/2018/11/23/join-the-east-const-revolution/

Почему исключение ? Всё читается справа налево в каноническом виде.

Это как раз вариант где const можно ставить слева не является каноническим.

Поэтому некоторые стандарты кодирования предлагают всегда ставить const справа.

Я ровно это и сказал. Для const сделали исключение - в определенной ситуации const допускается писать слева. Могу только предположить, что авторам языка не понравилось, что const получился не в начале строки, и они влепили этот страшный костыль вместо того, чтобы пересмотреть сам дизайн.
В итоге редко кто знает, что типы читаются справа налево, потому что вот же, контрпример.
Я одно время сидел на StackOverflow. Там такие вопросы прилетали почти каждый день. И каждый раз ответом был некорректное правило spiral rule https://c-faq.com/decl/spiral.anderson.html. За год ежедневного сидения не видел ни разу, чтобы ответили правильно и показали right-left rule https://cseweb.ucsd.edu/~ricko/rt_lt.rule.html. Всё, что нужно знать о дизайне языка и об общем уровне его понимания

Наследие или нет, это особенность грамматики С и С++. Грамматика (объявления) в С и С++ упрощенно такова

<спецификатор-типа> <декларатор>, <декларатор>, ..., <декларатор>;

В С и С++ самый левый const возле самого типа являются частью спецификатор-типа, а не частью индивидуального декларатор (сравните с *, которая наоборот, является частью индивидуального декларатор, а не спецификатор-типа).

Именно по этой причине грамотные программисты предпочитают выравнивать звездочки вправо, а cv-спецификаторы - влево. Более того, чтобы подчеркнуть особую грамматическую роль этого самого левого constего предпочитают писать слева от имени типа.

Правильно:

const int *const *a;

Неправильно:

int const *const *a;

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

В конечном итоге, если вам на уровне персональных предпочтений нравится выравнивать const вправо - ваше право. Но не пытайтесь подвести под это какое-то теоретическое обоснование. Все теоретические обоснования сразу же ведут к тому, что правильнее писать именно const int *const *a;

Чем const возле типа астрономически отличается от const возле указателя по своей сути? Кажется, по своей сути они не отличаются, потому что указатель это и есть тип.

char * a; - тип этой переменной - указатель
int * a; - тип этой переменной - указатель
void * a; - тип этой переменной - указатель

char и int перед указателем лишь указание на то, что будет, если инкрементировать или разыменовать указатель, они ничего не говорят о том, что хранится в самой переменной. А void * это просто указатель.

Что касается грамматики с разделением типа на две части, то это очередная иллюстрация тяжелого наследия.
Полагаю, сделано это было ради таких страшных объявлений:

int a, b, *c, d[5];

Чем const возле типа астрономически отличается от const возле указателя по своей сути?

Мой комментарий выше содержит ответ на этот вопрос.

Именно по этой причине грамотные программисты предпочитают выравнивать звездочки вправо, а cv-спецификаторы - влево.

А другие не менее грамотные программисты предпочитают east-const.
ИМХО: читая код сверху вниз гораздо приятнее прыгать по типам прямо, а не зигзагом. Наличие const при этом имеет второстепенное значение.

Неверно.
Строковые литералы и в С и в С++ являются массивами и имеют тип char [N] в языке С и const char [N] в языке С++. Независимо от типа, строковые литералы являются немодифицируемыми объектами и в С, и в С++.

строковые литералы являются немодифицируемыми объектами и в С, и в С++

Для С — не совсем так. В Стандарте ситуация с модификацией строкового литерала относится к UB, то есть отдана на откуп авторам конкретного компилятора. У того же LLVM clang опция -fwriteble-strings приводит к тому, что литералы хранятся в rw-секции.

До тех пор, пока мы остаемся в рамках языка С, формально "ситуация отдана на откуп авторам конкретного компилятора" - это "implementation-defined behavior". А попытка модификации строкового литерала - это undefined behavior. Это сильно разные вещи.

Конкретная реализация имеет право реализовывать расширения языка, в том числе конкретная реализация имеет право определять поведение в случае undefined behavior. Но это уже выходит за рамки языка С и никакого отношения к С не имеет.

Почти согласен, здесь есть пространство для терминологической дискуссии. Слово “undefined” характеризует не “behavior”, а позицию стандарта. Это не “неопределённое поведение”, а “стандартом не предписывается, как должен поступить компилятор, и как будет вести себя полученная программа в перечисленных случаях”. Но если с такими случаями, описанными UB в стандарте, как деление на 0 или разыменовывание указателя на объект, время жизни которого прошло (иначе говоря — которые невозможно выявить при компиляции) всё предельно ясно — поведение определяется средой исполнения, то с теми случаями, которые тоже UB по стандарту, но они совершенно очевидны ещё на разных этапах компиляции/связывания, всё несколько иначе. Авторы компилятора всё равно же реализуют то или иное поведение своего детища. Например, функция main объявлена не соответствующим ни одному из предусмотренных стандартом вариантов. Авторы компилятора вольны поступать в этом случае как им заблагорассудится — могут сообщить об ошибке декларации и прекратить компиляцию, могут выдать предупреждение и продолжить компиляцию, могут молча компилировать себе дальше. UB превратилось из-за этого IDB? Нет, конечно. Потому, что — возвращаемся выше — UB не есть неопределённое поведение (поведение программы всегда определено её автором) UB есть поведение, которое стандартом никак не регламентируется.

И теперь с этой же позиции присмотримся к термину implemetation-defined behavior: это не поведение, определённое реализацией (повторюсь, если не считать случаев, в которых поведение определяется средой исполнения, все остальные определено стараниями программиста). Это поведение, которое стандарт предписывает авторам компилятора документировать. А к случаям UB такого требования в стандарте нет. Вот и вся, в сущности, разница.

В двоичном интерфейсе приложений (ABI) x64 по умолчанию используется четырехрегистровое соглашение о вызове, т. н. x64 calling convention

Могу ошибаться, но это справедливо только для windows. В linux x86_64 ABI передаёт через регистры 8 целочисленных аргументов (остальные через стек).

Верно, это зависит от платформы.

Я ввел в заблуждение, когда обозначил, что в x64 calling convention есть строго-оговорённое количество используемых регистров. Я доработаю этот абзац, чтобы он соответствовал действительности

Какой ужос. Это ж сколько всяких особенностей и UB нужно, по-хорошему, держать в голове помимо прочего. Хорошо что я не пишу на Си давно уже :)

Да почти никто в голове такого не держит конечно, если наткнешься на подобное в процессе - раскопаешь уже что не так. Просто как правило эти особенности никто в коде использовать не будет.

То, что в статье - скорее всего да. Но в плюсах кроме этого ещё куча всяких UB, которые позволяют легко выстрелить себе в ногу.

Я на плюсах мало писал, но даже банальная арифметика между int-ами разной разрядности часто приводила к багам в тех проектах, с которыми имел дело. В новых языках типа Rust/Go это явно запрещено - хочешь? кастуй тип.

Я бы сказал, что такие особенности нужно держать в голове в одном случае - вы проходите собеседование. Больше оно почти никогда не пригодится

Комменты - отличная иллюстрация что никто полноценно C и C++ не знает. У всех знания фрагментальны. Поэтому типовой собес это всего-лишь определение насколько фрагментальные знания собеседующего пересекаются с фрагментальными знаниями кандидата. Но какая в этом ценность? Гораздо ценнее кооперирующаяся команда с непересекающимися знаниями её участников.

А все знать и не нужно. Важно уметь пользоваться тем, что знаешь наверняка. Документацию никто не отменял, наряду с классикой она д.б. настольной книгой. Программы нужно уметь читать, это главное. Разумеется речь о качественной продукции, исходные тексты библиотек, фреймворков.

Все люди в мире свято верят в незыблемый принцип: "Никогда не обобщайте и не говорите за других". Не надо проецировать свои фрустрации, вызванные фрагментальностью ваших знаний, на всех.

// якобы здесь утечка памяти, т. к. по правилам RAII во время уничтожения
// Class, в деструкторе может возникнуть исключение; а т. к.

Дык утечка при бросании исключения будет если в отнаследованном классе в конструкторе бросать исключения. Т.к. происходит конструирование снизу вверх сконструируется родительский класс, потом попытается сконструироваться, потомок не конструируется в итоге выделенная память родителя остается. Ну и выделять все это дело надо не на стеке, т.к. RAII скорее всего всё почистит, включая родителя.

void f(...) {} добавили только в C23.

Верно. Дополню, что суть замечания в том, что до C23 все variadic функции в С должны были иметь хотя бы один не-variadic параметр.

svlasov, спасибо, уточню это в статье

Уже с самого начала - дичайшее количесто ошибок в статье. Просто ошибка на ошибке.

Потому что объявление void f() является эллипсисом в языке C (ellipsis notation; их также часто называют как variadic arguments

Что??? Эллипсис в языке С - это именно ..., то есть действительно variadic arguments. Но объявление c ()никакого отношения к ellipsis или variadic arguments не имеет и никогда не имело. Это т.наз. объявление без прототипа, которое не указывает список параметров. Я не буду расписывать подробно, но замечу, что такое объявление не совместимо с ellipsis-определением. Функции с ellipsis-определением в С, то есть f(...), обязаны быть объявлены заранее именно с ..., иначе при вызове такой функции поведение не определено. Это одно из мест "классического" стандартного С, в котором требуется ранее (до первого вызова) объявление функции.

Т. е. следующий код абсолютно корректен и компилируется любым С-компилятором:

void f() { printf("f()"); }

int main() {
f(5, 3.2f, "test");

Да, такой код является формально корректным из-за того, что функция объявлена без прототипа. Однако поведение такого кода не определено. Вызов функции без прототипа эквивалентен вызову вообще не объявленной функции (в С89/90 такое разрешалось). При вызове функции без прототипа компилятор С обязан "придумать" прототип функции на основе переданных аргументов. В данном случае это будет f(int, double, char *)(именно double). Если при этом оказывается, что "придуманный" прототип не совпадает с фактическим определением функции, то проведение не определено. Именно это происходит и в вашем случае - проведение не определено.

Это "абсолютно корректен" или нет?

Чтобы указать компилятору языка C, что функция не принимает аргументов, нужно указать это явно с помощью аргумента void:

Ключевым различием здесь, которую я уже упомянул выше, является "объявление с прототипом" против "объявления без прототипа". Объявление с (void) - это уже объявление с прототипом. Кстати, объявления без прототипа давно являются deprecated и в С23 будут, наконец, формально запрещены.

Итак, подытожим в виде таблицы:

Таблица повторяет вышеуказанную бредятину о том, что () - это якобы "эллипсис". Это грубейше неверно.

Кстати, обратите внимание, в языках C/C++ аргументы часто передаются справа налево по C-декларации, это как раз нужно для того, чтобы работали такие функции как printf(). Чтобы на вершине стека был параметр, по которому мы сможем определить сколько данных лежит еще на стеке.

Очередной заряд полнейшей чуши... Функции printf()ничего подобного не нужно. Никакой передачи параметров "справа налево" не существует. Это не говоря уже о том, что современные ABI позволяют передавать даже variadic аргументы в регистрах процессора.

Историческая справка: до появления стандарта ANSI C, был и альтернативный стиль объявления аргументов функций, т. н. стиль Кернигана и Ричи (стиль K&R). Пример кода (нужно компилировать C компилятором):

Во-первых, то, о чем вы писали выше, это и есть "альтернативный стиль K&R". Всякий раз , когда в языке С вы пишете ()в объявлении или определении функции - это объявление без прототипа, то есть тот самый "альтернативный стиль K&R" - один из его частных случаев. Чтобы уйти от "альтернативного стиля K&R" для функции без параметров мы и пишем (void)

Во-вторых, не "до появления стандарта ANSI C", а и в стандарте ANSI С тоже. Стандарт ANSI С разрешает K&R объявления, хотя там они давно являются deprecated. Стандарт С89/90 разрешал вызов функций вообще без объявления. С99 запретил вызов без объявления, оставив однако возможность делать объявления без прототипа. Только C23 запрещает объявления без прототипа и K&R-стиль в целом. При этом хотя объявления функций c ()являются K&R объявлениями, в С23 они запрещены не будут - их "сконвертировали" в эквивалент (void), то есть все станет в точности как как в С++.

При передаче аргументов в variadic function применяются следующие правила неявных приведений типов аргументов (правило default argument promotions): 

Во-первых, default argument promotions состоят из integer promotions плюс преобразование float в double. Вы развернули описание, но у вас однако integer promotions описаны не совсем корректно. Ну да ладно.

Во-вторых, default argument promotions делаются при передаче не только variadic аргументов, но и при вызове функций без прототипа (именно поэтому в примере выше я указал, что передача аргумента 3.2fприводит к "придуманному" прототипу с параметром double). Вы это все, конечно, попытались запихать в свою теорию "это всё эллипсис", но это - полная чушь.

Дело в то, что существуют различные т. н. calling convensions (соглашения о вызовах функций)

Никакого отношения к языкам С и С++ это, разумеется, не имеет. Никаких calling conventions ни в С, ни в С++ нет. Что это делает в статье про "особенности С и С++" - не ясно.

А что на счет строковых литералов, например "string literal"? Это будет char*

Грубейше не верно. Строковый литерал - это массив, то есть char[N] в языке С и const char[N] в C++.

Ответ очень прост: charsigned char и unsigned char — это три разных типа. Но при этом стандарт не запрещает, чтобы char был псевдонимом либо signed char, либо unsigned char.

"Псевдонимами" в терминологии С и С++ называют идентичные типы, то есть одни и те же типы под разными именами. Так как charsigned char и unsigned charкак вы правильно заметили - это три разных типа, никаких "псевдонимов" тут нет и быть не может. О каком "стандарт не запрещает" вы ведете речь - не ясно.

Далее читать не стал...

да, закончил чтение на том, что объявления без прототипов назвали эллипсисами. Казалось бы, что мешало проконсультироваться с тем же cppreference перед написанием статьи? Не говоря уже просто о знании предмета.

Кстати, объявления без прототипа давно являются deprecated и в С23 будут, наконец, формально запрещены.

больше того, в новых версиях компиляторов вроде как задействовали -Wstrict-prototypes (то есть из коробки кидается ворнинг в таких местах)

lrrr11 Правильно что не стали читать, не надо было и этот комментарий писать

Далее:

Дело в том, что в C тип символьного литерала имеет тип int, а не char. Поэтому такие функции стандартной библиотеки языка C как char *strchr( const char *str, int ch ) принимают int, а не char в аргументе ch.

Это - совершенно не верное объяснение. Тип символьной константы здесь совершенно ни при чем.

Причина, по которой стандартные функции принимают тип int заключается в том, что в старинном K&R С у функций не было прототипов. Функции либо объявлялись как (), либо вызывались без объявления вообще. Все это разрешалось и в первом стандарте С - С89/90. При вызовах таких функций передаваемые аргументы всегда безусловно подвергались default argument promotions, о которых вы сами уже упоминали. В процессе default argument promotions тип char превращается в тип int и в функцию передается уже именно int. Вот именно поэтому у "классических" стандартных функций вы никогда не увидите параметров типа char, а вместо них будуь параметры типа int.


По этой же самой причине вы никогда не увидите у "классических" стандартных функций параметров типа short или float. Другими словами, в K&R C вообще не было возможности передать параметры типа char, short или float. Эти типы передавались как int, int и double соответственно.

А ваши домыслы про влияние типа символьного литерала тут совершенно ни при чем.


TheCalligrapher

Это - совершенно не верное объяснение. Тип символьной константы здесь совершенно ни при чем.

Большое спасибо, за объяснение, как будет время, внесу необходимые правки

TheCalligrapher большое спасибо за такой развернутый комментарий

Лучше такой объем опускайте под спойлер, чтобы другим читателям было удобнее читать комментарии.

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

ABI позволяют передавать даже variadic аргументы в регистрах процессора.

что в статье и сказано, читайте статью внимательнее, но я согласен, что возможно стоило выразить это в более явной форме

Во-первых, то, о чем вы писали выше, это и есть "альтернативный стиль K&R"

спасибо, это я тоже поправлю, чтобы это было явно указано

Вы это все, конечно, попытались запихать в свою теорию "это всё эллипсис", но это - полная чушь.

Нет, не пытался. Но если Вы меня так поняли, значит я плохо написал

Никакого отношения к языкам С и С++ это, разумеется, не имеет. Никаких calling conventions ни в С, ни в С++ нет. Что это делает в статье про "особенности С и С++" - не ясно.

Полностью согласен, я это писал только для того чтобы дизасм объяснить для printf. Конкретно почему там не через стек толкаются параметры. А то у кого-то могли возникнуть вопросы. Но этот текст нужно либо вообще удалить из статьи, либо опустить под спойлер.

Грубейше не верно. Строковый литерал - это массив, то есть char[N] в языке С и const char[N] в C++

Что в статье и написано, читайте внимательно.

Спасибо за статью!
Простите за оффтоп, но есть небольшой вопрос. Камрады, подскажите, есть ли какие-то более-менее централизованные базы/опиcания/списки/дайджесты неплохих библиотек на С++ (и тот же вопрос про Qt)? Знаю вот такой ресурс, а есть ли что-то еще?

UFO just landed and posted this here

Не знаю, малоизвестный факт или нет, но в функции/методе с типом результата void можно возвращать результат типа void:

#include <iostream>

void print (const string_view &message) { std::out << message << std::endl; }

void printMessage () { return print ("Hello World"); }

int main (int argc, char** argv) { printMessage(); return 0; }

Эта возможность особенно полезна в шаблонах, но работает и без шаблонов.

Sign up to leave a comment.

Articles