Это я уже давно всё понял — у нас просто сильно разные задачи.
3) Динамическое включение отладки — ненамного дороже включения при помощи дефайнов, зато позволяет прямо в момент ошибки включить отладку и посмотреть, что происходит.
Отладочный вывод — это, несомненно, хорошо. Но для вычислительных задач с детерминированными входными данными без случайных внешних событий это попросту не нужно. Это будет либо убийство производительности (проверка условия на логирование на каждом чихе), либо низкая информативность. Все равно что на JavaScript программировать серьёзные вещи (гусары с nodejs — молчите).
Поэтому для задачи обработки изображений я предпочитаю статическую настройку параметров отладки. Ну а для серверного кода — да, логирование со всеми уровнями важности, по-другому никак.
А выравнивание зависело от той переменной, которую gcc превращал в константу.
Для таких целей обычно используют alignas, а не делают паддинг переменными, делая код крайне непереносимым и зависимым от конкретного компилятора и его настроек.
Это не UB, это просто БАГ — порча памяти при записи или мусор при чтении.
ОШИБАЕТЕСЬ. Вместо самой vTable в объекте указатель на неё. Это вы книги для чайников читали, там действительно иногда так рассказывают.
Видимо, надо действительно быть чайником, чтобы нормально объяснять :)
Для меня настолько очевидно, что [vtable] — это указатель на таблицу, а не сама таблица, что по-другому быть и не может.
C этим почти согласен. Там две ссылки.
Именно это и я хотел объяснить. Есть поля во втором объекте — придётся вставлять вторую ссылку на таблицу, тратя память на каждый объект. Нет полей — память тратить не нужно.
4 assert на крайние точки. Исполняются однократно. Остальное — математикой.
Что мешает комбинировать оба подхода: debug-режим — все проверки, в том числе и внутренние, включены, release — включены только проверки входных параметров (например, размеры изображений, выравнивание).
Не очень понимаю, как математика защищает от программистских ошибок.
Цена остановки стана — 40 тысяч долларов (рулон стали) улетает в брак.…
Вот один раз в такой ситуации побудете — отучитесь писать код с багами. :-)
Опять же: вопрос исключительно в цене ошибки. Задачи разные бывают.
В моём случае разработки носят преимущественно теоретический характер. Попробовал метод, применил — не понравилось, закодил следующий.
Задачи в области обработки медицинских изображений, где цена ошибки уже высока, до реальной практики так и не дошли — нет заинтересованных заказчиков.
было bool fatsMode = false; Потом в одной из веток fatsMode = true; Когда закомментарили эту ветку, компилятор перестал выделять память под fatsMode. Ну и все выделение памяти — поехало.
А можете пояснить, каким образом это могло повлиять на работу программы? Желательно конкретным примером.
Например, если вы принимаете на вход функции &int32 и преобразовываете внутри функции в &int64, то будьте готовы к тому, что это UB со всеми вытекающими.
А ещё есть такая подлая штука в оптимизаторах, как strict type aliasing, например.
Указатель содержит адрес объекта, передаваемого функции.
Вот структура типичного объекта в памяти:
[vtable][fields]
Адрес этой структуры и передаётся в функции.
Вот структура объекта с множественным наследованием:
[vtable1][fields1][vtable2][fields2]
Объединить два vtable в один:
[vtable12][fields1][fields2]
невозможно, т.к. непонятно, как взять адрес от второго объекта.
С другой стороны, если у объекта нет полей, то указатель будет указывать на объект, содержащий только vtable. Соответственно, нет нужды размещать vtable в каждом объекте, когда можно сделать его глобальным.
ну чайники ещё и не такие ошибки делают. Чтобы разница была существенной, нужно воткнуть assert внутрь часто исполняемого цикла. Или внутри assert вызвать что-то трудоемкое. Для junior простительно, для midle уже баг.
Простой пример: обработка изображений, а именно проверка на выход за границы изображения при доступе к пикселю. Это очень часто встречающаяся ошибка, поэтому приходится втыкать assert внутрь GetPixel.
Но для хорошо отлаженных алгоритмов: свёртка, сумма, фурье там всякие — эта проверка избыточна и должна быть отключена. А вот для новых алгоритмов — включена.
Вы на машине так же ездите? Пока не попали в аварию — тормоза отключены?
Только это авария с нулевыми потерями — как в играх, а не реальной жизни.
И сколько времени вам потребуется, чтобы найти ошибку. проявляющуюся раз в 3 года? Как только у вас большая система с десятком нитей — вы можете искать до посинения.
Мне не нужна поддержка кода. Написал код — получил результат — забыл о коде.
Бывает, например, что оптимизатор превращает переменную в константу. Передаем указать на int32, функция трактует его как указатель на int64 и портит следующее слово
А зачем пытаться изменить константу, да и вообще, делать действия, не предусмотренные стандартом, а затем бороться с багами оптимизатора? Просто интересно.
Даже просто изменение скорости машины влияет на ошибки. Как пример — одна из функций в VCL в delhpi 7 сбоила только на медленных машинах. Чуть побыстрее — и все проходило штатно.
Попробую угадать: в те времена процессоры были однопоточные, нормальных высокоуровневых средств синхронизации не было, вот программсты и писали кривой многопоточный код.
Но плюс в вашей схеме есть — она позволяет разработчикам и тестерам работать менее эффективно и тем самым — получать больше денег при том же уровне отлаженности приложения.
Типичная проблема современности: заказчик хочет сырой код с багами прямо сейчас, а не отлаженный код потом, и платит за это деньги. Его право.
Зачем хранить дублирующуюся константную информацию, когда можно хранить ссылку?
Представьте себе вызов по базовому классу. Передаваемый указатель содержит vtable и поля класса, причём одним куском памяти. Вот из-за требования одного куска памяти и приходится дублировать vtable.
И vtable при множественном наследовании, кстати, будут разные в зависимости от смещения класса.
Релизная сборка — это не особенность VC++, это просто автоматизация рутинных действий по выставлению ключей компилятора и значений условной компиляции.
При реализации вычислительных алгоритмов, например, обработки изображений, разница между версиями с включёнными и выключенными проверками может быть очень существенна.
Схема такая: программа упала или тесты показали неправильный результат — включаем проверки — ищем ошибку — исправляем ошибку — запускаем заново.
При наследовании мы дополняем vTable новыми методами, а при расширении — создаем новую vTable.
Так оно и есть. Но основная причина создания второй vTable — это наличие полей, а не различия в логике дополнения/расширения.
Если в классе есть поля, то мы вынуждены создавать копию vtable в каждом объекте. Если же полей нет, то нет необходимости её дублировать — достаточно разместить её как часть основного vtable. Именно по такому принципу и работают некоторые компиляторы.
> Вы лучше объясните свои слова про «инвестировать в себя».
Любые действия, которые будут иметь положительный эффект при потере работоспособности.
> Первое неизбежно, второе наивно и порой бывает жестоко.
Это и есть диверсификация рисков. Впрочем, если вы абсолютно уверены в стабильности государства, то от второго варианта можете и отказаться.
> Надеяться надо на себя, а не на чью-то шею.
Если надеяться только на себя, то это нужно уходить отшельников в тайгу.
На чью-то шею все равно придётся надеяться. На банки, которым доверяешь хранение собственных денег. На государство, которое обещает не допускать экономических кризисов, тратить налоговые отчисления на благое дело и обеспечивать социальные гарантии.
Но рано или поздно оказывается, что единственные люди, на которых можно положиться — это семья.
Либо вы смиряетесь с тем, что железка и/или компилятор под неё имеют ошибки в реализации и пишете код с учётом этих ошибок, либо не используете аппаратный FP.
Можно ещё писать код из предположения, что память или устройство хранения данных может аппаратно сбоить.
Почти любая ошибка у меня будет поймана, корректно обработана и не повлияет на работу программы.
Вопрос исключительно в стоимости написания кода (дополнительная логика для обработки ошибок) и его производительности (куча проверок против скорости выполнения) против ущерба от ошибок.
Слои защиты
1) assert — это вывод отладочного сообщения + abort, а не продолжение работы программы.
2) У меня другой подход к данному вопросу. Исключения не должны использоваться для обработки штатных ситуаций. Например, ошибка передачи данных по сети — штатная ситуация, а нехватка памяти — нештатная, т.к. в большинстве случаев приводит к невозможности продолжения штатной работы. Поэтому оператор new должен выдавать исключение, а не nullptr.
3) Да, нужны.
4) Если у вас может быть вызван десктруктор с this == nullptr, вы что-то делаете совсем не так.
5, 6) Возможно, но это все равно может привести к частичной потере данных. Ворд, например, так и делает.
7) Не всё программирование серверное.
Сразу видно человека, который не знаком с тем, как интерфейсы реализованы на низком уровне.
Интерфейс — это абстрактный класс без полей + немного кодогенерации.
Несогласные — пишите, почему и где я не прав, мне самому уже интересно стало. Что из нижеперечисленного неверно?
1. Если бы xor edi, edi не обнуляла верхние 32 бита, то пришлось бы пользоваться xor rdi, rdi. И дело даже не в OoOE, а в том, что потом с нулём сравнивается rdi целиком, а не нижние 32 бита.
2. Зависимость от предыдущего значения регистра — это плохо. Из-за этого 8 и 16-битные команды могут работать медленнее, чем 32-битные.
3. Необходимость копирования частей регистров — это и есть дополнительные операции, генерируемые микрокодом и мешающие оптимизации выполнения команд.
4. В AVX похожая ситуация с обнулением верхней части регистров. И причина этому — регистры YMM были реализованы как пара виртуальных XMM-регистров, отмапленных на «физические» регистры, либо на нулевую константу. Из-за этого задача сохранения половины регистра усложняла поток операций, генерируемых микрокодом, в т.ч. из-за необходимости отдельного копирования половинок регистров.
Что же это за coding standards, которые разрешают сравнение this с нулём и исключения в деструкторах?
В VCL, кстати, множественное наследование разрешено, но только если оно не меняет адрес объекта (т.е. VCL объект должен идти всегда первым), а остальные классы не имеют полей.
Ну оконные библиотеки — достаточно низкий уровень.
Низкий уровень — это контейнеры. А библиотеки — это прикладной код высокого уровня.
Ну в общем в очередной раз убедился. что лучше на C++ не писать без особой нужды.
Так оно и есть.
Лично нам лучше держаться стандартов 25летней давности.
Возможно, нужно было оставить труп в покое и развивать новый язык, а не городить костыли в языке. Но результат был бы тот же: старый стандарт бы просто умер из-за отсутствия поддержки.
Отладочный вывод — это, несомненно, хорошо. Но для вычислительных задач с детерминированными входными данными без случайных внешних событий это попросту не нужно. Это будет либо убийство производительности (проверка условия на логирование на каждом чихе), либо низкая информативность. Все равно что на JavaScript программировать серьёзные вещи (гусары с nodejs — молчите).
Поэтому для задачи обработки изображений я предпочитаю статическую настройку параметров отладки. Ну а для серверного кода — да, логирование со всеми уровнями важности, по-другому никак.
Для таких целей обычно используют alignas, а не делают паддинг переменными, делая код крайне непереносимым и зависимым от конкретного компилятора и его настроек.
Это UB по стандарту, а не баг.
Решение проблемы:
и передача в функцию &value64, а не &value32.
Видимо, надо действительно быть чайником, чтобы нормально объяснять :)
Для меня настолько очевидно, что [vtable] — это указатель на таблицу, а не сама таблица, что по-другому быть и не может.
Именно это и я хотел объяснить. Есть поля во втором объекте — придётся вставлять вторую ссылку на таблицу, тратя память на каждый объект. Нет полей — память тратить не нужно.
Что мешает комбинировать оба подхода: debug-режим — все проверки, в том числе и внутренние, включены, release — включены только проверки входных параметров (например, размеры изображений, выравнивание).
Не очень понимаю, как математика защищает от программистских ошибок.
Опять же: вопрос исключительно в цене ошибки. Задачи разные бывают.
В моём случае разработки носят преимущественно теоретический характер. Попробовал метод, применил — не понравилось, закодил следующий.
Задачи в области обработки медицинских изображений, где цена ошибки уже высока, до реальной практики так и не дошли — нет заинтересованных заказчиков.
А можете пояснить, каким образом это могло повлиять на работу программы? Желательно конкретным примером.
Например, если вы принимаете на вход функции &int32 и преобразовываете внутри функции в &int64, то будьте готовы к тому, что это UB со всеми вытекающими.
А ещё есть такая подлая штука в оптимизаторах, как strict type aliasing, например.
Вот структура типичного объекта в памяти:
Адрес этой структуры и передаётся в функции.
Вот структура объекта с множественным наследованием:
Объединить два vtable в один:
невозможно, т.к. непонятно, как взять адрес от второго объекта.
С другой стороны, если у объекта нет полей, то указатель будет указывать на объект, содержащий только vtable. Соответственно, нет нужды размещать vtable в каждом объекте, когда можно сделать его глобальным.
Простой пример: обработка изображений, а именно проверка на выход за границы изображения при доступе к пикселю. Это очень часто встречающаяся ошибка, поэтому приходится втыкать assert внутрь GetPixel.
Но для хорошо отлаженных алгоритмов: свёртка, сумма, фурье там всякие — эта проверка избыточна и должна быть отключена. А вот для новых алгоритмов — включена.
Только это авария с нулевыми потерями — как в играх, а не реальной жизни.
Мне не нужна поддержка кода. Написал код — получил результат — забыл о коде.
А зачем пытаться изменить константу, да и вообще, делать действия, не предусмотренные стандартом, а затем бороться с багами оптимизатора? Просто интересно.
Попробую угадать: в те времена процессоры были однопоточные, нормальных высокоуровневых средств синхронизации не было, вот программсты и писали кривой многопоточный код.
Типичная проблема современности: заказчик хочет сырой код с багами прямо сейчас, а не отлаженный код потом, и платит за это деньги. Его право.
Представьте себе вызов по базовому классу. Передаваемый указатель содержит vtable и поля класса, причём одним куском памяти. Вот из-за требования одного куска памяти и приходится дублировать vtable.
И vtable при множественном наследовании, кстати, будут разные в зависимости от смещения класса.
При множественном наследовании (C++) и наличии полей таки приходится создавать vtable в экземплярах классов, иначе буде невозможен доступ к полям.
Статические поля, понятное дело, вообще никак не участвуют в полиморфизме и ни на ято не влияют.
При реализации вычислительных алгоритмов, например, обработки изображений, разница между версиями с включёнными и выключенными проверками может быть очень существенна.
Схема такая: программа упала или тесты показали неправильный результат — включаем проверки — ищем ошибку — исправляем ошибку — запускаем заново.
Так оно и есть. Но основная причина создания второй vTable — это наличие полей, а не различия в логике дополнения/расширения.
Если в классе есть поля, то мы вынуждены создавать копию vtable в каждом объекте. Если же полей нет, то нет необходимости её дублировать — достаточно разместить её как часть основного vtable. Именно по такому принципу и работают некоторые компиляторы.
Любые действия, которые будут иметь положительный эффект при потере работоспособности.
> Первое неизбежно, второе наивно и порой бывает жестоко.
Это и есть диверсификация рисков. Впрочем, если вы абсолютно уверены в стабильности государства, то от второго варианта можете и отказаться.
Если надеяться только на себя, то это нужно уходить отшельников в тайгу.
На чью-то шею все равно придётся надеяться. На банки, которым доверяешь хранение собственных денег. На государство, которое обещает не допускать экономических кризисов, тратить налоговые отчисления на благое дело и обеспечивать социальные гарантии.
Но рано или поздно оказывается, что единственные люди, на которых можно положиться — это семья.
Либо вы смиряетесь с тем, что железка и/или компилятор под неё имеют ошибки в реализации и пишете код с учётом этих ошибок, либо не используете аппаратный FP.
Можно ещё писать код из предположения, что память или устройство хранения данных может аппаратно сбоить.
Вопрос исключительно в стоимости написания кода (дополнительная логика для обработки ошибок) и его производительности (куча проверок против скорости выполнения) против ущерба от ошибок.
1) assert — это вывод отладочного сообщения + abort, а не продолжение работы программы.
2) У меня другой подход к данному вопросу. Исключения не должны использоваться для обработки штатных ситуаций. Например, ошибка передачи данных по сети — штатная ситуация, а нехватка памяти — нештатная, т.к. в большинстве случаев приводит к невозможности продолжения штатной работы. Поэтому оператор new должен выдавать исключение, а не nullptr.
3) Да, нужны.
4) Если у вас может быть вызван десктруктор с this == nullptr, вы что-то делаете совсем не так.
5, 6) Возможно, но это все равно может привести к частичной потере данных. Ворд, например, так и делает.
7) Не всё программирование серверное.
Интерфейс — это абстрактный класс без полей + немного кодогенерации.
1. Если бы xor edi, edi не обнуляла верхние 32 бита, то пришлось бы пользоваться xor rdi, rdi. И дело даже не в OoOE, а в том, что потом с нулём сравнивается rdi целиком, а не нижние 32 бита.
2. Зависимость от предыдущего значения регистра — это плохо. Из-за этого 8 и 16-битные команды могут работать медленнее, чем 32-битные.
3. Необходимость копирования частей регистров — это и есть дополнительные операции, генерируемые микрокодом и мешающие оптимизации выполнения команд.
4. В AVX похожая ситуация с обнулением верхней части регистров. И причина этому — регистры YMM были реализованы как пара виртуальных XMM-регистров, отмапленных на «физические» регистры, либо на нулевую константу. Из-за этого задача сохранения половины регистра усложняла поток операций, генерируемых микрокодом, в т.ч. из-за необходимости отдельного копирования половинок регистров.
Чтобы действия людей имели эффект, эти действия должны быть организованы. Но люди неспособны самоорганизоваться без организатора.
В VCL, кстати, множественное наследование разрешено, но только если оно не меняет адрес объекта (т.е. VCL объект должен идти всегда первым), а остальные классы не имеют полей.
А тут опа — множественное наследование (см. одну из ссылок в посте). И B() уже работает с this по адресу 4 или 8, а не 0.
Например, для инъекции кода, когда хотим подгрузить свою библиотечку в чужой процесс.
Низкий уровень — это контейнеры. А библиотеки — это прикладной код высокого уровня.
Так оно и есть.
Возможно, нужно было оставить труп в покое и развивать новый язык, а не городить костыли в языке. Но результат был бы тот же: старый стандарт бы просто умер из-за отсутствия поддержки.