Pull to refresh

Comments 194

А можно вопрос? В чем смысл статьи?
Читатели, возможно, негодуют – опять компилятор «ломает» код и не выдает предупреждений!

Компилировать старый или беззаботно написанный новый код с неопределенным поведением становится все интереснее и интереснее.

Я может и простой студент, а не программист в крупной компании, но подобный «работающий» код никогда не должен писаться. Т.е. сначала используют undefined behaviour, пытаясь сделать его «defined», а потом удивляются и жалуются что компилятор совместимость со старыми программами ломает! Радуются Go, Rust'ам и другим новым языкам, когда в старых языках можно писать аккуратнее, без всяких хаков (например не убираем «ненужные» круглые и фигурные скобки).

Наоборот, новый компилятор помогает находить неправильные участки программы. Не у всех-же есть крутые инструменты как PVS-Studio.
Подобный код не редкость в больших проектах с долгой историей.
Помниться сравнение this с NULL было в недрах MFC.
Интересный факт: сравнение this с нулем есть даже у Страуструпа в его «Programming — Principles and Practice Using C++», причем в относительно свежем (2014 г) издании.
На Google Books есть электронный вариант с частичным предпросмотром, поиск по «this» позволяет найти пример кода с реализацией функции Link::insert() на 619-й странице, где выполняется сравнение this с nullptr.
Смысл статьи — «если в вашем проекте эн лет висело ружьё, то теперь-то оно выстрелит вам в ногу»
Эту статью через полгода кто-нибудь нагуглит, в попытках понять, почему же старый код, который прекрасно комплировался gcc5, не компилируется на gcc6.
На gcc 6.1 код по-прежнему компилируется, просто может работать по-другому. Значительно сложнее заметить.

Если оптимизатор видит такое, то должен смочь вывести и предупреждение. Другой вопрос — что делать, если в проекте и так 100500 предупреждений при сборке.

Интересный вопрос, что кому код должен? :-)

Один из вариантов — во время работы конструктора произошло исключение (например, нехватка памяти). Деструктор выполняется при не полностью созданном объекте. При этом часть вложенных объектов — вообще не инициализированы. Как пример — большое окно с сотней таких CWindow. И вот тут подобная проверка полезна, она добавляет надежности, больше шансов, что деструктор выполнится до конца. Подобноые решения есть во многих оконных библиотеках.

Так что зря они совместимость поломали. Оконные библиотеки при нехватке памяти начнут рушиться как карточные домики.
Деструктор выполняется при не полностью созданном объекте
В случае исключения деструкторы выполняются только для тех объектов (и подобъектов), которые были полностью сконструированы к моменту возникновения исключения.
часть вложенных объектов — вообще не инициализированы
В таком случае нельзя полагаться, что в них будут именно нулевые значения.
> В случае исключения деструкторы выполняются только для тех объектов (и подобъектов), которые были полностью сконструированы к моменту возникновения исключения.

я отстал от жизни? Конструктор объекта выделил память одним new, вторым, третьим и на четвертом словил исключение. И что, не запустится деструктор и выделенная память утечет? Я чуть нечетко написал. Речь о ситуации, когда в классе мы имеем ссылки на объекты, которые создает конструктор этого класса. То есть класс — это экранная форма, и в нем указатели на кнопки и поля ввода, кторые создает конструктор класса.

> В таком случае нельзя полагаться, что в них будут именно нулевые значения.
Можно. Если даже компилятор не инициализирует память объекта нулями, то нам ничего не мешает присвоить полям-указателям NULL, а потом — вызвать new.
Конструктор объекта выделил память одним new, вторым, третьим и на четвертом словил исключение. И что, не запустится деструктор и выделенная память утечет?
Если исключение покинуло конструктор, деструктор объекта вызван не будет. Если адреса созданных конструктором объектов хранятся в «обычных» указателях — членах класса, то «отработают» тривиальные деструкторы этих членов класса и действительно объекты, адреса которых хранились в этих членах класса, могут утечь. Чтобы этого избежать, нужно использовать «умные» указатели, деструкторы которых нетривиальные и позаботятся о привязанных к указателям объектах.
ничего не мешает присвоить полям-указателям NULL
Тогда они перестанут быть «вообще не инициализированными».
> Если исключение покинуло конструктор, деструктор объекта вызван не будет.
Значит что-то сильно поменялось в С++ за 25 лет. В Borland C++ 3.1 он вызывался, насколько помню.

> Тогда они перестанут быть «вообще не инициализированными».
Это тонкости. У Borland память объекта обнуляется до конструктора, поэтому неинициализированный член класса — это NULL.
Значит что-то сильно поменялось в С++ за 25 лет. В Borland C++ 3.1 он вызывался, насколько помню.

Когда я попытался в нём ради интереса написать, я просто не смог его скомпилировать. Это не C++.

Это тонкости. У Borland память объекта обнуляется до конструктора, поэтому неинициализированный член класса — это NULL.

Неинициалилизированный — значит, не имеет никакого значения. Если член класса имеет значение NULL, значит, он уже инициализирован. Нулём.
я отстал от жизни? Конструктор объекта выделил память одним new, вторым, третьим и на четвертом словил исключение. И что, не запустится деструктор и выделенная память утечет?

Видимо, отстали. Современное программирование на C++ предполагает использование умных указателей. Если в коде нет ни одного delete — это хороший код, если ни одного new (кроме случаев, где он оправдан, например, в низкоуровневых библиотеках) — это прекрасный код.

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

Можно. Если даже компилятор не инициализирует память объекта нулями, то нам ничего не мешает присвоить полям-указателям NULL, а потом — вызвать new.

Если вы вручную выделяете память, то обязаны вручную же её и удалять.

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

Если же вы хотите сначала полностью проинициализировать поля класса нулями, а выделять память в конструкторе вручную, то придётся позаботиться и о ручном освобождении памяти, поставив catch и запихнув в него код очистки.
> При этом, если исключение было выброшено не в теле функции конструктора, а инициализатором поля,
Нет, именно в теле функции. С инициализаторами полей автоматика достаточно хорошо разбирается

> кроме случаев, где он оправдан, например, в низкоуровневых библиотеках
Ну оконные библиотеки — достаточно низкий уровень.

Ну в общем в очередной раз убедился. что лучше на C++ не писать без особой нужды.

> Современное программирование на C++
Современное — синоним низкой переносимости под разные ОС и разные железки. Лично нам лучше держаться стандартов 25летней давности. Ну или на 10 лет вперед продумывать, на какие железки придется портировать, а на какие нет.
Нет, именно в теле функции. С инициализаторами полей автоматика достаточно хорошо разбирается
Если члены класса — «обычные» указатели, а список инциализации инициализирует их, передавая результат new SomeType(), «автоматика» не поможет. Если, например, первые три инициализатора отработали успешно, а при работе четверого произошло исключение, отработают в обратном порядке деструкторы полностью сконструированных членов класса, которые для «простых» указателей тривиальные, и снова произойдет утечка привязанных к ним объектов.
Ну оконные библиотеки — достаточно низкий уровень.

Низкий уровень — это контейнеры. А библиотеки — это прикладной код высокого уровня.

Ну в общем в очередной раз убедился. что лучше на C++ не писать без особой нужды.

Так оно и есть.

Лично нам лучше держаться стандартов 25летней давности.

Возможно, нужно было оставить труп в покое и развивать новый язык, а не городить костыли в языке. Но результат был бы тот же: старый стандарт бы просто умер из-за отсутствия поддержки.
Ну С99 не умер и вполне поддерживается. как и С++98. Но мы держимся ещё ниже, потому что вдруг придется реанимировать версию на MS-DOS? У нас не то, чтобы совсем embeded, но машинки слабенькие. А главное — выбор процессора за заказчиком.

Сейчас вот — страшный зверь — lcc++ МСВС на МЦСT. и не поменять — концепция МСВС не разрешает установку своего софта.

Имхо, под железки лучше писать на plain c. Да больше возни, но зато точно знаешь что ожидать от кода.


На "современном C++" можно запросто писать под железо, и при этом код будет очень хорошо переносим. Но конечно, да — скртых способов выстрелить себе в ногу на плюсах больше.
Какая у вас железка, если не секрет, что вы так переживаете за совместимость?


Передовой край для железок сейчас что то типа Rust, но его далеко не везде применишь — llvm backend есть не под все архитектуры, да и с поддержкой либами железа у него пока грустно

Ну почти на Си и пишем. Комментарии, например, везде С++ные.

У нас планида такая — своими технологиями затыкать дыры в чужих проектах. Конкурсы выигрывают одни, подрядчиками записаны другие, а фактически — делаем мы. А потом читаем победные статьи и улыбаемся. Высокоточный GNSS (GPS/ГЛОНАСС) у нас. Измерение расстояний между антеннами с СКО 5 миллиметров.

Основные оптимизации — идут от алгоритма, а не от качества компиляторов. И основные ошибки — от программистов, а не от недочетов языка. Смена языка и компилятора — может дать процентов 5-10, максимум 20.Смена алгоритма или программиста — выигрывает разы. Так что менять язык на что-то современное — есть смысл лишь для больших проектов.

для больших проектов.

или для новых. Очень понравилось использовать C++11 для Baremetal под ARM926E-JS (Cypress FX3). Ограничение по памяти: 300кБ под рантайм (стандартный выкинут, заменён своей лайт-версией), RTOS (ThreadX) и логику. В 300 кБ сейчас влазим с дебаг сборкой, правда не со всеми строками (логи приходится отключать).

Отдельного внимания заслуживает использование регистра rdi. Вызывающий код обнуляет edi – половину rdi, а вызываемый код ДОВОЛЬНО НЕОЖИДАННО – использует наполовину обнуленный rdi.

Инструкция xor edi,edi в 64-битном режиме обнуляет весь rdi, так что нет никакого «наполовину обнулённого» регистра.
В проекте он, конечно, описан, но ведь документацию читают только ламеры ©
Инструкция xor edi,edi в 64-битном режиме обнуляет весь rdi, так что нет никакого «наполовину обнулённого» регистра.

Да, любая инструкция с 32-битными операндами будет обнулять старшие 32 бита. Просто trade-off между скоростью работы и сложностью аппаратной реализации регистров.

А почему именно
xor edi, edi
а не
xor rdi, rdi
: да потому что первая команда на 1 байт короче.
Это не trade-off — это фича, важная для OoOE. Если не обнулять верхнюю половину, то следующая после xor edi, edi инструкция, использующая rdi, будет иметь зависимость и от xor, и от предшествовавшей инструкции, использовавшей rdi. Лишние зависимости делают невозможными перестановки инструкций.
Это не trade-off — это фича, важная для OoOE. Если не обнулять верхнюю половину, то следующая после xor edi, edi инструкция, использующая rdi, будет иметь зависимость и от xor, и от предшествовавшей инструкции, использовавшей rdi. Лишние зависимости делают невозможными перестановки инструкций.

В таком случае, достаточно было бы обнулить верхнюю половину регистра самостоятельно (xor rdi, rdi).

Дело не столько в зависимостях, сколько в необходимости сохранения (=копирования) верхней части регистра одновременно с выполнением операции над нижней частью, а это усложнение конвейера и реализация дополнительных ФУ. Никогда не замечали, что операции с 32-битными целыми выполняются быстрее, чем с 8 и 16-битными целыми?
Несогласные — пишите, почему и где я не прав, мне самому уже интересно стало. Что из нижеперечисленного неверно?

1. Если бы xor edi, edi не обнуляла верхние 32 бита, то пришлось бы пользоваться xor rdi, rdi. И дело даже не в OoOE, а в том, что потом с нулём сравнивается rdi целиком, а не нижние 32 бита.

2. Зависимость от предыдущего значения регистра — это плохо. Из-за этого 8 и 16-битные команды могут работать медленнее, чем 32-битные.

3. Необходимость копирования частей регистров — это и есть дополнительные операции, генерируемые микрокодом и мешающие оптимизации выполнения команд.

4. В AVX похожая ситуация с обнулением верхней части регистров. И причина этому — регистры YMM были реализованы как пара виртуальных XMM-регистров, отмапленных на «физические» регистры, либо на нулевую константу. Из-за этого задача сохранения половины регистра усложняла поток операций, генерируемых микрокодом, в т.ч. из-за необходимости отдельного копирования половинок регистров.
Не претендую на точность, но лично мне логика подсказывает, что:

если операция movl (%rdi), %eax аппаратно не зависела бы от xor edi,edi — при реореринге в %eax появится значение взятое из предыдущего адреса, который был в регистре. Это наводит на мысль, что такое movl наверное зависит от такого xor. То есть это не оптимизация ООЕ, а просто байт сэкономили.

а каким адекватным способом вообще можно докатиться до того, что this будет равен 0?
UFO just landed and posted this here
Если фабрика не сможет собрать объект и вернет nullptr.
Потом вызываем невиртуальный метод.

Если фабрика может вернуть nullptr, вызов метода без проверки на nullptr не является "адекватным".

A()->B()
Если в A() произошла ошибка, он может вернуть nullptr. B() может просто проверять this на nullptr и корректно отваливаться. Когда цепочка из большого количества вызовов, это удобно. Альтернатива — это исключения возбуждать, либо проверять после каждого вызова результат. И то, и другое решение так себе. В итоге люди проверяют this на ноль.
Если в A() произошла ошибка, он может вернуть nullptr. B() может просто проверять this на nullptr и корректно отваливаться.

А тут опа — множественное наследование (см. одну из ссылок в посте). И B() уже работает с this по адресу 4 или 8, а не 0.
Это всё понятно. Но когда такое используют, то обычно понимают такие моменты. Вообще, очень часто множественного наследование запрещено в coding standards.

Я не спорю, что тут есть свои подводные камни, я просто привожу пример когда это вполне уместно, если не считать что это UB.
Что же это за coding standards, которые разрешают сравнение this с нулём и исключения в деструкторах?

В VCL, кстати, множественное наследование разрешено, но только если оно не меняет адрес объекта (т.е. VCL объект должен идти всегда первым), а остальные классы не имеют полей.
Про исключения в деструкторах я не говорил. Мало ли какие поля не инициализированы (и это нормальное состояние структуры данных). Тут всё зависит от предметной области и такое бывает вполне оправдано, не все поля бывают прибиты гвоздями к полу и инициализированы. Хотя лично я бы постарался это обрабатывать как-то иначе.
А это стандартная штука. Уже описывал чуть выше. Есть класс, представляющий окно. И есть го члены — указатели на объекты полей ввода и кнопок.
Словили исключение посредине конструктора — и всё. Половина указателей nil — половина инициализировано.

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

Это примеры из библиотеки VCL из Dephi и C Builder. Такие методы — не панацея, но сильно увеличивают шансы, что локальная нехватка памяти не приведет к завершению программы.
Нехватка памяти практически всегда фатальна. В этом случае нужно как можно корректнее завершить приложение, а не пытаться продолжать работать.

А ещё хуже — исключение из деструктора, когда сам объект ещё числится в списках.

Обращение программистам: никогда не бросайте исключения в деструкторе. Даже если очень хочется. Создайте метод Close или Dispose, но руки прочь от деструкторов. Рассматривайте деструкторы как механизм аварийного удаления объекта.

Принцип RAII хорош, но не может быть применён для всего, чего только можно.
> никогда не бросайте исключения в деструкторе
+100500. Но это не повод переписывать сотни строк библиотеки чужой библиотеки.

Исключение в деструкторе обычно имеет причиной порчу данных до деструктора. И для надежной программы полезно не только исправить ошибку, но и обработать это исключение. Точнее сначала написать восстановление после такой ошибки, а уж потом — исправлять её. Подробности я написал чуть ниже.

Исключение посредние конструктора мой любимый вопрос на собеседованиях. Если конструктор выкидывает исключение, то объект считается не созданным, и грубо говоря обращение к этому объекту это UB.


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


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

Все верно, за исключением нескольких моментов.
1) В Object pascal (Delphi) деструктор действительно вызывался из-за исключения в конструторе.Могу ошибаться, но мне помнилось что и в ранныих версиях С++ было именно так.

2) Перенесение обязанности в конструктор не снимает проблему. Если у в ас сотни new, то или писать сотни catch или сотни проверок или защиты типа обсуждаемой. А лучше — и защиты и проверки и побольше уровней catch.

3) Отсутствие в С++ маппинга аппаратных исключений на программные — это… как бы приличней…… ну в общем крайне плохо. Любое обращение через нулевой указатель приводит к вылету программы. Или к обработке сигналов, где сложно разбираться, кто создан, а кто нет. И ладно бы это касалось только обращения через нулевой указатель, аппаратное исключение может кинуть и при операциях с плавающей запятой, том же извлечении корня из отрицательного числа.

4) В итоге — чтобы сохранить данные при аппаратном исключении — надо раскорячиться. как та корова в бомболюке.

Как пример — http://www.sysauto.ru/index.php?pageid=508 Это писалось на дельфи, объем = десяток человеко-лет, 135 тысяч строк кода, в сервисе — работают два десятка нитей. Цена ошибки (если не повезло) — это цена рулона оцинкованной стали (35-40 тысяч долларов). Поэтому изоляция ошибок там многослойная. На уровнях отдельных процедур, объектов, подсистем и даже приложения в целом (переход на дублирующий сервер).

В системе есть ошибки (ну куда же без них). А обращения по нулевому указателю редко, но бывают. Но изоляция ошибок такова, что она работает 356*24 уже больше десяти лет. Работает — без потери данных. В большинстве случаев ошибка исправляется на уровне перезапуска объекта, реже — перезапуска подсистемы.

И проверка в деструкторе, что объект уже создан — важный элемент обеспечения защиты. Не важно, из-за какой ошибки вызвался деструктор несозданного объекта. Может его повторно вызвали (хорошая практика после деструктора очистить ссылку на объект). Или сбой в конструкторе был. Или порча памяти. Важно — что мы не даем исключению расползтись как лавина, а минимизируем его последствия.

Время на отладку на живой технике там было 2 часа в месяц. Так что отлаживались на наколенных имитаторах. И защиты, защиты, защиты. Каждая найденная реальная ошибка в коде — исправлялась дважды-трижды. То есть вначале отлаживается защита, дающая возможность при этой ошибке работать дальше, а уж потом — правилась сама ошибка.

5) Мне очень интересно, через сколько лет Word научиться сохранять файл при вылете. Лет 30-50, наверное. А ваше мнение?

P.S. Ну и что вы делаете, если конструктор выдал access vioaltion? Да ещё не в вашем коде, а в вызове чего-то системного?

  1. Последний раз что то делал на паскале и borland c++ 3.1 20 лет назад. Не буду врать, что и как было — не помню.


  2. Что бы не писать кучу проверок возможно использовать std::unique_ptr. Но даже без него, достаточно одного блока try catch в конструкторе. Вот как то так:

class A {
public:
  A *objA = 0;
  B *objB = 0;
  A () {
    try {
      objA = new A;
      objB = new B;
    }
    catch (...) {
      if (objA)
         delete objA;
      if (objB)
         delete objB;
      throw ...;   
  }
}

3,4) В C/C++ вообще нет никаких аппаратно и платформо зависимых конструкций. И это правильно — язык должен быть рассчитан на все платформы.
Немного упрощенно — аппаратные исключения обычно перехватываются ОС, и если аппаратное исключение не фатальное и ОС сочтет возможным, то передаст исключение в программу через некий API. В Linux например обращение к нулевому указателю вызывает сигнал SIGSEGV, и программа может его перехватить, установив соответствующий sighandler.


Если говорить про то, как писать саму ОС, или baremetal программу и как в ней обработать аппаратное исключение — это на 200% зависит от архитектуры процессора. Чаще всего вызывается прерывание, меняется контекст, передается управление обработчику прерывания.
Начальная часть обработчика прерывания обычно пишется на ассемблере, что бы вытащить из регистров контекст места, в котором была аппаратная ошибка.


Бесспорно, конечно хорошо подумать о том, что бы при падении не терялись пользовательские данные. Не думаю что word в данном случае хороший пример.


Просто пытаться сохранить стэйт программы из текущих объектов, когда случился сегфолт — не удачное решение. Сегфолт случается от того, что программа стала писать/читать память не по тем местам, где ожидал программист, и как следствие данные уже могут быть испорчены. И вместо пользовательских данных, там может быть каша.
Как простое решение — периодически сохранять данные в отдельный буфер, и считать его md5, или даже уносить буфер с resque бэкапом за пределы адресного пространства доступного программе.


Проверять указатели на nullptr перед удалением в деструкторе и после удаления обнулять их — безвредная практика, хуже от нее не станет.
Но после сегфолта просто так копаться по старой структуре объектов, даже тотально проверяя все на 0/не 0 нельзя, обратите внимание.


P.S. Ну и что вы делаете, если конструктор выдал access vioaltion? Да ещё не в вашем коде, а в вызове чего-то системного?

Конструктор не может выдать "access violation". Как писал выше не стоит путать средства языка C++ и использование его на конкретной платформе с конкретным процессором. Вариантов может быть много.

2) Не всегда помогает. Если у вас десяток объектов и они имеют ссылки друг на друга — все так просто не получится. Ещё раз повторю, что это я про оконную библиотеку и конструкторы классов, представлющих окна. При всем великолепном дизайне VCL есть куча редких и редчайших ситуаций.

3) Лукавите. Механизм сигналов — он платформеннно и апппаратно независим.

>Linux например обращение к нулевому указателю вызывает сигнал SIGSEGV,
Это не в linux, это стандарт POSIX — http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/signal.h.html
В Windows, FreeBSD и куче других систем будет выдан тот же сигнал.

Да, там есть аппаратно-зависимые части, но они сильно под капотом. Распечатать информацию об ошибке- это аппаратно зависимо.
И то — «The <signal.h> header shall define the siginfo_t type as a structure, which shall include at least the following members:» Фактически только регистры процессора передаются в аппаратно-зависимой структуре.

> язык должен быть рассчитан на все платформы.
Насколько помню, это не мешало включить включить аппаратные исключения в АДА. С другой стороны, деление на 0 или корень из -1 — это алгоритмическая ошибка. А вызывает SIGFPE.

> как в ней обработать аппаратное исключение — это на 200% зависит от архитектуры процессора.
Реализация long double или long long тоже на 200% зависит от архитектуры. Как и вся кодогенерация в целом. И что? Давно уже (с алгола 60) известно, что это не мешает.

> Начальная часть обработчика прерывания обычно пишется на ассемблере,
Знаете, вычисление квадратного корня тоже на ассемблере написано (если не делается апрпаратно). И что?

> Бесспорно, конечно хорошо подумать о том, что бы при падении не терялись пользовательские данные. Не думаю что word в данном случае хороший пример.
Word — как раз хороший пример, как большая корпорация с кучей грамотных программистов не смогла этого сделать,

> Просто пытаться сохранить стэйт программы из текущих объектов, когда случился сегфолт — не удачное решение.
> и как следствие данные уже могут быть испорчены. И вместо пользовательских данных, там может быть каша.
Не стоит спорить о вкусе устриц с теми, кто их ел.

Исключение при вызове new — тоже МОЖЕТ означать, что куча глобально испорчена. И вместо объектов там каша. Более того, в большинстве случаев именно это и означает. Нехватка виртуальной памяти — зверь редкостный, а все остально — более-менее глобальное разрушение кучи.

Реально access violation в 95-99% случаев — обращение к полям не созданного объекта или вторичная ошибка в деструкторе при обработке исключения. Вторичные ошибки — просто игнорируются, а при первичных — функция перезапускается заново. 3-5 ошибок в одной функции — перезапускаем подсистему. Если и это не помогло — приложение.

Если не верится — поработайте с любыми дельфийскими GUI-приложениями. Там на уровне архитектуры VCL ошибки изолированы. Нажимаете на кнопку, получаете access violation, но все остальные кнопки программы — работают. В отличие от C++ приложения, которое вылетает без сохранения данных.

И этот подход — оправдывается примерно в 99% случаев. Да, БЫВАЮТ глобальные разрушения в куче или стеке. БЫВАЮТ. Но на самом деле они крайне редки. И если уж что-то глобально разрушено — мы это поймем, получив прерывание в процедурах сохранения.

> Но после сегфолта просто так копаться по старой структуре объектов, даже тотально проверяя все на 0/не 0 нельзя, обратите внимание.
МОЖНО. Но для этого нужно отмпапить SEGSEGV в исключение. И тогда try catch вполне разбирается в том, что произошло и чем это грозит. Обычно — ничем, сбой в GUI-части, данные целы. А вот без механизма структурный исключений это делать действительно тяжело. Потому что главное (место ошибки и стек вызовов) безвозвратно потеряно.

Описанный мной подход — это десяток лет работы 24*365 несмотря на ошибки в коде. И это ноль потерь данных. Но это не на С++, а на delphi.

И ещё раз повторю правило. Любая ошибка закрывается минимум дважды. Сначала на уровне реакции на ошибку (catch). а уж после отладки реакции — правится сама ошибка.

> Конструктор не может выдать «access violation».
Может-может. Если не думать, что библиотека и ядро ОС написано безошибочно — то может. Даже внутри системных вызовов Windows бывает. Относитесь к любой GUI-операции как в вероятности — в 99.99% все будет хорошо. А в 0.01% — плохо.

А «access violation» есть везде, где есть виртуальная память.

Word — как раз хороший пример, как большая корпорация с кучей грамотных программистов не смогла этого сделать,

Не «не смогла сделать», а «не стала делать».
Цена сбоя в Ворде — не рулон стали, а всего-навсего чертыхнувшаяся секретарша.
Не думаю, что много бы нашлось желающих покупать «неубиваемый Ворд» по цене софта для управления производством 24*365.
Стоимость разработки Word — в тысячи раз больше, чем нашего софта 24*365. У нас там всего лишь десяток человеко-лет. Word — это 20 лет разработки и тысячи программеров. Стоимость всего нашего проекта — это годовая зарплата ведущего программера в микрософте.

При том, что Windows сама по себе поддерживает структурные исключения, их реализация в VC++ — не больше человеко-года. Реализация в прикладном коде — человеко-месяцы.

Так что про подъем цены — не надо. В ворде огромное количество редко используемых фичей. Они многократно делали то, что нужно лишь 1% пользователей. Например — купили программу для ввода математических формул.

Так что именно, что не смогла. Потому что после десятого вылета ворда — любой покупатель начинает смотреть в сторону конкурентов. Так что маркетинговое преимущество тут сильное. Скорее всего ни наткнулись на амбиции компиляторщиков.
Какие ещё «амбиции компиляторщиков»? В VC++ есть преобразование SEH -> C++ exception: _set_se_translator.
В Word просто пошли по другому пути: не делать сложных манипуляций при access violation — тем более, что если AV вообще произошло, то в данных вполне может быть каша, а получить документ с кашей намного неприятнее, чем потерять последний сеанс работы — а периодически делать временную копию, пока данные гарантированно целы, при AV вылетать, а после перезапуска предлагать её восстановить.
СПАСИБО, не знал.

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

Ну при порче памяти шансы получить кашу есть и без всякого AV. И большие. Если у объекта 70% данные, а 30% указатели — то шансы 70%, что локальная порча одного слова памяти не приведет к AV.

Так что это ложные страхи.

Просто перехватывать SEH-исключения на С и C++ можно было ещё с момента появления этих самых SEH-исключений в Windows 95, через синтаксис __try / __except / __finally.
Глянул — аж с 1993его года https://accu.org/index.php/journals/1771

Ну тогда я разработчиков Word вообще не пониманию. Неужели они надеялись отладить свой код полностью? Или у них к 1993ему году настолько много было унаследованного кода, что они так и не смогли решиться на рефакторинг?

Как вариант — они так и не перетащили ворд на C++ с обычного Си.

Гм, прямо хоть приятелю в микрософт пиши…
  1. Так кроме unique_ptr еще есть weak_ptr и shared_ptr, совместно на них можно строить весьма сложные конструкции. Про VCL тоже врать не буду — последний раз что то на ней делал 12 лет назад…
    Даже если по каким то причинам использовать умные указатели не представляется возможным/не хочется, то всегда можно написать руками код, который будет гарантированно корректно работать (исходя из предположения, что никто другой не портит кучу и в ОЗУ не случается аппаратных ошибок). Если у вас код в других местах портит кучу, то это повод задуматься о тотальном рефакторинге — хоть в тысяче мест вставляйте проверку на 0, не поможет.
    А если аппаратные проблемы с ОЗУ, то тут даже не знаю что сказать.


  2. Как вы верно заметили, сигналы это элемент стандарта POSIX. Есть куча платформ, начиная от win32, кончая железками на cortex-m где нет POSIX, и как следствие сигналов.

Про АДА — в разных языках — разный подход. Но кажется, к обсуждаемому C++это отношения не имеет.


Система типов включены в стандарт языка С. Аппаратные исключения — нет. Такой вот стандарт.
Математические вычисления не зависят от ОС, а вот обработка исключений еще зависит и от ОС.


Есть аппаратная платформа, есть ОС, есть user space, есть GUI фреймворк и наконец есть язык программирования с его рантаймом.
Вы по большей части описывается специфичное для вас сочетание вышеперечисленных компонентов.


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


Нет никаких гарантий, что обращение


B *b = nullptr;
b->xxx = 123;

вызовет исключение, например если размер класса B большой, и смещение xxx больше размера защитной страницы, то эта конструкция отработает не вызвав исключения, однако попортив что угодно в памяти!

>Если у вас код в других местах портит кучу, то это повод задуматься о тотальном рефакторинге
Тотальный рефакторинг из-за каждой мелкой ошибки? Ошибку нужно просто ловить и править. А ДО исправления сделать так, чтобы она не разрушала всю программу. Типичные сценарии порчи кучи — это ссылка на удаленный объект, двойное выполнение деструктора и так далее. В хорошо отлаженный проектах этого нет. А на этапе отладки — бывает.

По опыт работы с COM- объектами — подсчет ссылок и автоматическое удаление объектов, на которые никто не ссылается, больше мешает, чем помогает. То есть постоянные проблемы с тем, что кто-то удалился слишком рано, а кто-то — не удалился. Это несмотря на то, что дельфи простые случаи автоматизирует. Ну или благодаря этому.

>Есть куча платформ, начиная от win32, кончая железками на cortex-m где нет POSIX, и как следствие сигналов.
Списочек можно? Только реальный. На win32 сигналы есть. Дело в том, что «нет POSIX» означает, что POSIX реализован не в полном объеме или не совсем по стандарту. На NewLib сигналы есть — ftp://sourceware.org/pub/newlib/libc.pdf
Собственно сигналы — это часть стандартной библиотеки языка Си. Так что попрошу список, в каких библиотеках их нет.

А что надо дописать кусочки руками под используемую ОС — так это много для чего надо. Например для инициализации static внутри процедур. Оно очень зависит от реализации нитей и в NewLib и библиотеки gcc не входит. Но это не повод запретить в С++ использовать static внутри процедур.

> Математические вычисления не зависят от ОС,
ДА НУ? Неужели в С++ придумали, как независимо от ОС устанавливать, что делать при извлечении корня из отрицательного числа? Вариантов два — или NaN или исключение. А В силу потери точности при подсчете ряда может на одной машине получится ноль, а в другой — маленькое отрицательное число. 80 бит long double на Intel — это не 64 бита на ARM. Это уже зависимость от архитектуры пошла.

> Но в любом сочетании нельзя писать программу, которая может нечаянно обратиться к произвольному адресу, перехватить исключение, и дальше продолжить работу.
КОМУ нельзя? Любая программа содержит ошибки. Если программа большая — в ней будут и ошибки такого рода. Вы можете потратить сотни человеко-лет, но все равно одна оставленная вами ошибка — это тот дятел, что разрушит всю вашу цивилизацию.

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

Повторюсь. Если в программе много функций (как в том же ворде) acces violation при выполнении одного из пунктов меню в 99% мешает работать только этому пункту меню. В 0.9% случаев — нескольким пунктам меню. И только 0.1% случаеев — это глобальная проблема.

> Нет никаких гарантий, что обращение
Нету. Точно так же нету никаких гарантий, что исключение из new не говорит о глобальной поломке кучи. Более того, пока у вас есть свободная виртуальная память — это 99% именно глобальная поломка кучи.

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

Вы можете истратить сотни человеко лет на поиски ошибок — но все равно не получите гарантию, что найдете их все. Блог PVS studio показывает, что во всех крупных проектах есть ошибки. Более того, есть достаточно много ошибок.

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

А гарантии дает только страховой полис. В программировании гарантий нет, есть только вероятности.

Списочек можно? Только реальный. На win32 сигналы есть. Дело в том, что «нет POSIX» означает, что POSIX реализован не в полном объеме или не совсем по стандарту. На NewLib сигналы есть — ftp://sourceware.org/pub/newlib/libc.pdf
Собственно сигналы — это часть стандартной библиотеки языка Си. Так что попрошу список, в каких библиотеках их нет.

В самой системе win32 нет сигналов — есть их эмуляция как вы верно заметили на уровне libc.


Одно из важных преимуществ c/c++ на железе — возможность работы приложения вообще без стандартной libc. libc не является обязательным атрибутом программы на c/c++. Бесспорно, для некоторых возможностей самого языка c/c++ требуют наличия небольшой рантайм либы, но это уже не libc, а другая очень компактная либа типа libgcc.


Пример окружений где нет stdlib — например, ядро linux, ядро любой RT ОС, и почти каждая 2-ая программа baremetal для микроконтроллеров.

Ну не могу сказать, что возведение в вещественную степень на машине без аппаратной плавающей точки — сильно компактная либа. Я уж не говорю о библиотеке ввода вывода (<< и >> или printf). С другой стороны, поддержка _try _except _finally — намного компактней, чем эмулятор плавающей точки. Так что это не аргумент. Не хотите — не используйте.

мы на FreeRtos используем упрощенный вариант printf, ибо библиотечный мало того, что огромен, он ещё и требует наличия new, то есть кучи. А кучи у нас там нет.

Да и с одинаковым исполнением на разных машинах у С++ засада. Ибо размер int — везде разный. И начинаются танцы с бубнами типа stdint. И такая же засада с long double, где количество бит — от 64 до 128, в зависимости от железа.

Не говоря уж о том, что в каждом компиляторе — свои погремушки.

Так что пока не вижу у вас серьезных аргументов, почему вещественном переполнении программа должна вылетать.

Сами прерывания плавающей точки — определены в стандарте https://en.wikipedia.org/wiki/IEEE_floating_point#Exception_handling А вот их обработка — уже идет сигналами.

Как в анекдоте про вильку, тарельку и бутильку «умом понять это невозможно, это можно только запомнить».

В системе целочисленных типов в C++11 все очень четко прописано. Если требуется конкретная разрядность используется intXX_t/uintXX_t, если требуется переменная, с которой процессору удобнее работать, используйте int. Где засада то?


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


Пытаюсь вас убедить, что код, ловящий null pointer derefernce через исключения операционной системы в какой то момент может сломаться, а вы лишитесь возможно важного круга защиты вашей программы, ибо по стандарту языка null pointer dereference это UB, а не сегфолт.

>Где засада то?
В printf и принципе «ни одного варнинга» Способы решения есть, но переделывать муторно

> Речь шла об исключениях вызванных UB.
Для меня это одна категория — исключения. которые не ловятся и вызывают слет программы. Точнее ловятся, но так, что лучше бы и не ловились.

> Пытаюсь вас убедить, что код, ловящий null pointer derefernce через исключения операционной системы в какой то момент может сломаться
Знаете, вещественная арифметика ещё как ломается. На одной машине 2. было ровно 16 (EC-1036 с серийным номером 3), на другой — деление ненормализованного на ненормализованное выдавало «отсутствующая инструкция» (СМ-2 ревизия платы микрокода 11, если не путаю). Это за соседним столами коллеги нашли.

Лично у меня _fpclass из float.h вообще все сломал — он грузил мусор в слово управление плавающим сопроцессором.

И что? Не использовать вещественную арифметику?

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

Важно, что у меня СЕМЬ слоев защиты, а у вас — 1-2. Почти любая ошибка у меня будет поймана, корректно обработана и не повлияет на работу программы. А вы — НАДЕЕТЕСЬ, что найдете все ошибки. А поиск всех ошибок — мало того, что очень дорог — он ещё и невозможен принципиально. Мы тогда добились одной ошибки на 2 тысячи строк кода. А сколько ошибок в ваших проектах?

Слои защиты
1) агрессивное использование assert (в дельфи он выдает исключение)
2) агрессивное использование try finally и try except
3) Проверки на if (ptr != NULL) до вызовов
4) Проверки на if (this == NULL) в деструкторах и том, что может быть вызвано из других деструкторов
5) Перезапуски частей подсистем (пересоздание объектов)
6) Перезапуски подсистем
7) Переход управления на дублирующий сервер

Стратегия защиты
1) Планирование перезапусков — на этапе проектирования
2) assert, try, проверки до вызовов — при написании кода
3) Любая найденная ошибка изолируются до её исправления. То есть в коде ставится 1-3 защиты, которые нейтрализуют последствия конкретной ошибки или проверяют её (assert) на раннем этапе. Делается в предположении, что найденная ошибка — лишь одна из большого класса похожих ситуаций.
4) Ошибки исправлются
Знаете, вещественная арифметика ещё как ломается.

Либо вы смиряетесь с тем, что железка и/или компилятор под неё имеют ошибки в реализации и пишете код с учётом этих ошибок, либо не используете аппаратный FP.

Можно ещё писать код из предположения, что память или устройство хранения данных может аппаратно сбоить.

Почти любая ошибка у меня будет поймана, корректно обработана и не повлияет на работу программы.

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

Слои защиты

1) assert — это вывод отладочного сообщения + abort, а не продолжение работы программы.
2) У меня другой подход к данному вопросу. Исключения не должны использоваться для обработки штатных ситуаций. Например, ошибка передачи данных по сети — штатная ситуация, а нехватка памяти — нештатная, т.к. в большинстве случаев приводит к невозможности продолжения штатной работы. Поэтому оператор new должен выдавать исключение, а не nullptr.
3) Да, нужны.
4) Если у вас может быть вызван десктруктор с this == nullptr, вы что-то делаете совсем не так.
5, 6) Возможно, но это все равно может привести к частичной потере данных. Ворд, например, так и делает.
7) Не всё программирование серверное.
> Либо вы смиряетесь с тем, что железка и/или компилятор под неё имеют ошибки в реализации
Либо исправляем железку. За ЕС-1036 завод сильно извинялся и прислал новые платы, в СМ-2М платы с новой версией микрокода взяли из шкафа.

> Можно ещё писать код из предположения, что память или устройство хранения данных может аппаратно сбоить.
А так и пишем для батарейного SRAM. Между запусками может сдохнуть батарейка и данные испортится. Так что храним ещё и CRC.

> Вопрос исключительно в стоимости написания кода (дополнительная логика для обработки ошибок)
ДЕШЕВЛЕ, чем исправление всех ошибок. Раз в 10 дешевле. Ну и на 30% дороже, чем написание сбоящего дерьма. Причем, если не делать дублирование серверов и перезапуск подсистем — будет всего лишь процентов на 10 дороже.

> и его производительности (куча проверок против скорости выполнения) против ущерба от ошибок.
Производительность теряется на 0.1%. Потому что время тратится на циклы, а проверки в цикле никто не делает. Потери производительности идут при срабатывании исключений, а не на проверках и try-блоках.

>1) assert — это вывод отладочного сообщения + abort, а не продолжение работы программы.
«Ох уж эти сказки, ох уж эти сказочники...» (с) падал прошлогодний снег
http://delphi-box.ru/assert-delphi.html
procedure Assert (expr: Boolean [; const msg: string]);
Если проверяемое утверждение будет ложным, то процедура прекратит работу и сгенерирует исключение EAssertionFailed с выдачей ошибки в сообщении.

Описываемая система была написана на дельфи. Впрочем, в C Builder все дельфийские штучки есть. А для GCC — написан свой собственный assert.

> 2) У меня другой подход к данному вопросу.
И в чем его отличия? УВЫ, непонятно.

«агрессивное использование try finally и try except» означает, примерно такой код
Lock();
_try {
_try {

} _except {
Вывод сообщение об ошибке
}
} _finlaly {
Unlock();
}

Независимо от ошибок — ресурс разлочится. Как минимум мы этим устраняем ситуацию вечного захвата ресурса одним тредом. Как максимум, когда в качестве Lock — захват памяти из кучи — устраняем утечки памяти.

> Исключения не должны использоваться для обработки штатных ситуаций.
Выполнение исключений — процесс относительно медленный, согласен.

> а нехватка памяти — нештатная, т.к. в большинстве случаев приводит к невозможности продолжения штатной работы.
32 мега памяти, PHP + Apache + приложние на С++. Пришел десяток клиентов — и нехватка памяти стала штатной. Так что подождали 10 мс и пошли выделять опять.

>4) Если у вас может быть вызван десктруктор с this == nullptr, вы что-то делаете совсем не так.
Если у вас есть программа больше 100 тысяч строк — вы готовы поспорить на миллион долларов, что у вас такого НИКОГДА не произойдет? Я ж тестер, я ж найду. Подменю new, чтобы он рандомно исключения выдавал + проверка во всех деструкторах.

Обычно вызов деструктора от NULL — это третичная ошибка. Было исключение (первичная ошибка) и обработалось оно кривовато (вторичная ошибка). Но восстанавливаться оно мешает.

> 5, 6) Возможно, но это все равно может привести к частичной потере данных.
А можно сделать и без потерь. Но это — процентов 20 стоимости.

> 7) Не всё программирование серверное.
Перезапуск приложения можно и на клиенте сделать. Только от потери питания спасать не будет. И от аварии жесткого диска. Но от программных ошибок — спасти может.

«Ох уж эти сказки, ох уж эти сказочники...» (с) падал прошлогодний снег
http://delphi-box.ru/assert-delphi.html
procedure Assert (expr: Boolean [; const msg: string]);
Если проверяемое утверждение будет ложным, то процедура прекратит работу и сгенерирует исключение EAssertionFailed с выдачей ошибки в сообщении.

Давайте все-таки использовать общепринятые термины, а не то как их поняли разработчики конкретной библиотеки?


to assert в переводе с английского — "утверждать". Когда программист пишет assert — он утверждает что некоторое высказывание истинно. Утверждения не надо проверять, им принято верить. Assert — это разновидность комментария, пригодная для автоматического анализа.


То, что утверждения иногда проверяются во время работы — это особенность некоторых библиотек, а не Assertов как таковых.

> Давайте все-таки использовать общепринятые термины, а не то как их поняли разработчики конкретной библиотеки?
«Общепринятые» это для какого языка? Когда пишешь на дюжине языков и читаешь полсотни — привычки отдельных любителей VC++ как-то не принимаешь во внимание.

Рекомендую ознакомиться хотя бы с вики — https://en.wikipedia.org/wiki/Assertion_(software_development)

Как правило, assert вызывает исключения, в С за неимением полноценных исключений, использовались сигналы, а в С++ просто оставили сишный вариант.

> То, что утверждения иногда проверяются во время работы — это особенность некоторых библиотек, а не Assertов как таковых.

static_assert — это отдельная сущность, введенная в С++11.

Да, конечно, можно рассматривать assert как разновидность контракта. Но в большинстве языков это именно динамически проверяемая разновидность.

Хотите — сделайте сравнительный обзор языков программирования по семантике assert.

C++: макрос assert из стандартной библиотеки отключается в релизной сборке (при определенном макросе NDEBUG)
C#: метод Debug.Assert отключен в релизной сборке (без определенного ключа DEBUG)
Java: ключевое слово assert ничего не делает если при запуске не указать параметр -enableassertions
Javascript: тут console.assert не отключается. Но при этом он (в браузерном варианте) и не останавливает работу скрипта — то есть, опять-таки, ничего не защищает.


Какие там еще популярные языки есть?

> C++: макрос assert из стандартной библиотеки отключается в релизной сборке
Плиз, описание «релизной сборки» из стандарта. :-) Релизная сборка — особенность VC++, в gcc её нету

> Какие там еще популярные языки есть?
Гм, я вам про Фому, вы мне про Ерёму.

Я вам про выполнение assert во время исполнения программы, а не во время компиляции. А вы про то, что его можно отключить. Да, отключить можно. Но если программист писал assert грамотно — это не нужно.

Как сказал кто-то из классиков «У любителей отключать проверки после отладки надо отключать тормоза на машине после обучения вождению».
Релизная сборка — это не особенность VC++, это просто автоматизация рутинных действий по выставлению ключей компилятора и значений условной компиляции.

При реализации вычислительных алгоритмов, например, обработки изображений, разница между версиями с включёнными и выключенными проверками может быть очень существенна.

Схема такая: программа упала или тесты показали неправильный результат — включаем проверки — ищем ошибку — исправляем ошибку — запускаем заново.
> Релизная сборка — это не особенность VC+
Ну куча juniorов считают её вообще особенностью языка Си. :-)

> разница между версиями с включёнными и выключенными проверками может быть очень существенна.
ну чайники ещё и не такие ошибки делают. Чтобы разница была существенной, нужно воткнуть assert внутрь часто исполняемого цикла. Или внутри assert вызвать что-то трудоемкое. Для junior простительно, для midle уже баг.

> Схема такая: программа упала или тесты показали неправильный результат — включаем проверки
Вы на машине так же ездите? Пока не попали в аварию — тормоза отключены?

> включаем проверки — ищем ошибку
И сколько времени вам потребуется, чтобы найти ошибку. проявляющуюся раз в 3 года? Как только у вас большая система с десятком нитей — вы можете искать до посинения.

Я уж не говорю о том, что при отключении оптимизации многое ошибки просто пропадают. Дело не только в ошибках оптимизатора. Бывает, например, что оптимизатор превращает переменную в константу. Передаем указать на int32, функция трактует его как указатель на int64 и портит следующее слово. Но с оптимизацией и без неё — это разные слова.

Мы как-то 2 часа убили, пытаясь понять, почему закомментирование одного, не выполняемого в тесте куска кода влияет на выполнение совсем другого куска. Как оказалось — при закомментировании убиралось присваивание и компилятор превращал переменную в константу.

Даже просто изменение скорости машины влияет на ошибки. Как пример — одна из функций в VCL в delhpi 7 сбоила только на медленных машинах. Чуть побыстрее — и все проходило штатно.

Но плюс в вашей схеме есть — она позволяет разработчикам и тестерам работать менее эффективно и тем самым — получать больше денег при том же уровне отлаженности приложения.
ну чайники ещё и не такие ошибки делают. Чтобы разница была существенной, нужно воткнуть assert внутрь часто исполняемого цикла. Или внутри assert вызвать что-то трудоемкое. Для junior простительно, для midle уже баг.

Простой пример: обработка изображений, а именно проверка на выход за границы изображения при доступе к пикселю. Это очень часто встречающаяся ошибка, поэтому приходится втыкать assert внутрь GetPixel.

Но для хорошо отлаженных алгоритмов: свёртка, сумма, фурье там всякие — эта проверка избыточна и должна быть отключена. А вот для новых алгоритмов — включена.

Вы на машине так же ездите? Пока не попали в аварию — тормоза отключены?

Только это авария с нулевыми потерями — как в играх, а не реальной жизни.

И сколько времени вам потребуется, чтобы найти ошибку. проявляющуюся раз в 3 года? Как только у вас большая система с десятком нитей — вы можете искать до посинения.

Мне не нужна поддержка кода. Написал код — получил результат — забыл о коде.

Бывает, например, что оптимизатор превращает переменную в константу. Передаем указать на int32, функция трактует его как указатель на int64 и портит следующее слово

А зачем пытаться изменить константу, да и вообще, делать действия, не предусмотренные стандартом, а затем бороться с багами оптимизатора? Просто интересно.

Даже просто изменение скорости машины влияет на ошибки. Как пример — одна из функций в VCL в delhpi 7 сбоила только на медленных машинах. Чуть побыстрее — и все проходило штатно.

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

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

Типичная проблема современности: заказчик хочет сырой код с багами прямо сейчас, а не отлаженный код потом, и платит за это деньги. Его право.
> проверка на выход за границы изображения при доступе к пикселю.
4 assert на крайние точки. Исполняются однократно. Остальное — математикой.

> Только это авария с нулевыми потерями — как в играх, а не реальной жизни.
Цена остановки стана — 40 тысяч долларов (рулон стали) улетает в брак. Худшее, что можно сдедать — это взорвать печь, работающую на водороде. С учетом того, что собственное рабочее место при отладке над печью, шанс выжить отрицательные.

И это мы ещё не управляли станом, а лишь делали «черный ящик» (+ система визуализации и отладки) для контроллеров, станом управляющих. Но шансы при ошибке вмешаться в управление — были

> Мне не нужна поддержка кода. Написал код — получил результат — забыл о коде.
Ну если ваш код одноразовый — то бывает и так. Ну или если клиенты одноразовые. Продали лоху халтуру и пошли искать следующего лоха.

> А зачем пытаться изменить константу,
Читайте ВНИМАТЕЛЬНО — «оптимизатор превращает переменную в константу».
.
было bool fatsMode = false; Потом в одной из веток fatsMode = true; Когда закомментарили эту ветку, компилятор перестал выделять память под fatsMode. Ну и все выделение памяти — поехало.

«Попробую угадать: в те времена процессоры были однопоточные, нормальных высокоуровневых средств синхронизации не было»
В те времена процессоры были 2-4 ядерные, а высокоуровневые средства были придуманы лет 20 назад.

Но вот в РЕАЛИЗАЦИИ высокоуровневого средства (ожидания завершения треда) — коллеги из Borland лажанулись. Причем лажанулись так, что у них, на быстрых машинах все хорошо было. А вот на медленных — ошибка встала в полный рост.

> заказчик хочет сырой код с багами
Прямо так в договоре и написано — не меньше 50 багов на модуль? :-)

Такое впечатление, что вы на PHP пишите.

УВЫ, у нас УГОЛОВНЫЙ КОДЕКС.

Ну вот, например

«УК РФ, Статья 217. Нарушение правил безопасности на взрывоопасных объектах

1. Нарушение правил безопасности на взрывоопасных объектах или во взрывоопасных цехах, если это могло повлечь смерть человека либо повлекло причинение крупного ущерба, — 3. Деяние, предусмотренное частью первой настоящей статьи, повлекшее по неосторожности смерть двух или более лиц, — наказывается… либо лишением свободы на срок до семи лет...»

Ну как пример. На одном цементном заводе решили ввести встрой цементный фильтр, не дожидаясь готовности программы. Слава богу, решили САМИ, ни одной наши подписи не было. Итог — не уследили, фильтр щабился цементной пылью и упал. Цементный фильтр — это такая огромная дура на ножках. Под фильтром — рабочие места ЧЕТЫРЕХ человек.

Один был в туалете, один в курилке, двое удрали из под падающего фильтра.

А если бы НЕ УДРАЛИ?

Слава богу — ни одной нашей подписи, разрешающей эксплуатацию без ПО не было. А кто подписал — пошли под суд. Вместе с тем, кто вовремя не проверил фильтр.

Вот один раз в такой ситуации побудете — отучитесь писать код с багами. :-)

4 assert на крайние точки. Исполняются однократно. Остальное — математикой.

Что мешает комбинировать оба подхода: debug-режим — все проверки, в том числе и внутренние, включены, release — включены только проверки входных параметров (например, размеры изображений, выравнивание).

Не очень понимаю, как математика защищает от программистских ошибок.

Цена остановки стана — 40 тысяч долларов (рулон стали) улетает в брак.…
Вот один раз в такой ситуации побудете — отучитесь писать код с багами. :-)

Опять же: вопрос исключительно в цене ошибки. Задачи разные бывают.

В моём случае разработки носят преимущественно теоретический характер. Попробовал метод, применил — не понравилось, закодил следующий.

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

было bool fatsMode = false; Потом в одной из веток fatsMode = true; Когда закомментарили эту ветку, компилятор перестал выделять память под fatsMode. Ну и все выделение памяти — поехало.

А можете пояснить, каким образом это могло повлиять на работу программы? Желательно конкретным примером.

Например, если вы принимаете на вход функции &int32 и преобразовываете внутри функции в &int64, то будьте готовы к тому, что это UB со всеми вытекающими.

А ещё есть такая подлая штука в оптимизаторах, как strict type aliasing, например.
> Что мешает комбинировать оба подхода:
МНОГО что
1) debug и release — два разных режима компиляции, два разных вариант исполняемого кода. для большой программы — найдется причина, из-за которой они будут существенно разными. То есть с разными ошибками. Отладив debug — мне не бдуем иметь гарантии, что отладили release — там могут вылезти совсем другие ошибки.
Да, на идеальных компиляторах при идеальных программистах такого нету. А на реальных — есть.

2) debug и release -это или в шубе или голышом. А в реальном мире мы одеваемся по погоде. Принцип все или ничего не удобен. Удобнее — набор дефайнов, каждый из которых включает свой тип тяжелых агрессивных проверок.

3) Динамическое включение отладки — ненамного дороже включения при помощи дефайнов, зато позволяет прямо в момент ошибки включить отладку и посмотреть, что происходит. Собственно у меня динамика управляет подробностью вывода информации на консоль: fatal, msg, error, warning, information, statisctic, debug, full. Обычно стоит на error, но при нужде — можно переключить и прямо на испытаниях понять, что происходит. Помогает при генеральском-эффекте, когда 15 минут до приезда чужого гендиректора, а оно внезапно не работает.

> Не очень понимаю, как математика защищает от программистских ошибок.
https://ru.wikipedia.org/wiki/Формальная_верификация

> Задачи в области обработки медицинских изображений, где цена ошибки уже высока, до реальной практики так и не дошли — нет заинтересованных заказчиков.
Ну да, сырой код с багами медикам не нужен. :-))))

> А можете пояснить, каким образом это могло повлиять на работу программы? Желательно конкретным примером.
В системе не было кучи. FreeRTOS, STM32 и так далее. И был большой объект, написанным математиком.
Ну сделали char objStorage[4096], а потом Placement New. Ну и выяснилось, что модуль реализующий объект работает, только когда объект выровнен на 4хбайтовую границу. А если не выровнен — вылетает. Ну gcc так решил, что раз объект — значит выровнен на границу слова. А выравнивание зависело от той переменной, которую gcc превращал в константу.

> Например, если вы принимаете на вход функции &int32 и преобразовываете внутри функции в &int64, то будьте готовы к тому, что это UB со всеми вытекающими.
Это не UB, это просто БАГ — порча памяти при записи или мусор при чтении. Но баг, проявляющийся в зависимости от закомментирования совсем другого куска кода (куда программа не заходит) это нечто.

Это я уже давно всё понял — у нас просто сильно разные задачи.

3) Динамическое включение отладки — ненамного дороже включения при помощи дефайнов, зато позволяет прямо в момент ошибки включить отладку и посмотреть, что происходит.

Отладочный вывод — это, несомненно, хорошо. Но для вычислительных задач с детерминированными входными данными без случайных внешних событий это попросту не нужно. Это будет либо убийство производительности (проверка условия на логирование на каждом чихе), либо низкая информативность. Все равно что на JavaScript программировать серьёзные вещи (гусары с nodejs — молчите).

Поэтому для задачи обработки изображений я предпочитаю статическую настройку параметров отладки. Ну а для серверного кода — да, логирование со всеми уровнями важности, по-другому никак.

А выравнивание зависело от той переменной, которую gcc превращал в константу.

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

Это не UB, это просто БАГ — порча памяти при записи или мусор при чтении.

Это UB по стандарту, а не баг.

Решение проблемы:

union {
    int64   value64;
    struct {
        int32    value32;
        bool    b;
  }
}


и передача в функцию &value64, а не &value32.
> Но для вычислительных задач с детерминированными входными данными без случайных внешних событий это попросту не нужно.
Ну до тех пор, пока не окажется, что на отладочной сборке все хорошо, а на релизной — ИНЫЕ результаты. Например из-за оптимизации. И вот тогда после вставки отладки советую её не убирать, а просто отключить. Ибо один такой баг — это сигнал, что их может быть ещё десяток.

> Для таких целей обычно используют alignas,
КОНЕЧНО. Именно как отсутствие выравнивания это и было квалифицировано. Заменили char objStorage[4096] на что-то вроде double objStorage[512] и все пошло.

> а не делают паддинг переменными,
ЖУТЬ.

> Это UB по стандарту, а не баг.
Баг это. Неверное описание типа. У части API — void *, в обертках надо правильно писать тип.
Ну до тех пор, пока не окажется, что на отладочной сборке все хорошо, а на релизной — ИНЫЕ результаты

Обычно это является проявлением ошибок в программе.

Я только один раз сталкивался с действительно багом компилятора — C++ Builder неправильно генерил код для работы с многомерным массивом (писал 4-байтный float в ячейки, а должен был 8-байтный double).

Заменили char objStorage[4096] на что-то вроде double objStorage[512] и все пошло.

И таким образом, вы снова пришли к UB, т.к. выравнивание типов — implementation specific. Вы надеетесь на компилятор, что он выровнит double по размеру типа, что, вообще говоря, он делать не обязан.

Баг это. Неверное описание типа. У части API — void *, в обертках надо правильно писать тип.

Устранение переменной для экономии памяти — это не баг. Полагаться на расположение переменных в памяти — это тоже UB. Я привёл решение по его исправлению, рекомендуемое по стандарту (union).

Ну а кривое API — да, проблема, но к оптимизатору не относится.
> Обычно это является проявлением ошибок в программе.
Чаще — агрессивная оптимизация. Тот же алиасинг.

> Я только один раз сталкивался с действительно багом компилятора
см. выше. Компилятор (плюс библиотека плюс линкер) при одних и тех же настройках сделал два взаимоисключающих действия.
1) Сгенерил код для работы с объектом, требующий выравнивания.
2) new выдал невыровненный адрес и вызвал с ним конструктор
Как минимум — компилятор должен был понять, что мы передаем невыровненный адрес в Placement New и выдать ошибку.

> выравнивание типов — implementation specific
Ровно как и генерация кода, работающего только с выровненным объектом.

> Вы надеетесь на компилятор, что он выровнит double по размеру типа, что, вообще говоря, он делать не обязан.
Он ОБЯЗАН сделать одно из двух
ИЛИ сгенерировать код, работающий с невыровненными данными.
ИЛИ выровнять данные.

Так что никакого UB.

alignas введен в С++11, что для нас уж точно implementation specific. То есть поддерживается лишь на части компиляторов. В тот момент даже последние версии gcc C++11 полностью не поддерживали. Для общего кода у нас требование — работать на gcc 2.95.4 из МСВС 3.0. В связи со спецификой МСВС компилятор там менять нельзя.

Но вообще думать об implementation specific когда речь идет о коде на конкретную железку (и только на неё) — это ПЯТЬ! Железка — наша собственная, изготовлена в трех экземплярах, специфична — донельзя.

> Устранение переменной для экономии памяти — это не баг.
ЕЩЁ РАЗ. Неверное описание типа в параметрах — это БАГ. API просит void *, в параметрах обертки написали &int32, А надо было &int64.

> Я привёл решение по его исправлению, рекомендуемое по стандарту (union).
мягко говоря, ваше решение не к той задаче.

UFO just landed and posted this here
> какое влияние неубранные ассерты окажут?
НИКАКОГО. Внутрь цикла асссерты ставят только школьники. Профи если и ставят, то под if (i & 0x1000), то есть на каждый 4096ой оборот цикла.

> является самым узким местом.
Прочтите про SSE2 и попробуйте переписать на SSE2. Как минимум — будут полезны инструкции управления кэшем.

Если SSE2 не подходит, то делайте так
Counts[b[i]<<1+(a [i] < threshold)]++;
Тут четные элементы — это secondCounts, а нечетные firstCounts. Выигрыш будет за счет отказа от if, то есть оптимизации наполнения конвеера.

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

Попробуйте прислать мне в личку, что вы хотите сделать — может и придумаю, как это ускорить.Оптимизатор дает максимум процентов 20, а вот переделка алгоритма иногда и в 1000 раз ускоряет. Правда обычно за счет потребления памяти.
UFO just landed and posted this here
> Есть функция, которая считает энтропию некоторого массива величин. Её надо ассертом бы покрыть, что массив непустой, не правда ли?
Почему? Если не зашли в цикл подсчета энтропии — пусть выдает -NaN, получите exception при использовании результата. Хотя… это же у вас C++, то есть изначально SIGFPE будет… Придется включать преобразование аппаратных исключений в программные. Да, это более дельфийский способ.

> хотя log2 всяко дольше всяких проверок, но код с ассертом уже будет совсем не то, не правда ли?
Почему? Ну я, скажем, готов потратить 1% времени процессора на производительность. Вы — ну скажем 0.1%
Сравнение в assert по времени — как сравнение в заголовке цикла. Навскидкут типичное торможение будет на уровне 0.01% при циклах от 10 элементов. Вы ведь не делаете подсчет энтропии прямо в в цикле, а выносите в процедуру. Хотя затраты на вызов — чуть больше, чем на ассерт.

>Вот только проблема, что она вызывается в цикле,
И там так сложно все написано, что при любом вызове может быть 0 элементов? Третье решение — это математически верифицировать цикл. То есть доказать, что он не может вызывать энтропию для пустого массива.

Четвертое решение — ассерт лишь в тех ветвях цикла, что могут привести к пустому массиву. При этом срабатывание ассерт даст больше информации.

Пятое решение — пусть энтропия для пустого массива выдает -INF. А после окончания вашего цикла — вставить assert на то, правдоподобен ли полученный результат.

Видите сколько вариантов? Всегда можно найти компромисс между надежностью и скоростью.

>У меня примат-образование, но не суть.
Хуже математиков программируют только физики. :-) Математику (кандидату наук!) нужно выставить бит. Пишется int32u mask= 2.**N (программер написал бы 1 << N). На IA32 все работает, ибо вещественные длиной 80 бит. Лет через 5 код портируется на ARM, где вещественные — только 64 бита. В итоге чуть не хватает точности и вместо 2*31 получается 2*31-1, то есть выставлены все биты, кроме нужного. Ловили это года 3. И только когда поймали — поняли, почему у нас не работало, когда был виден 32ой спутник GPS.

1983 год. Физик (доктор наук, физфак СПбГУ) получает в программе деление на 0. Смотрит в код и говорит, что физически эта величина нулем быть не может. Делаем ему обход — если 0, то не делить. И он считает, что все в порядке — ошибка исправлена.

Кодировать все-таки должны программеры.

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

> Random forest я делаю
УВЫ, математика у меня на уровне школы, так что понял мало.

Если известно общее число объектов для классов, то можно считать только firstCounts. secondCounts получим вычитанием. В SSE2 есть смысл посмотреть на управление кэшем. Если все данные не влезают в кэш, то разумно в нем оставить счетчики. А a[i] и b[i] не кэшировать.

> i идёт не по всем элементам массива
> Пробегать надо, причем, не по всем объектам, а лишь по некоторым, что определяется неким массивом, из которого берутся i.
А если сначала скопировать нужные элементы в отдельный массив? А потом — обработать SSE2. Если массивы a и b не лезут в кэш, а получившиеся массивы влезут в кэш — то может ускориться. А если такое «пробегание» используется в нескольких циклах — ускорение может стать приличным.

Если не зашли в цикл подсчета энтропии — пусть выдает -NaN, получите exception при использовании результата

Вот в этом и заключается отличие debug и release версий.

В release режиме, предлагаемом вами, ошибка поймается где-то позже, причём причину ошибки (конкретная итерация цикла, где произошёл вызов) будет искать довольно трудоёмко.

Если добавить отключаемые assert (=debug версия), то ошибка поймается внутри вызова функции, дальше будет легко подняться по стеку.

Но вас вариант с отключаемыми на этапе компиляции проверками не устраивает по причине возможного внесения оптимизатором багов в код.
> В release режиме, предлагаемом вами, ошибка поймается где-то позже, причём причину ошибки (конкретная итерация цикла, где произошёл вызов) будет искать довольно трудоёмко.
НАОБОРОТ. Этот вариант применяется когда мы в том же обороте цикла делаем вычисления с вычисленной энтропией. То есть при исключении — мы будем иметь ссылку на строку кода сразу за вызовом подсчета энтропии.

> Если добавить отключаемые assert (=debug версия), то ошибка поймается внутри вызова функции, дальше будет легко подняться по стеку.
Ну может вам и легко понять, в каком из пяти циклом с десятком вызовом данной функции был отказ. А мне — сложно. Я предпочитаю иметь точку отказа там, где произошла ошибка. И если что — вставить в сообщение об ошибках — значения переменных.

> Но вас вариант с отключаемыми на этапе компиляции проверками не устраивает по причине возможного внесения оптимизатором багов в код.
Это лишь одна из причин.

Основная разница ИНАЯ
1) Ваши проверки нужны, чтобы показать заказчику. что ОШИБОК НЕТ.
2) Мои — чтобы НАЙТИ ОШИБКУ, а в идеале — надежно работать НЕВЗИРАЯ на ошибки.

Зачем вам диагностика на машине? Раз в год заехал на СТО, проверился — и всё. А автопроизводители — такие же дураки, как и я, они постоянно проверяют узлы машины.

Вы давно видели падающий лифт? Не видели? Ну и я не видел. Так давайте выкинем из лифта тормоза, сделанные на случай обрыва троса.

Система оцинковки стали — не лифт и не машина. Цена ошибки — НАМНОГО больше. В день выпускается продукции больше миллиона долларов. Это уже ближе к самолетной надежности.

Но надежность самолета — это очень дорого. А то, что я предлагаю (без перезапусков серверов и систем) — стоит КОПЕЙКИ.До 1% времени выполнения + экономия времени программиста на отладке, которая покрывает затраты на времени написания.

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

А когда вы в очередной раз услышите про глючность микрософт — вспомните, что глючность эта идет от схемы debug-release.

Посмотрите, например, как пишет Бйорн Струстрап — https://habrahabr.ru/company/pvs-studio/blog/270191/ Кусочек называется «This is the place for paranoia». Ну и цитата из Страстрапа " Я считаю, что все серьезные приложения должны использовать такой «параноидальный тест» для отлова «невозможных» ошибок."

НАОБОРОТ. Этот вариант применяется когда мы в том же обороте цикла делаем вычисления с вычисленной энтропией. То есть при исключении — мы будем иметь ссылку на строку кода сразу за вызовом подсчета энтропии.

Вы же предлагали поставить assert после цикла? А если assert ставить внутрь цикла, то не логичнее ли его разместить один раз внутрь вызываемой функции, а не писать однотипный код после её вызова?

Насчёт всего остального: логи дополняют систему с ассертами, а не заменяют, логи — для удалённого анализа кода. К тому же assert можно поставить в узкое место, а запись в лог — нет.

assert — это не средство проверки, это средство отладки. Грубо говоря, assert-ом проверяются условия, которые по логике никогда не должны случиться, но из-за бага могут.

Пример: проверка граничных условий при обращении к элементу массива. Можно сделать полную валидацию входных значений, но допустить баг либо в самом алгоритме, либо в проверке условий. Тогда assert здесь будет последним рубежом.
>А если assert ставить внутрь цикла,
А если прочесть то, что я пишу?

Исключение — это в данном случае не assert, это аппаратное прерывание FPU. см. https://ru.wikipedia.org/wiki/IEEE_754-2008 и fenv.h в POSIX. То есть пока исключение не сработало — потери производительности НОЛЬ по модулю.

assert внутри цикла ставят школьники, не надо меня к ним приравнивать.

> К тому же assert можно поставить в узкое место, а запись в лог — нет.
Опять сказки, сказки, сказки… нету никакой разницы между

assert(ptr == NULL)

и if (ptr == NULL) LogPrintf(.....)

Потери производительности — одинаковые, на проверку условия и переход.

> assert — это не средство проверки, это средство отладки.
Вы же сами признались, что пока у вас нету самого трудоемкого этапа жизненного цикла — сопровождения. Вы написали код — и выбросили его. Потому что его поддержка никому не нужна. А в серьезных системах — не так, сопровождение стоит 90% средств, потраченных на проект и развитие. Почитайте хотя бы Брукса https://ru.wikipedia.org/wiki/Мифический_человеко-месяц

> Тогда assert здесь будет последним рубежом.
Так вот, assert — это прежде всего средство сопровождения. Инструмент, локазующий ошибку. А за ним — идут многочисленные средства восстановления после ошибок… Ибо чем ближе к месту возникновения мы обнаружили ошибку — тем меньший урон она нанесла системе.

Хотите писать как микрософт — получите такое же глюкало, как у микрософта.

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

Продолжу аналогию. Порезали палец, получили assert (сигнал о боли). Пытаемся работать этим пальцем. 3 раза не получилось — работаем без этого пальца. Не вышло опять 3 раза подряд — отключаем кисть. Опять 3 раза не вышло — руку. И вот когда уже с разрушенной рукой не вышло — вот тогда умираем. То есть перезапускаемся или переходим на резервный сервер. Ну или умираем с багрепортом разработчику.

Но пока ваши программы никому всерьез не нужны — можете писать как хотите. Всерьез — это когда ваша ошибка стоит намного больше, чем заказать вам киллера. :-) Всерьез — это когда от вашей ошибки может погибнуть десяток людей. Всерьез — это когда ваша квартира покроет меньше 1% от ущерба.
> Исключение — это в данном случае не assert, это аппаратное прерывание FPU

Чем включение прерываний FPU отличается от включения ассертов?

> Опять сказки, сказки, сказки… нету никакой разницы между assert(ptr == NULL) и if (ptr == NULL) LogPrintf(.....)

Разница в условной компиляции
> Чем включение прерываний FPU отличается от включения ассертов?
Школьного учебника мало? Ну тогда https://ru.wikipedia.org/wiki/Прерывание

«В зависимости от источника возникновения сигнала прерывания делятся на:
синхронные, или внутренние — события в самом процессоре как результат нарушения каких-то условий при исполнении машинного кода: деление на ноль или переполнение стека, обращение к недопустимым адресам памяти или недопустимый код операции;»

Пока прерывания не произошло — инструкции выполняются с той же скоростью. При возникновении исключительной ситуации — возникает прерывание. Обработка прерывания — вещь довольно медленная, это несколько сотен команд. Ну примерно как максимальный элемент на массиве длиной 100 найти. Но это — в том редком случае, как исключение прошло.

assert — это все-таки оператор условного перехода. Микроскопическое торможение кода. Прерывания FPU работают без этого торможения. Аналогичное прерывание — обращение через указатель, равный NULL. При обращении *b — процессор тоже выдает прерывание, если указатель b равен NULL. Только это прерывание неотключаемое, а прерывание FPU по стандарту аппаратно включается и выключается

>Разница в условной компиляции
КОПЕЕЧНАЯ разница. Скомпилируйте debug вариант с дефайном NDEBUG и сравните скорость между двумя debug-вариантами. разница будет копейки. Аналогично можете скомпилировать release без NDEBUG — разница в скорости опять будет копейки. То есть сильно меньше 1%. Разница в 5-10% между release и debug — в оптимизации, а не в assert.

Если получите больше 0.1% — дело в 2-3 aasert, попавших в циклы. Вот ИХ — лучше исключить, заменив на менее трудоемкие варианты. Или под условную компиляцию, если они поставлены по делу.
А теперь поясняю: в большинстве современных языков программирования прерывания FPU недоступны by design. Стандарт IEEE 754 также не рекомендует их использовать.

Включая прерывания FPU, вы рискуете поломать существующий код, написанный в соответствии со стандартом. Но вам ничто не мешает включать прерывания для отладки кода.

Ситуация аналогична ассертам: в релиз-сборке не должно быть ни ассеров (=abort при ошибке), ни прерываний. Но для отладки оба инструмента хороши.
А если без сказок — читайте POSIX — http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/fenv.h.html
«The functionality described on this reference page is aligned with the ISO C standard. Any conflict between the requirements described here and the ISO C standard is unintentional. This volume of POSIX.1-2008 defers to the ISO C standard.»

> в большинстве современных языков программирования прерывания FPU недоступны by design.
ОК, назовите примеры языков, где 3.5/0. не вызовет исключения. :-)

Если говорить правильно — недоступно ОТКЛЮЧЕНИЕ прерываний FPU, то есть они всегда включены. В том числе — и реакция на +NaN и -NaN.

> Ситуация аналогична ассертам: в релиз-сборке не должно быть ни ассеров (=abort при ошибке), ни прерываний.
Это вы откуда такой бред взяли? Из вредных советов Остера?

ну как известный вам пример — Delphi и С++ Builder собраны с включенными ассертами. И иногда по ним падают.
Вот вам пример ассерта то ли в ODBC, то ли в excel — http://stackoverflow.com/questions/24945177/assertion-failed-getting-external-data-from-sql-server
А вот вам ассерт в AutoCAD — http://forums.autodesk.com/t5/design-review/adr-2011-assertion-failed-error-message/td-p/2744159/highlight/true
Вот windows media player — http://www.elektroda.pl/rtvforum/topic3073568.html

Ну есть такое в любой профессии. Иногда люди бояться, что неофиты наступят им на пятки. И чтобы остаться уникальными специалистами — дают заведомо вредные советы. Может не стоит такие книжки читать?

Или вы всерьез верите, что можно написать программу без ошибок?

> ОК, назовите примеры языков, где 3.5/0. не вызовет исключения. :-)
Java, C#
UFO just landed and posted this here
UFO just landed and posted this here
> Причём, это был quiet nan, то есть, никаких исключений и падений.
А это определяется режимом сопроцессора. см. http://pubs.opengroup.org/onlinepubs/9699919799/functions/fesetexceptflag.html

> А я ещё долго удивлялся, чего у меня после рефакторинга классификация сломалась (на самом деле не долго)
При моем подходе сначала будет понятно, где точка поломки, а уж потом — а что мы этим сломали и сломали ли вообще.

> Ну, на плюсах я при этом пишу лет с 12-13 :)
я даже системного программиста с дипломом литфака педиинститута видел. Ещё на ЕС-1022. Исключения разные бывают.

> Неа, только один раз для каждого набора индексов, к сожалению
А запись пары a и b — не сильно труднее записи индекса. Может от индексов вообще отказаться? Или этот набор индексов для чего-то нужен?

> Кстати, я тут переписал код с магией в [] вместо ифа, и получилось даже медленнее процента на три, хехе.
Оптимизатор мог примерно ту же магию применить. Иногда бывает, что он лучше оптимизирует, чем ручками.

Можно попытаться вывернуть алгоритм (как того пингвина в гвинпина). Сейчас у вас видимо последовательность циклов по объему памяти, превышающему кэш. А можно попытаться все загнать в один цикл — но больше будет процент попадания к кэш. Идея очень сырая, конечно.
UFO just landed and posted this here
> Экзепшоны упоминаются, но quiet или signaling nan (равно как и возможность их настройки вроде указанной вами) — нет.

См. https://en.wikipedia.org/wiki/IEEE_floating_point#Exception_handling
«Invalid operation (e.g., square root of a negative number) (returns qNaN by default).»
Так что нам нужен FE_INVALID

Пример есть тут — http://en.cppreference.com/w/c/numeric/fenv/feexceptflag

> равно как и возможность их настройки вроде указанной вами
УВЫ, Для управления режимами — fegetenv и далее уже определяется реализацией.

>не копировать же эти гигабайты строк матрицы туда-сюда всё время
ПОЧЕМУ? Если при копировании вы влезете в кэш — это быстрее.
Дополню каплю. Страховку в вашем примере дает как раз проверка b на NULL. В большинстве случаев — нулевым является this. Поэтому полезно в отдельных местах проверять, что this не NULL. И если NULL — кинуть исключение. А уж потом программер по логам найдет ошибку и исправит её.

Это как раз та проверка, что выкинута в новом gcc.
Те, кто используют множественное наследование — сами себе злобные буратино.

Но ваша идея понятна. Вы считаете, что надежность программ на С++ зависит только от ошибок программистов. А поскольку ошибки всегда есть — все программы на С++ ненадежны. То есть там, где нужна надежность — там С++ не место. ОК, понял.
this может принять значение nullptr в следующем случае:
class foo {
public:
    void bar() { assert(this == nullptr); }
};

int main() {
    foo *p = nullptr;
    p->bar();
}


В этом случае проверка на равенство nullptr имеет право даже не выполняться. Если мы вызвали метод экземпляра класса, считается, что указатель p содержит валидный адрес обьекта. Правильно было писать так:
class foo {
public:
    void bar() {}
};

int main() {
    foo *p = nullptr;
    if (p != nullptr)
        p->bar();
}
Вы так уверены, что сможете вставить ВСЕ проверки? Ну возьмите ну скажем код VCL (оконная библиотека Delphi), вставьте те проверки, что там нету, а я покажу, где реально может возникнуть NULL.

А если ваш собственный код больше 100 тысяч строк — предлагаю спор на миллион долларов. Подменяем new, чтобы оно рандомно выдавало исключение и проверяем, что ни в один метод не придет NULL вместо this. Если у вас хоть какая-то обработка исключений реализована — я выиграю.

В отличие от вас — я не считаю ни себе, ни своих сотрудников гениальными программистами. Поэтому знаю, что ошибки в наших программах есть. А надежность — она от механизмов противодействия ошибкам, а не от веры в собственную непогрешимость.
Ещё раз: если вы попали в метод экземпляра класса, то this != nullptr. Так должно быть по стандарту. И компилятор имеет право генерировать код исходя из этого условия. Если программист допустил преобразование nullptr к указателю на объект, а потом вызвал метод у этого объекта, то этот программист сам себе злобный Буратина.

А отлавливать такие вещи можно если gcc подать опцию -fsanitize=undefined. Это заставляет компилятор вставлять рантайм-проверки во все места, где может возможно undefined behavior. Естественно, это имеет смысл применять только в дебажных сборках.

Я не понимаю, зачем вы об этом спорите. Jef239 говорит о том, что


  1. Программисты делают ошибки.
  2. Специфика его сферы деятельности предполагает написание кода, максимально защищённого от сбоев, в том числе сбоев вследствие ошибок программистов.
  3. В результате ошибок программиста nullptr может попасть в this и это «штатная» нештатная ситуация, от которой нужно защищаться, потому что «ой, программист ССЗБ, что написал код, который при исключении в new использует nullptr как указатель на обьект» — это слабое утешение для ситуации «в результате программного сбоя потеряно N тысяч долларов [… M раз подряд]».
  4. Дебажная сборка не имеет смысла, т.к., во‐первых, она отличается от релизной, и, во‐вторых, в тестах все ситуации не предусмотришь, а необработанное UB вроде nullptr в this и крах всего приложения в релизе — большие убытки.

Короче, ССЗБ или нет, если от внезапного nullptr в this можно защититься, и на практике такое хотя бы изредка случается, то от него нужно защититься. А компилятор со своими изменениями в обработке UB вставляет палки в колёса.

> Так должно быть по стандарту.
Вы готовы поставить миллион долларов своих денег (или денег своей фирмы) на то, что компилятор всегда действует по стандарту и не имеет ошибок? Что все библиотеки не имеют ошибок? Что все ваши сотрудники пишут без ошибок?

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

> этот программист сам себе злобный Буратина.
да не себе, а ДРУГИМ!!! В том же gcc бывали вылеты из-за обращения к нулевому указателю.

> Это заставляет компилятор вставлять рантайм-проверки во все места, где может возможно undefined behavior.
Вы же понимаете, что лукавите? Не во ВСЕ, а лишь в ту небольшую часть, которая есть в исходном коде. Какой там размер libc? Миллионы строк? Ну вот и поймите, что в исходном коде у вас лишь несколько процентов. А все остальное — в объектниках.

Передача NULL как первичная ошибка программиста — это огромная редкость. Как вторичная ошибка, то есть при отработке исключения — уже чаще. Как третичная (исключение у вас + баг в библиотеке) — достаточно типична.

Типичный сценарий. Есть экранная форма — это класс из библиотеки. Она использует объекты, как библиотечные, так и написанные вами. В какой-то момент происходит ошибка, ну скажем new выдает исключение. Отрабатывает деструктор формы. И вызывает ваш класс с nil вместо this.

Вы можете долго орать, что разработчики библиотеки — сами для вас буратино. Можете кричать, что это UB. Можете ждать годами, когда они починят. Если проект опенсорсный — можете и сами исправить. И долго пропихивать исправление. Или править код в каждой новой версии. А можете — обойти ошибку. Ваш выбор?

Так вот, именно обходу ошибки и мешает новое поведение gcc.

Так вот, gcc
Когда вы на машине влетите в аварию — вы можете гордиться, что все сделали по ПДД. Вот только погибших этим не вернешь. :-( А можете — нарушить ПДД и обойтись без аварии.

Декабрь 1983года, Питер, проспект Смирнова. Ребенок перебегает дорогу прямо перед колесами. Водитель резко сворачивает на тротуар, умудряется не задеть никого на автобусной остановке и останавливается за 10 сантиметров от стены дома.

Грубое нарушение ПДД? Конечно.Вот только выжили ВСЕ. Ни синяков, ни травм, лишь валидола всем захотелось Даже ГАИ не вызывали.

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

В 99.99% — да, согласен с вами.


Но!


  1. В стандарте C++03 про удаление nullptr написано невнятно, однозначность про удаление nullptr появилась только в стандарте C++11
  2. Если оператор delete перегружен, то увы, даже gcc 6.1 с -std=c++17 не вставляет проверки на 0, и просто вызывает перегруженый оператор с 0, а clang, кстати, принудительно указатель на 0, перед вызовом перегруженного delete.

Вот пример, того что генерит gcc -std=c++17 -O3


operator delete(void*):
        rep ret
main:
        subq    $8, %rsp
        movl    $4, %esi
        xorl    %edi, %edi
        call    operator delete(void*, unsigned long)
        xorl    %eax, %eax
        addq    $8, %rsp
        ret

В итоге проверка указателя на 0 остается на плечах аллокатора, Который, в каком нибуть странном окружении может оказаться взрывоопасным.

GCC и не должен вставлять проверку — это задача того, кто написал кастомный operator delete.
if (this == 0)

В нормальном коде никогда этого не встретить
Надо вообще компилятору выдавать ошибку «пересмотрите свой код немедленно» при обнаружении такого

В те времена, когда оператор new возвращал 0 при нехватке памяти — такие проверки были нормальным защитным программированием.

а что мешало проверить указатель перед его разыменовыванием, а не после?
т.е.
MyType* p = new MyType;
if(p == 0)
  onMemoryLack();
else
  p->someMethod();

Если бы программисты никогда ничего не забывали — программы можно было бы вовсе не отлаживать :)

MyType* p = new (nothrow) MyType;

Иначе сравнивать с 0 нет смысла совсем.
такие проверки были нормальным защитным программированием

А в те времена стандарт это не считал UB?
видимо в те времена new не кидал исключений

В те времена считалось, что операторы в программе всегда выполняются последовательно, и одного тестового запуска под отладчиком достаточно, чтобы Undefined behavior превратился в Implementation-defined behavior.

так смысл любого предупреждения компилятора в том, чтобы программист немедленно пересмотрел свой код. Имо код должен в режиме -Wall -Wpedantic собираться без предупреждений
А люди должны платить налоги и соблюдать пдд.
Интересно, есть ли хоть один более-менее большой проект (>10.000loc, к примеру), который собирается без предупреждений.
Тем не менее к этому надо стремиться. В особо грустных случаях (например, предупреждения во внешних модулях) ворнинги отключаются для отдельных модулей/подпроектов/файлов.
Типичный сценарий — «Написали кучу кода, он компилировался без ворнингов. Перешли на новую версию компилятора, высыпалось более 9000 ворнингов. Править кучу кода некогда, просто отключили конкретные ворнинги для конкретных файлов.»
Если у проекта долгая история, то этот сценарий повторяется многократно, и в итоге из каждого файла ворнинги сыплются как из рога изобилия, так что -Wall -Wpedantic на уровне проекта становится бессмысленным.
да, сценарий популярный и не особо приятный. Однако если подумать, откуда могут взяться «новые» ворнинги:
а. компонент стал deprecated — наверное, его изначально не стоило использовать
б. было использовано доколе «безопасное в силу реализации» ub, а теперь компилятор может сломать код оптимизациями (как в описанном в статье случае). Придется как минимум тестировать, высоковероятно — править код.
в. новый компилятор более умный и подсвечивает подозрительные места. Наверное, стоит глянуть
г. была использована нелегальная с точки зрения языка конструкция, которую старый компилятор почему-то пропускал (например, gcc 4.8 пропускает неявный каст значения одного enum class в другой). Еще более поздний компилятор может на этом месте нарисовать ошибку.

Выходит, что игнорировать ворнинги при переходе на новый компилятор — не такая уж и замечательная идея

в) иногда звучит как «компилятору добавили забагованную диагностику». Например, «компилятор начал видеть -Wconversion там, где отродясь такого не было», Вот, например: https://github.com/neovim/neovim/commit/82934e8797651b934569ba77bd9fd6d8f75e87e6: здесь все kSD* — положительные константы, определённые в enum. Или, ещё хуже: https://github.com/neovim/neovim/commit/82934e8797651b934569ba77bd9fd6d8f75e87e6: заметьте, каст к size_t уже есть после =, но GCC потребовался ещё. Где именно вы получите -Wconversion зависит от версии GCC, какие‐то версии более адекватны.


Clang гораздо лучше: я не припомню, чтобы он показывал -Wconversion там, где я с ним не согласен, с GCC для собственного спокойствия (CI отрабатывает не мгновенно, а моя версия GCC имеет другое мнение) иногда приходится расставлять кучу бессмысленных кастов: в первом случае вполне можно доказать, что любой возможный результат влезет в unsigned (но здесь хотя бы один каст около присваивания), во втором складываются целочисленная константа, булевы выражения (которые всегда будут 0 или 1) и size_t — зачем здесь предупреждать о -Wconversion, даже если по стандарту булевы выражения — это int?

Емнип, по умолчанию для енумов выбирается signed тип минимального необходимого размера чтобы вместить все значения. Вы можете указать underlying type enum'a явно, и тогда касты не потребуют конверсии.

Булевы выражения в с++ не являются интами, но определен неявный promotion bool -> int. Так или иначе, если вы не согласны с GCC-шным -Wconversion, (или другой «забагованной» диагностикой) то, как я уже сказал, можете отключить её отдельно, например: -Wno-conversion
Я вообще выступаю за то, чтобы неявная конвертация bool <-> int была невозможна, но это поломает кучу старого кода.

В коде значения из enum используются только в качестве констант, сам тип используется только в документации. Clang это видит и у него никакого -Wconversion. GCC в некоторых версиях нет. В общем, прочитав стандарт и предупреждение компилятора можно понять, откуда предупреждение там взялось — конкретно эти случаи являются скорее более строгим следованием стандарту, единственный баг(?) в -Wconversion когда я так и не смог понять, откуда предупреждение выкопалось — это -Wconversion на *p++ = (ch == NL ? NUL : ch);, где NL и NUL — константы вида '\n', ch — const char, p — char *: https://github.com/neovim/neovim/commit/c27395ddc84952b94118de94af4c33f56f6beca5. Но для меня эти поведения равнозначны — в первую очередь потому, что clang видит, что никаких проблем не предвидится, а GCC ругается. Вторая причина считать это именно ошибкой: часто мой gcc и gcc с travis не ругаются, а gcc с QB — да, что переводит срабатывающую диагностику в разряд ошибок.


В C++ булевы выражения, может, и не int, но C99 явно говорит, что операторы сравнения дают int.


Отключить, конечно, можно, но ветка‐то о том, что «новые» предупреждения лучше не игнорировать. Я в целом согласен, но говорю о том, что в реальности есть и случаи, когда новое предупреждение не будет говорить об ошибке или просто потенциально опасном месте в коде. Вот работало у вас приложение с -Wconversion и без предупреждений, переехали на другую версию (не знаю, у кого gcc новее — моя машина, QB или travis; вроде различные предупреждения появлялись/исчезали при движении по версиям в любую сторону) и, внезапно, -Wconversion.

у меня такое возникало из-за того что пришлось дублировать енум класс — значения нового не инициализировались значениями предыдущего без явного приведения к инту (в gcc 6.1, 4.8/5.3 пропускали). И по стандарту такое поведение является корректным. В конце концов, enum class для того и придумали, чтобы нельзя было вместо его значений подставлять инты.
В C++ булевы выражения, может, и не int, но C99 явно говорит, что операторы сравнения дают int.

ну так там булей нет
знаю проект мморпг, в котором /W4 и «Treat warnings as errors».
Они, конечно же, есть.
Например, Chromium: там warning == error в коде самого Хромиума, для third_party могут быть исключения.
А кода там не 10К, а гораздо больше.

Предупреждения сделанные при помощи #warning как замена всяким TODO считаются? :)


Не, конечно я согласен, что TODO всякое должно быть заменено на тикет в системе учёта. Но бывают вещи вроде "сделать более элегантно", а по тикету тебя ещё менеджмент постоянно дёргать будет или закроют нафиг.

Есть. И что из этого?
https://gcc.gnu.org/gcc-6/changes.html

The default mode for C++ is now -std=gnu++14 instead of -std=gnu++98.


Т.е. по новому стандарту это UB, а как следствие, компилятор может выпиливать подобные проверки.

А добавление скобок, для подавления предупреждений — стандартная практика.
В защиту программистов скажу, что на самом деле такой код встречается редко. Т.е. такие ошибки однозначно есть, но их не так уж много, относительно ошибок других типов.

P.S. Покупайте наших слонов. Используйте анализатор PVS-Studio для защиты от этой и многих других проблем. :)

Зато там, где он встречается — на нем все построено :)

Ещё бы кто-нибудь объяснил мне, зачем такой код было писать… Ну, кто-то же это придумал, у него же были какие-то мотивы, кроме мизантропии?
Историческая справка. В достандартную эпоху это вообще было нормально. :) Более того, не только сравнивали this с 0, но и меняли его. См. главу «Интересные наблюдения» в статье "К тридцатилетию первого C++ компилятора: ищем ошибки в Cfront".
Не следует писать код, приводящий к undefined behavior. Собственно, в чём смысл этой статьи? Показать какой GCC плохой — считает что в пользовательском коде никогда не встретится UB? Ну так он по стандарту должен это делать.

Кстати, какая альтернативно одарённая личность могла догадаться использовать в продакшене GCC с минорным номером версии меньше двойки?

Увольнять надо программистов, которые "беззаботно пишут" такой новый код на C++!
Этот милый код может ломаться с любой версией gcc


Как вы думаете, что выведет этот снипет? :)


#include <stdio.h>

class A {
public:
        int dummy;
};

class B : virtual public A {
public:
        void show_this () {
            if ( this != 0 )
                printf ("It's ok. this is not null, it is=%p :)\n",this);
        }
};

class C : virtual public A {};
class D : public C, public B {};

int main (int argc, char **argv) {
        D *d = nullptr;
        d->show_this ();
}
Достаточно уволить любителей множественного наследования не по делу.

Удивительно кривая конструкция по сравнению с аналогом в JAVA. В java мы можем придумать интерфейс. Ну скажем для списка — количество элементов, взять элемент по индексу. добавить-удалить элемент. А потом, на произвольном уровне иерархии добавить нужные методы в класс и сказать, что класс реализует этот интерфейс. И всё, никакого множественного наследования уже не нужно. Аналогично, но менее элегантно можно и в дельфи.
Сразу видно человека, который не знаком с тем, как интерфейсы реализованы на низком уровне.
Интерфейс — это абстрактный класс без полей + немного кодогенерации.
На низком уровне интерфейс — это «вторая» vtable (таблица виртуальных методов). По слухам в JAVA используется формат vtable от COM, так что физически есть одна vTable, являющаяся конкатенацией всех нужны vTable. Кроме того, если этот слух верен — то в каждой vTable есть три дополнительных метода.

Кстати, статические поля-константы в интерфейсе возможны.

Отличия интерфейсов от классов — в том, что расширение интерфейса в COM-модели — это не наследование в модели класса. При наследовании мы дополняем vTable новыми методами, а при расширении — создаем новую vTable.

Если у нас было 3 метода и мы расширили интерфейс ещё одним методом, то получим 3+4=7 строк в vTable. С учетом скрытых трех методов — это будет (3+3)+(4+3)= 13 строк. А при наследовании 3+1 — 4 строки.

Связано это с тем, что класс мы всегда можем привести от предка к потомку. А вот интерфейсы COM это не всегда позволяют. Поэтому QueryInterface у каждого интерфейса свой.
При наследовании мы дополняем vTable новыми методами, а при расширении — создаем новую vTable.

Так оно и есть. Но основная причина создания второй vTable — это наличие полей, а не различия в логике дополнения/расширения.

Если в классе есть поля, то мы вынуждены создавать копию vtable в каждом объекте. Если же полей нет, то нет необходимости её дублировать — достаточно разместить её как часть основного vtable. Именно по такому принципу и работают некоторые компиляторы.
vTable не создается в экземплярах объектов. vTable находится в классе. Если у нас есть 10 объектов класса TABC, то каждый объект имеет все поля, но ссылку на единую vTable.

Если речь о статических полях (поля класса) — то в интерфейсах есть только константы. А с обычные статические поля должны быть в общими и для базового класса и для его наследников. То есть обычно реализуются как глобальные переменные со сложным именем.
vTable не создается в экземплярах объектов.

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

Статические поля, понятное дело, вообще никак не участвуют в полиморфизме и ни на ято не влияют.
> При множественном наследовании (C++) и наличии полей таки приходится создавать vtable в экземплярах классов
ЗАЧЕМ? Зачем хранить дублирующуюся константную информацию, когда можно хранить ссылку? Понятно, что при множественном наследовании одной ссылкой не обойтись из-за необходимости обеспечить приведение ко всем базовым классам. Ссылок будет по числу классов, это понятно.

Если есть какие-то ещё тонкости — прошу пояснений. В моих компиляторах ООП не было. :-)
Зачем хранить дублирующуюся константную информацию, когда можно хранить ссылку?

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

И vtable при множественном наследовании, кстати, будут разные в зависимости от смещения класса.
> Передаваемый указатель содержит vtable
ЧТО? Указатель содержит адрес. Если это указатель на метод конкретного экземпляра — то два адреса (this и адрес метода).

Можете перевести ваше высказывание на понятный язык?
Указатель содержит адрес объекта, передаваемого функции.

Вот структура типичного объекта в памяти:
[vtable][fields]

Адрес этой структуры и передаётся в функции.

Вот структура объекта с множественным наследованием:
[vtable1][fields1][vtable2][fields2]


Объединить два vtable в один:
[vtable12][fields1][fields2]

невозможно, т.к. непонятно, как взять адрес от второго объекта.

С другой стороны, если у объекта нет полей, то указатель будет указывать на объект, содержащий только vtable. Соответственно, нет нужды размещать vtable в каждом объекте, когда можно сделать его глобальным.
> Вот структура типичного объекта в памяти:
ОШИБАЕТЕСЬ. Вместо самой vTable в объекте указатель на неё. Это вы книги для чайников читали, там действительно иногда так рассказывают.

Для проверки — посмотри sizeof объекта. Потом добавьте туда новый виртуальный метод. И посмотрите, изменился ли размер самого объекта.

> Объединить два vtable в один:[vtable12][fields1][fields2] невозможно, т.к. непонятно, как взять адрес от второго объекта.
C этим почти согласен. Там две ссылки. Просто иногда объединенная таблица идет одним куском памяти, а вторая ссылки ведет на её середину. Можно это трактовать как одну таблицу. можно — как две подряд.
ОШИБАЕТЕСЬ. Вместо самой vTable в объекте указатель на неё. Это вы книги для чайников читали, там действительно иногда так рассказывают.

Видимо, надо действительно быть чайником, чтобы нормально объяснять :)

Для меня настолько очевидно, что [vtable] — это указатель на таблицу, а не сама таблица, что по-другому быть и не может.

C этим почти согласен. Там две ссылки.

Именно это и я хотел объяснить. Есть поля во втором объекте — придётся вставлять вторую ссылку на таблицу, тратя память на каждый объект. Нет полей — память тратить не нужно.
> Для меня настолько очевидно, что [vtable] — это указатель на таблицу, а не сама таблица,
> Соответственно, нет нужды размещать vtable в каждом объекте, когда можно сделать его глобальным.

Сколько вас под одним ником пишет? :-) vTable — всегда глобальный, общий для всего класса. Одна там секция или несколько — не важно.

> Нет полей — память тратить не нужно.
Опять ошибка. Передаем указатель на производный объект в процедуру, которой нужен второй базовой класс. И эта процедура — хочет вызвать виртуальный метод. Под каким номером она будет его брать из vTable? под тем, что в базовом классе. А для первого базового класс? Да тоже самое. А для третьего? Опять то же

Так что тратить память на указатель на вторую vTable придется. Ну кроме ситуации, когда объект не виден извне модуля, а модуле — ко второму базовому классу не приводится. Но тут и vTtable не нужна и так понятно, какой метод вызывать.
Опять ошибка. Передаем указатель на производный объект в процедуру, которой нужен второй базовой класс. И эта процедура — хочет вызвать виртуальный метод. Под каким номером она будет его брать из vTable?

Она будет его брать из vtable для второго объекта. Просто указатель на vtable второго объекта будет храниться не как поле объекта, а как поле основного vtable, который у нас в единственном экземпляре.
this — это указатель на объект. **this — это vTable (обычно делают так для ускорения).
Вы хотите сказать, что в качестве this в базовый класс передастся указатель на " поле основного vtable, который у нас в единственном экземпляре".

Делаем 2 объекта производного класса. Преобразуем их ко второму базовому классу. Для обоих — получится одинаковое значение this — указатель на " поле основного vtable, который у нас в единственном экземпляре"
1) Как реализовать сравнение объектов на == и !=???
2) Как реализовать приведение от второго базового класса к исходному объекту?

Проверьте свою идею на компиляторе.
1) Если вы получите, что все this одинаковы — вы правы.
2) Если вы получите, что при добавлении второго базового класса sizeof не увеличился — вы правы.

По мне — так ОЧЕНЬ сомнительно. ОЧЕНЬ. Хотя из такого, что сравнение объектов классов на == должно работать.
Как реализовать сравнение объектов на == и !=???

При множественном наследовании адреса объектов и не будут совпадать — это нормально.
ЕЩЁ РАЗ. Сколько вас под одним ником пишет? Вы пишете взаимоисключающие вещи. ПОЧЕМУ?

> указатель на vtable второго объекта будет храниться не как поле объекта, а как поле основного vtable, который у нас в единственном экземпляре
> При множественном наследовании адреса объектов и не будут совпадать

ОДНОВРЕМЕННО оба высказывания не могут быть истинными. ИЛИ одно — ИЛИ другое.

ЕЩЁ РАЗ. Речь о ситуации, когда вы привели объект ко второму базовому классу и передали его в процедуру, которая знает только про второй базовый класс. ЧЕМУ будет равен адрес объекта в этой процедуре?
ОДНОВРЕМЕННО оба высказывания не могут быть истинными. ИЛИ одно — ИЛИ другое.

Могут, потому что это зависит от компилятора.

ЕЩЁ РАЗ. Речь о ситуации, когда вы привели объект ко второму базовому классу и передали его в процедуру, которая знает только про второй базовый класс. ЧЕМУ будет равен адрес объекта в этой процедуре?


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

Java, C# и Delphi принципиально разделяют классы и интерфейсы (схема один базовый класс + много интерфейсов). В них указатели на vtable интерфейсов размещаются в основной vtable, поэтому адреса объектов и интерфейсов будут совпадать.

В C++ же классы и интерфейсы не разделяются. Единственный признак — наличие или отсутствие полей, а дальше всё ложится на усмотрение компилятора.

Visual C++ и GCC не будут реализовать класс без полей специальным образом и при любом раскладе заведут второй указатель на vtable в объекте, при этом адреса при использовании подобных «интерфейсов» не будут совпадать.

А вот C++ Builder определяет в процессе компиляции логику работы (класс/интерфейс) и генерит разное размещение для случая классов и интерфейсов. Там адреса объектов совпадут, если используются только интерфейсы.
> Могут, потому что это зависит от компилятора.
Ох уж эти сказки, ох уж эти сказочники… Вы сколько компиляторов сами написали? Компилятор — такая же программа, не сложнее других. Несколько штук у меня написано. Некоторые вещи просто быстрее выполняются, если их компилировать.

Объяснить на уровне третьеклассника?

Есть базовый класс B с виртуальными методами. Есть процедура AnalyzeB(B *b1, B *b2). Она взывает виртуальные методы и сравнивает b1 c b2. Эта процедура находится в отдельном модуле и ничего не знает о производных классах. Она берет указатель на объект, самым стандартным образом получает из него vTable и по нему делает вызов виртуальных методов. Очевидно, что в объектах класса B есть ссылка на vTable.

Аналогично есть базовый класс A и процедура AnalyzeA(A *a1, A *a2). Она тоже в отдельном модуле.

Теперь берем класс AB, образованный от классов А и B. Делает объекты AB ab1, ab2; Передаем их в AnalyzeB. Как вы писали " адреса объектов и не будут совпадать", ибо иначе откажется работать сравнение на равенство. С другой стороны, AnalyzeB ничего не знает про AB. Она хочет по *b1 и *b2 получить их vTable.

Иными словами, приведение от AB к B должно выдать уникальный указатель, но по которому можно перейти к vTable ТИПОВЫМ способом. Поскольку в объектах класса B содержится ссылка на vTable то и этот указатель должен указывать на какую-то структуру, уникальную для каждого экземпляра (требование сравнения) и содержащую ссылку на vTable.

> указатель на vtable второго объекта будет храниться не как поле объекта, а как поле основного vtable, который у нас в единственном экземпляре
Видите, что не выходит? Если в AnalyzeB мы передадим «поле основного vtable», то b1 будет равно b2. Если передаем разные — значит нам размещать ссылку на vTable класса B внутри объекта.

А ещё есть такая штука, как приведение B* к AB*, то есть от базового класса к производному. Которое тоже должно работать.

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

Впрочем, есть ещё RTTI (оператор typeid) для которого даже у класса без виртуальных методов делается ссылка на vTable. Потому что информация RTTI всегда хранится вместе с VTable.

> Java, C# и Delphi принципиально разделяют классы и интерфейсы
Если НЕ РАЗБИРАЕТЕСЬ — СПРОСИТЕ. В дельфи (и в C Builder) intrerface — это не класс, это интерфейс COM. Абсолютно отдельная штука. Запомните — дельфийский интерфейс — это НЕ КЛАСС. Наследование интерфейсом — это не наследование классов. Читайте https://ru.wikipedia.org/wiki/Component_Object_Model чтобы узнать про COM.

Про С# просто не знаю, а интерфейсы в java — ближе к классам. Хотя, по некоторым данным, реализуются именно как COM.

> А вот C++ Builder определяет в процессе компиляции логику работы (класс/интерфейс) и генерит разное размещение для случая классов и интерфейсов.
Ах вот что вас смутило? Интерфейсы в C++ builder — это COM, это не класс.

Опять на уровне третьеклассника надо?
Тогда правило такое. Все должно быть ОДНОТИПНО. Тот кто работает с базовым классом — ждет, что в объекте есть ссылка на vTable c адресами методов и RTTI. Тот кто работает с COM — должен уметь по адресу интерфейса получить доступ к методу стандартным для COM- способом.

В реальности там чуть сложнее (могу глянуть, как именно), но на уровне новичка — оно примерно так. А дельфи (и его сишный брат-близнец С++ Builder) действительно сделали все, чтобы скрыть сущность интерфейсов. На простых примерах — можно даже забыть, что это COM-интерфейс.

Иными словами, приведение от AB к B должно выдать уникальный указатель, но по которому можно перейти к vTable ТИПОВЫМ способом.

Именно поэтому размещение в памяти при множественном наследовании обычно выглядит так, и мы оба это понимаем:

[vtableA*][A][vtableB*][B]

Метод, принимающий на вход A* и B*, будет получать разные адреса. Он не знает и не должен знать, является ли объект B частью чего-то или нет, отсюда и необходимость хранения указателя на vtable для каждого базового класса при множественном наследовании. Чуть интереснее обстоит дело при виртуальном наследовании — там добавляется ещё поле для хранения смещения, и оно тоже ест память.

А ещё есть такая штука, как приведение B* к AB*, то есть от базового класса к производному. Которое тоже должно работать.

И оно работает, но только с помощью dynamic_cast, которое шерстит vtable, а не простым преобразованием типа указателя.

Впрочем, есть ещё RTTI (оператор typeid) для которого даже у класса без виртуальных методов делается ссылка на vTable.

Для класса без виртуальных методов в C++ оно обрабатывается на этапе компиляции, а не выполнения.

В дельфи (и в C Builder) interface — это не класс, это интерфейс COM

Кто вам такое рассказал? В Object Pascal интерфейс играет ту же роль, что и интерфейсы в C# и Java. Это языковое средство.
Синтаксис интерфейсов в C++ Builder соответствует обычному множественному наследованию.

Интерфейсом COM он становится только после прописывания атрибута (GUID) и соответствующей кодогенерации компилятором под конкретную операционную систему.
Ещё есть Delphi под Linux, там тоже есть COM-объекты?
> И оно работает, но только с помощью dynamic_cast, которое шерстит vtable,
не надо путать vTable c RTTI. Это чуть разные вещи. Читайте https://ru.wikipedia.org/wiki/RTTI

> Для класса без виртуальных методов в C++ оно обрабатывается на этапе компиляции
ДА НУ? Вы хотите сказать, что если класс без виртуальных методов привести к базовому классу, то у него typeid будет равен базовому классу, а не наследнику? Ну хоть MSDN почитайте.

https://msdn.microsoft.com/ru-ru/library/fyf39xec.aspx
«Если выражение указывает на тип базового класса, а объект, фактически, является типом, извлеченным из базового класса, результатом является ссылка type_info для производного класса»

> Кто вам такое рассказал? В Object Pascal интерфейс играет ту же роль, что и интерфейсы в C# и Java. Это языковое средство.
Я вам уже давал ссылку. почитайте. Или в help слазайте.

> Синтаксис интерфейсов в C++ Builder соответствует обычному множественному наследованию.
Так это СИНТАКСИС. А не семантика. Семантика там совсем другая. Ну хоть https://habrahabr.ru/post/181107/ почитайте, там человек на типичную ошибку налетел.

А единый синтаксис — это МАРКЕТИНГОВАЯ фишка. В своей время Borland очень гордился тем, что смог впихнуть COM в синтаксис классов. И что интерфейсы писать якобы так же просто, как классы. ну см. налет по ссылке выше, чтобы понять, что не все так идеально как в рекламе.

> Интерфейсом COM он становится только после прописывания атрибута (GUID)
Опять не правы. Вы почитайте https://habrahabr.ru/post/181107/ — там без всяких GUID человек на неприятную особенность интерфейсов нарвался.

> Ещё есть Delphi под Linux, там тоже есть COM-объекты?
Как минимум там есть GUID в интерфейсах — http://www.codenet.ru/progr/delphi/kylix/
Сам я Kylix не крутил, но скорее всего — там сохранена COM-архитектура интерфейсов. Просто она ограничена приложением + dll, написанными на том же kylix — инфраструктуры-то для запуска других приложений нет.

Ещё есть https://ru.wikipedia.org/wiki/Free_Pascal Вроде как там тоже COM. По крайней мере хвастаются совместимостью, а её без COM не сделать.
не надо путать vTable c RTTI.

Необходимое условие работы RTTI — наличие vtable. Собственно, по vtable однозначно определяется класс, а дальше дело техники.

ДА НУ? Вы хотите сказать, что если класс без виртуальных методов привести к базовому классу, то у него typeid будет равен базовому классу, а не наследнику?

Да. Именно так описано и в стандарте, и в MSDN (читайте на английском, русский перевод ужасно корявый):

The expression must point to a polymorphic type (a class with virtual functions).Otherwise, the result is the type_info for the static class referred to in the expression.

Так это СИНТАКСИС. А не семантика. Семантика там совсем другая.

Вот именно к этому я и шёл. Если я возьму кусок кода на C++ с множественным наследованием интерфейсов, то представление объекта в памяти после компиляции GCC и C++ Builder будет различным. Ну да, C++ Builder сделает интерфейс COM-совместимым, но для меня этот факт не имеет никакого значения.
> Необходимое условие работы RTTI — наличие vtable.
НЕТ. RTTI есть у классов без vTable. собственно см. вашу же цитату: " the result is the type_info for the static class referred to in the expression". Другое дело, наличие в объекте класса УКАЗАТЕЛЯ на RTTI. Действительно, там обычно общий указатель с vTable. То есть в одну сторону растет vTable, а в другую RTTI, а указатель указывает на границу между ними. Но это как принято делать. Стандарты вроде не ограничивают реализацию общим указателем.

> Да. Именно так описано и в стандарте, и в MSDN
ВЫ ПРАВЫ. Но существование RTTI у класса без полиморфных методов ваша цитата подтверждает.

> Если я возьму кусок кода на C++ с множественным наследованием интерфейсов, то представление объекта в памяти после компиляции GCC и C++ Builder будет различным.
GCC просто не скомпилирует интерфейсы.

> Ну да, C++ Builder сделает интерфейс COM-совместимым, но для меня этот факт не имеет никакого значения.
ОШИБАЕТЕСЬ. Ещё раз, прочтите https://habrahabr.ru/post/181107/

// Изначально Intf.RefCount = 0, это нормальное состояние для TInterfacedObject
// Интерфейс Intf заходит в область видимости процедуры Kill
// Выполняется Intf._AddRef, теперь RefCount = 1
procedure TForm1.Kill(Intf: IMyIntf);
begin
Intf.TestMessage;

// Интерфейс выходит из области видимости, выполняется Intf._Release
// И, так как RefCount стал равень нулю, объект уничтожается: TMyClass.Destroy
// Это и становится причиной того, что дальше все идет не так, как ожидалось.
// Дальнейшая работа с этим классом невозможна.
end;

Вот такого кода достаточно, чтобы вызвать деструктор у класса. И ровно то же самое — на С++. Это и есть отличие в семантике.

НЕТ. RTTI есть у классов без vTable.

Ну не умею я выражаться так, чтобы не цеплялись к словам. Хорошо, напишу так: корректная работа RTTI (возврат реального типа) требует наличия vtable у объекта.

GCC просто не скомпилирует интерфейсы.

Почему? C++ Builder позволяет объявлять интерфейсы как классы C++:

You can declare a class that represents an interface just like any other C++ class.

Ref: http://docwiki.embarcadero.com/RADStudio/Seattle/en/Inheritance_and_Interfaces#Declaring_Interface_Classes

ОШИБАЕТЕСЬ. Ещё раз, прочтите https://habrahabr.ru/post/181107/

Что мне мешает использовать интерфейсы без счётчика ссылок? Т.е. не использовать TObject и IUnknown в качестве базовых. Другое дело, что без счётчика ссылок действительно будет плохо.
> корректная работа RTTI (возврат реального типа) требует наличия vtable у объекта.
То же не так. У указателей нету vTable, но есть RTTTI. Просто статический. :-)

> Ну не умею я выражаться так, чтобы не цеплялись к словам.
Нас в 9ом классе этому на физике учили — как давать определения. В качестве тяжелой задачи — попробуйте дать определение стола и стула.

>Почему? C++ Builder позволяет объявлять интерфейсы как классы C++:
Да, похоже вы правы. я то с интерфейсами возился в дельфи, а не в C Builder.

> Что мне мешает использовать интерфейсы без счётчика ссылок?
Семантика! Несколько неточное описание:
1) При копировании указателя на интерфейс вызывается AddRef
2) В качестве деструктора указателя на интерфейс вызывается Release

> Т.е. не использовать TObject и IUnknown в качестве базовых.
Судя по вашей ссылке, единственное отличие интерейесов — это наследование от IUnknown.

Но меня сильно смущает «Typically, when declaring an interface class, C++Builder code also declares a corresponding DelphiInterface class that makes working with the interface more convenient:»

Мда, не возился я с интерфейсами в C Builder (только в дельфи). Похоже, что DelphiInterface — это действительно интерфейсы COM, а вот в классах — обычное множественное наследование.

Тогда вы во многом были правы.

> В них указатели на vtable интерфейсов размещаются в основной vtable, поэтому адреса объектов и интерфейсов будут совпадать.
И опять вы не совсем правы. Интерфейс приводится к классу, поэтому обнаружить отличие сложно. Но оно есть.
http://dit.isuct.ru/content/view/138/55/ — посмотри на рисунок в разделе 6.15

Это нужно для того, чтобы передать интерфейс в dll, работающую с COM-объектами. Ну или наоборот, работать с интерфейсом, который дает dll (или EXE), например с тем же word.

Поэтому в классе два указателя — один на VMT (vTable для сишников), а второй на IMT. Просто IMT обычно общий для все реализуемых интерфейсов. Не помню, можно ли там прямо прописать dispid (номер метода), но если можно — то придется делать в классе несколько IMT.
Мне кажется, что дальнейшее обсуждение этого не имеет никакого значения.

Важно просто понимать, что нет единого стандарта низкоуровневой реализации механизмов объектно-ориентированного программирования. В разных языках и/или при использовании различных компиляторов представление объектов в памяти может быть различным.

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

До тех пор. пока вы привязаны к конкретному компилятору и конкретной железке, вы можете писать код, учитывая особенности конкретного компилятора и архитектуры. Если же вы хотите иметь переносимый код, то придётся отказаться от всего, что выходит за рамки стандарта языка.
> Мне кажется, что дальнейшее обсуждение этого не имеет никакого значения.
На вашем уровне понимания механизмов компиляции — да, не имеет. С авторами компиляторов вполне можно это обсудить.

>В разных языках и/или при использовании различных компиляторов представление объектов в памяти может быть различным.
Только когда дело не касается COM. Вот для COM-интерфесов — все как в стандартах микрософт описано.

> Если же вы хотите иметь переносимый код, то придётся отказаться от всего, что выходит за рамки стандарта языка.
«Ох уж эти сказки, ох уж эти сказочники....» (с) падал прошлогодний снег. «Давайте спорить о вкусе устриц с теми, кто их ел» (с) Жванецкий

На скольких процессорах работает ваш код? На скольких операционках? На скольких компиляторах?
У нас:
8086, IA32, ARM. SPARC, планируется IA64
MS-DOS, Windows, linux+МСВС, FreeBSD, QNX. скоро будет FreeRTOS
Borland С++, C++ Builder, gcc, clang, lcc, VC++.

Если у вас зоопарк меньше — давайте уж я вам прочту лекцию про переносимость, а не вы мне. Потому что я этой переносимостью 25 лет занимаюсь.

Стандарты — дело десятое. Не так важно, что в них. Важно — что реализовали авторы компиляторов. Была в свое время замечательная книжка по переносу программы на FORTRAN-66 между различными ОС на PDP-11. Все компиляторы — одной фирмы, стандарт — один и тот же, а вот прочтения этого стандарта — РАЗНЫЕ.

Основное — это создать слой совместимости. На макросах, на процедурах-обертках, о одинаково выполняемый на всем зоопарке. И выработать свой набор конструкций, одинаково работающий на всем зоопарке компиляторов с разницей в выпуске в 25 лет.

Вот вы в другом посте предлагали alignas. Да, стандарт. Только в большинстве используемых компиляторов его нету. А заменить компилятор в МСВС нельзя, потому что это военная операционка. Там по правилам — иди свой собственный код — или то, что входит в состав ОС. Поэтому вместо этого #pragma pack в разных её вариантах. И __attribute__ и __declspec(align(1)).

Или взять new. хорошая штука, стандартная. Только нету во FreeRTOS кучи. И не будет — там и так памяти мало.

Или printf, мощный процедура, стандартная, огромная… на мелкий микропроцессор — просто не влезает. Так что там используется упрощенный вариант. Он нестандартен, зато РАБОТАЕТ.

Так что стандарты и совместимое подмножество — вещи очень разные. Вон, стандартный Паскаль вообще не был на персоналке реализован. Get и Put выкинули из языка вообще все. И ничего, никому это не помешало. Ну кроме школьников, конечно.
Разные бывают задачи, разные. Если нет нужды писать код, совместимый со старыми компиляторами, то и не надо этого делать. Сейчас с поддержкой стандартов дела обстоят чуть лучше.

И выработать свой набор конструкций, одинаково работающий на всем зоопарке компиляторов с разницей в выпуске в 25 лет.

У меня возникало такое ощущение, когда я изучал файлы STL или Boost. Впрочем, и в них в последних версиях совместимость со старыми компиляторами совсем поломана.

Если у вас зоопарк меньше — давайте уж я вам прочту лекцию про переносимость, а не вы мне.

Реквестирую увлекательный пост на эту тему — было бы интересно.
> Сейчас с поддержкой стандартов дела обстоят чуть лучше.
Именно, что ЧУТЬ. Команда разработчиков компиляторов с АЛГОЛА-68 опытным путем выяснила, что они используют 30% возможностей языка. Команда эта занималась портирование компилятора на разные военные процессоры, а компилятор был написан на том же алголе-68. То есть на этапе, когда сам компилятор компилируется и работает — 70% языковых возможностей ещё не проверено.Тесты помогают, но не сильно.

Так что в простых случаях — все работает стандартно. А вот сложные комбинации — не всегда.

Стандарты хороши, когда прошло уже лет 10 и все споры о реализации закончились. Ну скажем С99 и С++98 — достаточно стандартны. POSIX.1-2008 — тоже вполне стандартен. А вот С++11… думаю, что можно нарыть немало примеров, когда реализации отличаются достаточно сильно из-за разного прочтения стандартов.

> Реквестирую увлекательный пост на эту тему — было бы интересно.
я не считаю себя эталоном в переносе. Так что увлекательного поста не получится, а будет нудное описание прописных истин. :-( Ну в смысле прописных для тех, кто уже занимался переносом. Собственно все просто — пишем под один компилятор, потом переносим под другой, потом под третий. Под остальные — уже переносить просто.

В качестве байки. В FORTRAN-66 оператор END в конце программы определялся как «буквы E, N, D следующие в указанном порядке. Это чтобы дать возможность в последней перфокарте указать конец файла не только компилятору, но и ОС

Стандарты — дело десятое. Не так важно, что в них. Важно — что реализовали авторы компиляторов. Была в >свое время замечательная книжка по переносу программы на FORTRAN-66 между различными ОС на PDP-11. >Все компиляторы — одной фирмы, стандарт — один и тот же, а вот прочтения этого стандарта — РАЗНЫЕ.

Стандарты это самое главное. Важно что бы все из соблюдали, и авторы компиляторов и разработчики. Иначе будет бардак.


Вот вы приводите пример из времен, когда травка была зеленее, а солнце краше. Прекрасно, но ваш пример не про стандарты в компиляторах, а про разницу в окружениях.


  1. PDP-11 это целое семейство. Начиная от простейших LSI-11 с 32RAM, без MMU, кончая навороченными PDP-11 с огромной по тем временам памятью 1+MB, и сверх прогрессивным MMU. Общего у них только система команд, а в остальном между ними пропасть — примерно такая же как сейчас между ARM Cortex-M0 и ARM Cortex-A53.


  2. Операционные системы для PDP-11 отличались друг от друга, так же, как сейчас отличаются ОС для Cortex-M0 и Cortex-A53:
    На простейших LSI-11 работал только RT11SJ, в котором нет многозадачности, а по сути только дисковые/терминальные драйвера и поддержка простейшей файловой системы. И только работа с физической адресацией 32КБ памяти.

На PDP-11 — RSX-11, RT-11XM да и тот же Unix: Это ОС другого класса: с вытесняющей многозадачностью и виртуальным адресным пространством.


  1. Могу предположить, что в книжке про которую вы говорите речь шла вовсе не про реализации самого языка, а именно про особенности программирования и переноса кода под два принципиально разных окружения.
    Сейчас тоже нельзя просто так взять программу на C++ для Linux, работающую на Cortex-A53, собрать под Bare Metal и запихнуть на STM32F101. Про подходы к портированию кода на C++ на разные ARM сейчас тоже можно многотомнички писать. Но при этом стандарт языка и реализации будут абсолютно стандартными.

Ну в смысле прописных для тех, кто уже занимался переносом. Собственно все просто — >пишем под один >компилятор, потом переносим под другой, потом под третий. Под остальные — уже переносить >просто.

Прописная истина, кажется, в другом. Надо не "писать под один компилятор, а потом переносить под другой", а писать по стандартам языка и не закладываться на заведомо частно платформенные решения типа win32 API, в случае если можно использовать общепринятые стандарты типа POSIX


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


В случае с GUI приложением, если бы подумали о портировании до начала разработки, то возможно вместо VCL использовали другое решение, более адекватно отвечающее требованиям переносимости и надежности.


Вот возьмем к примеру вашу историю про "не потерять пользовательские данные при крахе GUI приложения".


Современный подход к решению этой проблемы:
разделить приложение на две части: легковесный надежный бэкенд, который отвечает за сохранность данных, и работающий с ним по сети фронт (обычно это Web приложение на JS, но с таким же успехом фронт можно написать и на VCL/Qt/something else).


Это современный архитектурный подход к написанию надежных и портабельных приложений. GUI полностью изолировано от пользовательских данных.
Сохранность данных гарантирует аппаратные механизмы защиты памяти ОС. Это качественно иной уровень надежности, чем тысячи проверок в коде, и попытка спасать данные из корраптед кучи.

Ох… ответ будет длинным, придется делить на несколько постов.

> Стандарты это самое главное. Важно что бы все из соблюдали, и авторы компиляторов и разработчики. Иначе будет бардак.

Ох… был я в городе Пущино — господи, как все запущено… Возражений против тупого следования стандартам много.

1) Единого стандарта НЕТ. От слова СОВСЕМ. Какой стандарт вы предлагаете? С одной стороны для MS-DOS используется BC++ 3,1 1991 года выпуска.То есть ориентированный на вторую редакцию Струастрапа. С другой стороны в МСВС 3.0 бывает GCC 2.95.4 — там непонятно какая редакция Страустрапа, с третьей стороны есть современный GCC 6.2, который поддерживает стандарты только начиная с С++98. А ещё есть старый llcc, VC++, С++ Bulder и так далее.

2) НЕ ВСЕ есть в стандартах. В С++11 есть alignas для выравнивания. А как быть с выравниванием на старых компиляторах? Использовать нестандартные опции. Потому что иначе не разберешь бинарный поток.

3) Стандарт нельзя использовать целиком. Есть в стандарте std::vector. Но использовать его нельзя. Потому что машинка может быть с 384 килобайтами памяти и без swap (STM32 под FreeRTOS). И фрагментация кучи просто не позволяет писать надежно.

4) Разница в прочтении стандартов. Иногда она существенная. Хороший стандарт не диктует реализацию, и она может быть прилично разной.

5) Антипатия к стандартам у авторов отдельных компиляторов. Об этом — чуть ниже.

> в случае если можно использовать общепринятые стандарты типа POSIX
ОТЛИЧНЫЙ пример почему не надо тупо следовать стандартам. Читаем https://msdn.microsoft.com/en-us/library/ms235491.aspx «This POSIX function is deprecated. Use the ISO C++ conformant _open instead»
Само собой, POSIX не в курсе, что он устарел — http://pubs.opengroup.org/onlinepubs/9699919799/functions/open.html

Ну и совсем нехорошо с POSIX на FreeRTOS, где нету файловой системы и ещё много чего.

Так что пишем слой совместимости. И уже внутри него — POSIX, WIN32 и так далее.

> Надо не «писать под один компилятор, а потом переносить под другой», а писать по стандартам языка
Ну если у вас нету сопровождения — то можно и так. А если ваши программы должны жить 30 лет и переживать изменения стандартов — то не выйдет. Потому что за эти 30 лет потребуется перенос на платформу. на которой нету компилятора, работающего по старому стандарту.

> Важно что бы все из соблюдали, и авторы компиляторов и разработчики. Иначе будет бардак.
Да не бардак, а БАЗАР. В смысле «Собор и базар» Эрика Реймона.

Как только компиляторы станут реализовывать только стандарт — развитие языка прекратится и язык умрет. Это уже было понятно в районе 1970ого года и описано в учебниках. Был такой язык basic english, Такой, хороший упрощенный английский. Но на беду — запатентованный. То есть его развитие было невозможно. И вместо него — - использовался уродец pidgin english

Ещё один пример. Есть правила русского языка. По ним ВУЗ — среднего рода, ибо ЗАВЕДЕНИЕ. То есть надо говорить, что ВУЗ приняЛО студентов. :-) Но русскому языку — наплевать на его стандарты. И ВУЗ — перебрался в мужской род.

Ещё пример. Нормативный русский язык — это Питерский диалект. я из Питера, вроде бы проблем быть не должно. Но попытка купить в Москве БУЛКУ обернулась когнитивным диссонансом. Булка в Питере — совсем не то, что булка в остальных частях России.

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

Так что вместо тупого следования следования одному стандарту — выбираем некое ядро языка, которое не меняется от стандарта к стандарту. А для изменяющихся частей — опять слой совместимости.
1) Единого стандарта НЕТ. От слова СОВСЕМ. Какой стандарт вы предлагаете? С одной стороны для MS-DOS используется BC++ 3,1 1991 года выпуска.То есть ориентированный на вторую редакцию Струастрапа. С другой стороны в МСВС 3.0 бывает GCC 2.95.4 — там непонятно какая редакция Страустрапа, с третьей стороны есть современный GCC 6.2, который поддерживает стандарты только начиная с С++98. А ещё есть старый llcc, VC++, С++ Bulder и так далее.

Мы сейчас вообще про что спорим? Про то, как жить с унаследованным кодом 30-ти летней давности или про то, как стоит писать новый код.
Если говорить про первое, то это очень специализированная ниша, в которой действительно приходится постоянно натыкаться на "скелетов" в шкафу. Но зачем это экстраполировать в современность.


В общем случае стандарт языка стоит выбрать исходя из минимально поддерживаемого всем набором компиляторов.
Это нормально.


2) НЕ ВСЕ есть в стандартах. В С++11 есть alignas для выравнивания. А как быть с выравниванием на старых компиляторах? Использовать нестандартные опции. Потому что иначе не разберешь бинарный поток.

Начнем с того что разбирать бинарный поток накладывая на структуру это уже не очень хорошая практика. Обращения к полям структур с кастомными алигнмент структур это прямой путь получить на железе исключения unaligned access или вообще просто считать корраптед данные.
Правильно написать парсер, который читает побайтно исходные данные и собирает из них нативно выровненные структуры.
Конечно, иногда для оптимизации кода под конкретное железо требуется вручную задать выравнивание, например для обращения к регистрам или к буферам DMA. Это аппаратно зависимый код. Ему самое место в плфаформозависимых модулях. Но при чем тут стандарт языка — не понятно.


3) Стандарт нельзя использовать целиком. Есть в стандарте std::vector. Но использовать его нельзя. Потому что машинка может быть с 384 килобайтами памяти и без swap (STM32 под FreeRTOS). И фрагментация кучи просто не позволяет писать надежно.

Нужно использовать стандарт, который подходит под ваше железо с учетом специфики решаемой задачи. Тут недавно писал для STM32 с 64к ОЗУ и без RTOS — выбрал стандарт C-99, и не жалуюсь.
Хотя справедливости ради, стоит отметить, что проблемы с vector на 384 кб ОЗУ и фрагментацией памяти выглядят не как проблема стандарта, а проблема конкретного кейса использования вектора в вашей задачи с вашим аллокатором. Для предметного разговора от вас слишком мало вводных.


Ну и совсем нехорошо с POSIX на FreeRTOS, где нету файловой системы и ещё много чего.
Так что пишем слой совместимости. И уже внутри него — POSIX, WIN32 и так далее.

В любом большом проекте конечно появляется слой совместимости с платформами, потому что на каждой платформе свои API. Но как это противоречит утверждению, что вместо проприетатных стандартов следует использовать общие?


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

Стандарты С++ обратно совместимы. В чем проблема то?
А если речь, про то, что код "написанный под компилятор borland c++ 3.1", (который вообще клал на стандарты), перестал собраться, то извините, это не проблема стандарта, а проблема того что "написали код под конкретный компилятор".


Так что вместо тупого следования следования одному стандарту — выбираем некое ядро языка, которое не меняется от стандарта к стандарту. А для изменяющихся частей — опять слой совместимости.

В случае с C++ вы используете минимальный стандарт, который поддерживает то что вы "причислили к ядру языка". Написали где то
"for (x: range y)" или "auto xx =" — все у вас проект на c++11
Написали "p = make_shared ()" — у вас проект на c++14

> Мы сейчас вообще про что спорим? Про то, как жить с унаследованным кодом 30-ти летней давности или про то, как стоит писать новый код.
Про то, как писать код, переносимый на многие платформы, в том числе и старые. Или новые со старыми компиляторами, вроде NeuroMatrix.

> В общем случае стандарт языка стоит выбрать исходя из минимально поддерживаемого всем набором компиляторов.
ЕГО НЕТ. См. предыдущий ответ. Есть некое подмножество стандарта С++2.0 (вторая редакция Страустрапа), понимаемое всеми компиляторами. Практически мы пишем на Си с элементами из С++ (ссылки, комментарии, упрощения синтаксиса).

> Обращения к полям структур с кастомными алигнмент структур это прямой путь получить на железе исключения unaligned access или вообще просто считать корраптед данные.

Позвольте вам не поверить. Жду доказательств.
Грубо говоря вы считаете, что использование #pragma pack(push,1) (ну или alignas)в описании структур неверно реализовано в gcc и это не является багом компилятора???

> Но при чем тут стандарт языка — не понятно.
Да, ВЫ ПРАВЫ alignas не заменят ни #pragma pack ни __attribute__ ((packed)) ни __declspec(align(1))
Значит в стандарте этого пока нет. С++ на 45 лет отстал от паскаля, где PACKED был изначально.

> Но как это противоречит утверждению, что вместо проприетатных стандартов следует использовать общие?
Гм. Кажется у нас проблема с русским языком. У нас проприетарный означает запатентованный. То есть не свободный. Ну собственно как в википедии. А что это означает у вас?

Примеры проприетарных стандартов — это GIF, BMP, JPEG. TIFF. Примеры свободных стандартов — PNG, OpenEXR, JBIG2.
Вы действительно считаете, что надо использовать JBIG2 вместо JPEG? :-))))

А использовать надо СВОЙ слой совместимости. И уж из него — в те стандарты, в какие надо. Используя только POSIX — вы затрудняете себе перенос. Потому что на некоторых системах более трудоемко написать реализацию POSIX, чем свой реализовать свой собственный, заточенный под задачу API.

> Стандарты С++ обратно совместимы.
ДА НУ? Об этом даже Страустрап не знает. Почитайте https://habrahabr.ru/company/pvs-studio/blog/270191/ про присвавание this.

Стандарты БОЛЕЕ-МЕНЕЕ совместимы. Но включивши -Wall -Wextra вы уже узнаете много интересного. А уж если -Wpedantec…

> код «написанный под компилятор borland c++ 3.1», (который вообще клал на стандарты),
Намного больше проблем с Visual Studio. Они там много на что болт положили. sprintf, например, у них obsolete. strcpy — тоже.

> В случае с C++ вы используете минимальный стандарт, который поддерживает то что вы «причислили к ядру языка».
Не совсем так. Если использовать C++1.0 или С++2.0 — можно налететь. gcc их не поддерживает в полном объеме. Поэтому именно некое ядро.

> Написали «p = make_shared ()» — у вас проект на c++14
Нет, всего лишь VC++2010 https://msdn.microsoft.com/ru-ru/library/ee410595(v=vs.100).aspx
Ну или С++11 — http://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared
Позвольте вам не поверить. Жду доказательств.
Грубо говоря вы считаете, что использование #pragma pack(push,1) (ну или alignas)в описании структур неверно >реализовано в gcc и это не является багом компилятора???

Вы либо не умеете читать, либо не понимаете разницы между "использование #pragma pack(push,1) (ну или alignas)в описании структур неверно >реализовано в gcc и это не является багом компилятора?" и "Обращения к полям структур с кастомными алигнмент структур это прямой путь получить на железе исключения unaligned access или вообще просто считать корраптед данные"


gcc генерит инструкцию, которая читает/пишет слова по не выровненным адресам. А справится ли с ней процессор, уже зависит от конкретной реализации процессора. Например на ARM,


ldr     r2, [r0]

Если в r0 нечетный адрес, то возможны три варианта — считает правильное значение/выкинет аппаратное исключение/считает значение по ближайшему выровненному адресу.
Поведение зависит от возможностей ядра и битов управляющих регистров.


http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0344k/Beihgifg.html (например A8)


На этапе компиляции компилятор не знает, как будет себя вести инструкция на железе. Вы видимо ожидаете, что компилятор по умолчанию вставляет на каждый дерефернс указателя int * вот такую пачку инструкций вместо одной:


        ldrb    r6, [r0, #1]    
        movw    r1, #:lower16:.LC0
        ldrb    r3, [r0]       
        movt    r1, #:upper16:.LC0
        ldrb    r5, [r4, #2]    
        movs    r0, #1
        ldrb    r2, [r4, #3]   
        orr     r3, r3, r6, lsl #8
        orr     r3, r3, r5, lsl #16
        orr     r2, r3, r2, lsl #24

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


Значит в стандарте этого пока нет. С++ на 45 лет отстал от паскаля, где PACKED был изначально.

А кто вам мешает писать на паскале? Или даже на обероне, вроде его признали самым самым безопасным языком — вам видимо понравится :)


Дальнейшую чушь про ваше видение стандартов комментировать не вижу смысла.

> На этапе компиляции компилятор не знает, как будет себя вести инструкция на железе.

Это называется UB (undefined behavior). Или вы доказываете, что это UB по стандарту gcc, или — это отступление от стандарта. То самое, про которое вы говорили, что его не бывает. Или — вы просто не смогли разобраться. Что вероятнее всего.

>Вы видимо ожидаете, что компилятор по умолчанию вставляет на каждый дерефернс указателя int * вот такую пачку инструкций вместо одной:

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

#pragma pack
struct test {
int even;
char a;
int odd;};

Код для обращения к even будет простым, а вот для обращения к odd — сложным. Зато &odd не всегда (не во всех версиях gcc и не во всех режимах оптимизации) можно передать параметром в процедуру. Это при выровненном test. Если у нас массив test — то к обоим поля обращаться будем через сложный код. Ещё один момент — в рассуждениях выше struct test выровнен.

Не верите — ну так проверьте сами.

> А кто вам мешает писать на паскале?
А слишком мало платформ поддерживается. Мне собственно все равно на чем писать. Опыт показал, что зная полсотни языков можно с успехом отлаживаться даже на языке, который видишь впервые. У меня так с фокалом было. Сначала целый день исправлял детям баги в программах, а уж потом — прочел описание фокала и БК-0010. Это 1986ой год был.

> Дальнейшую чушь про ваше видение стандартов комментировать не вижу смысла.
Ничего страшного. Лет 20 поработаете, и увидите, что надо пользоваться смыслом слов из словаря. А не вашим местным жаргоном. Свободный стандарт и распространенный стандарт — разные вещи. Unix, например, проприетарный.

Это называется UB (undefined behavior). Или вы доказываете, что это UB по стандарту gcc, или — это отступление от стандарта. То самое, про которое вы говорили, что его не бывает. Или — вы просто не смогли разобраться. Что вероятнее всего.

А давно это pragma pack появилась в стандарте? Вы уж определитесь вы либо про стандарт либо про pragma pack. Вместе нельзя.


ДА. В машинном коде именно так. Вот только не на каждый. Похоже, что вы действительно не разбираетесь в кодогенерации. Ну вам простительно — вы не компиляторщик.
pragma pack

struct test {
int even;
char a;
int odd;};
Код для обращения к even будет простым, а вот для обращения к odd — сложным. Зато &odd не всегда (не во всех версиях gcc и не во всех режимах оптимизации) можно передать параметром в процедуру. Это при выровненном test. Если у нас массив test — то к обоим поля обращаться будем через сложный код. Ещё один момент — в рассуждениях выше struct test выровнен.

Вы как обычно написали чушь. gcc считает (и правильно делает) по другому.
gcc 4.8.2 arm:


#include <stdio.h>
#include <memory.h>

#pragma pack (1)
struct test {
int even;
char a;
int odd;};

void  foo(struct test *t)
{
   printf ("%d,%d",t->even,t->odd);

}

foo(test*):
        movw    r1, #:lower16:.LC0
        ldr     r3, [r0, #5]      @ unaligned
        ldr     r2, [r0]  @ unaligned
        movt    r1, #:upper16:.LC0
        movs    r0, #1
        b       __printf_chk
.LC0:
        .ascii  "%d,%d\000"

На сим диалог с вами заканчиваю.

> А давно это pragma pack появилась в стандарте?
Давно. В стандарте gnu89 она есть.

> Вы уж определитесь вы либо про стандарт либо про pragma pack. Вместе нельзя.
Читайте ВНИМАТЕЛЬНО. написано было «стандарт gcc» Свободные стандарты — тоже стандарт. Тем более, когда он поддерживается несколькими компиляторами (lcc, clang). А уж когда именно на нем написано ядро linux…

Кто там говорил, что свободные стандарты лучше проприетарных? ну вот вам ещё один свободный стандарт

> gcc считает (и правильно делает) по другому. gcc 4.8.2 arm:
Какие опции командной строки? Какие конфигурации? Ну хоть gcc -v сделайте.

Ваш опыт означает, что именно при ваших опциях он так не работает. Но не означает, что он ВСЕГДА работает не так.

Давно. В стандарте gnu89 она есть.
gcc заслуженно очень популярный компилятор, бесспорно. но в случая с языками аппелировать надо к стандартам IEEE, как к общепринятым.

gcc считает (и правильно делает) по другому. gcc 4.8.2 arm:
Какие опции командной строки? Какие конфигурации? Ну хоть gcc -v сделайте.
Ваш опыт означает, что именно при ваших опциях он так не работает. Но не означает, что он ВСЕГДА работает не так.

С говнокодом так обычно и бывает. "Код хороший, а опции компилятора плохие!" Удачи

> но в случая с языками аппелировать надо к стандартам IEEE, как к общепринятым.
ТААК… Почитайте https://ru.wikipedia.org/wiki/ISO и https://ru.wikipedia.org/wiki/IEEE для понимания отличий.
Стандарт С++ — это ISO. POSIX — это IEEE

> С говнокодом так обычно и бывает.
ну где неточности в естественных языках — там и говнокод в языках программирования. :-)

> «Код хороший, а опции компилятора плохие!»
Ну почему вы все неверно понимаете? Это не плохие опции, это означает, что у вас так настроено. Или на target cpu нету таких проблем или при конфигурации gcc было указано, что конфигурация регистров процессора именно такая.

У нас с вами разные задачи. Вам, вероятно, нужен бинарник, работающий в разных окружениях. На это не нужно в ПРИНЦИПЕ. Наоборот, для нас лучше, если бы бинарник запускался только на нашем изделии. Меньше шансов, что скопируют за малые деньги. За большие — понятно, что скопируют в любом случае. Мы не программами торгуем, а ПРИБОРАМИ.

ну вот вам прямое управление

-munaligned-access
-mno-unaligned-access
Enables (or disables) reading and writing of 16- and 32- bit values from addresses that are not 16- or 32- bit aligned. By default unaligned access is disabled for all pre-ARMv6 and all ARMv6-M architectures, and enabled for all other architectures. If unaligned access is not enabled then words in packed data structures are accessed a byte at a time.
The ARM attribute Tag_CPU_unaligned_access is set in the generated object file to either true or false, depending upon the setting of this option. If unaligned access is enabled then the preprocessor symbol __ARM_FEATURE_UNALIGNED is also defined.

ВИДИТЕ — «By default unaligned access is disabled for all pre-ARMv6 and all ARMv6-M architectures». «If unaligned access is not enabled then words in packed data structures are accessed a byte at a time.»
А я видел это на ARM9E с архитектурой ARMv5TEJ

А компилятор (любой) ещё круче умеет
struct KOPOBA
{
uint_32_t i1:3;
uint_32_t BOPOHA:16;
uint_32_t i2:13;
};

И ничего, работает. Это тоже стандарт, только уже https://en.wikipedia.org/wiki/RTCM
Конкретно RTCM 104 3.2 — http://www.rtcm.org/descriptions/10403.2.pdf Там массивы битовых структур, имеющих длину не кратную байту.

Я понимаю, вы видимо налетели на ошибку с доступом и решили, что это фатальная мисфича компилятора. А про доступ к битовым полям — не подумали, ибо не компиляторщик.

Мне тоже урок — попросить электронщика проверить, что биты в конфигурации проца выставлены.

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

КОД, сгенеренный при -mno-unaligned-access показать? Или описании gcc поверите на слово?
P.S. «The ARM attribute Tag_CPU_unaligned_access is set in the generated object file to either true or false, depending upon the setting of this option.» Вероятно, линкер это протаскивает до атрибутов исполняемого файла. То есть ядро знает, может ли исполняемый файл так обращаться. И скорее всего — ядро это тоже умеет поддерживать.

Так что, если в ядре включена нужная поддержка — все будет работать совсем автоматически.

Возможно у вас в ядре это было отключено. Или попытались код с A8 исполнить на более ранней архитектуре.
> Поведение зависит от возможностей ядра и битов управляющих регистров.
А нефиг брать ядра с одной помойки, а gcc c другой (правда и сами грешны). gcc нужно настраивать под процессор и опции ядра.

Впрочем, у вас небось обратная задача — исполнять один и тот же БИНАРНЫЙ код где угодно. У нас такой задачи нет — мы компилируем под конкретные железки. Причем часть из них — даже сами разрабатываем.

Совместимость бинарного кода нам не нужна, НАОБОРОТ, полезна привязка к железу.
Вторая часть. :-) Про архитектуру и пророков. :-)

> Современный подход к решению этой проблемы: разделить приложение на две части: легковесный надежный бэкенд, который отвечает за сохранность данных, и работающий с ним по сети фронт

Угу, больше 20 лет назад 1С-Бухгалтерия пошла этим путем. DBase в качестве сервера и тяжелый фронтенд. Вышло ужасно. Более 10 пользователей одновременно работать не могли. При создании отчета — вообще зависали все остальные клиенты.

На основе проблем с масштабирование у этой архитектуры сначала пришли к тонким клиентам, потом — к трехзвенной архитектуре (СУБД — сервер приложений — клиент).

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

> А начинать надо даже не с написания кода, а с проектирования архитектуры, выбора компонентов и фреймворков, которые будут использоваться.
ну что же, жду от вас рассказа, что будет через 30 лет. Сложно прогнозировать на 30 лет? Хорошо, опишите 2030 год, всего 14 лет вперед. какие архитектуры процессоров будут, какие стандарты, фреймворки, компоненты?

То приложение, которое у нас работает в куче операционок и под разными компиляторами имеет куски кода 20летней давности. Что там было с linux 20 лет назад? А что было с ARM? А что со стандартами? :-)

Хотите, чтобы ваша программа использовалась лет 50 — ну или наймите шамана для предсказаний — или уж готовьтесь, что придется переносить. И старайтесь не закрывать пути для переноса.


При чем тут 1С, и то что было 20 лет назад. Осмотритесь, на то, что есть сейчас Google Docs, Office 360, и еще целый вагон и тележка решений, которые работают надежно и данные кажется обычно не теряют.


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

А я где то по тексту писал про "Толстый клиент"? Фронт != "Толстый клиент".


Вообще, баланс между клиентом и сервером — тема для большого обсуждения.

Вот именно. Вы критикуете решения, в которых выбран неверный баланс. Но при чем тут общий архитектурный подход?


ну что же, жду от вас рассказа, что будет через 30 лет. Сложно прогнозировать на 30 лет? Хорошо, опишите 2030 год, всего 14 лет вперед. какие архитектуры процессоров будут, какие стандарты, фреймворки, компоненты?

Понятия не имею что будет :) Но одно знаю точно: код написанный сегодня на c++11/14 будет легче перенести на новые платформы, чем код повсеместно закладывающийся на конкретные версии компиляторов.

> При чем тут 1С, и то что было 20 лет назад.
История должна учить. Если история ничему не учит — это ошибки повторятся.

> Google Docs, Office 360
Сравнили океанский лайнер с катером. Вы трудоемкости оцените для начала. Там тысячи человеко-лет потрачены.

> А я где то по тексту писал про «Толстый клиент»? Фронт != «Толстый клиент».
Легковесный фронтенд вместе с легковесным бэкендом? Вы это имели ввиду? А куда основная логика уходит?

> Вы критикуете решения, в которых выбран неверный баланс. Но при чем тут общий архитектурный подход?
Может я и ошибаюсь, но легковесным может быть или бэкенд или фронтенд, но не оба сразу. Если вы настаиваете на легковесном бэкенде — то это как раз 1С.

А если вы признаете, что легковесный бэкенд не годится…

У нас была мноуровневая архитектура.

1) В контроллерах работал код сбора данных — 300 строк на ассемблере OMRON CV-1000 c 50 страницами документации на них. Вычитывали этот код 5 человек, причем каждый — несколько раз.

2) 2 сервера, в которых работали сервисы по схеме теплого резерва. Каждый сервер подключался к контроллерной сети через 2 независимых канала (SysmacLink и Ethernet-II). Сервис состоял из 20 тредов и выполнял функции:
— сбор информации
— сервер СУБД
— диагностика
— виртуальный контроллер и компилятор для него, Для исполнения машинного кода CV-100 на I686 мне показалось проще написать компилятор, чем интерпретатор
Основная защищенность от ошибок была именно на сервере.

3) С серверов через 2 независимых сети данные шли до клиентов, Клиентов было 2 типа — GUI и сервер-OPC.
GUI- клиенты. GUI-клиент был тяжелым, и имел некие зачатки искуственного интеллекта :-), в том числе
— Упрощение релейных схем (на самом деле компиляция в дерево, упрощение и декомпиляция обратно). Языки были IL, LD, FBD из https://ru.wikipedia.org/wiki/IEC_61131-3
— Обнаружение начальных причин аварий по неполным данным. Горжусь, что в споре с автором кода контроллера, выяснилось, что программа анализирует сложный код лучше человека.

Ещё клиент умел править СУБД по привязке сигналов к агрегатам, элементам, рабочим местам

Толщина клиента был из-за сложной системы поиска-фильтрации нужных кусков схем.

Клиент тоже был написан надежно, см. ниже.

4) OPC-Сервер был легковесный и надежности не требовал.

5) SCADA занималась только отображение и дублировала аппаратный пульт.

6) АСУ. Это уже не наша работа, но они могли стыковаться к нам через OPC.

Минута простоя стана — это 40 тысяч долларов улетают в брак, Поэтому простой считался в минутах и за 3 часа простоя лишался премии не конкретный виновник, а все служба. А премия у ребят была равна окладу.
Крайними были автоматчики. Поэтому при сбое — счет шел на секунды. То есть автоматчики ночью спали — но по сирене — должны были мгновенно понять, что же в стане длиной 500 метров произошло. Это при 10 тысячах входов и 2 тысячах управляющих выходов.

Так что необходимость перезапуска клиента — это лишние секунды. А их не было. Потому клиент был сделан так, что нужного результата можно было достичь несколькими путями. И даже при полном отказе одного пути — это было не критичным, автоматчики всегда имели в голове несколько путей.

Ну и как бы вы это реализовали? Архитектурно? За 10 человеко-лет.
> Понятия не имею что будет :)
СПАСИБО. Мы тоже не имели понятия, что понадобится. И сейчас имеем только на год-два вперед.

> Но одно знаю точно: код написанный сегодня на c++11/14 будет легче перенести на новые платформы

1) Расскажите, как вы перенесете код на С++11 на https://ru.wikipedia.org/wiki/МСВС 3.0 с компилятором gcc 2.95.4 и ядром 2.4.32. Специфика МСВС в том, что как только вы туда ставите свой компиялтор — система теряет сертификацию. Компилироваться разрешено только на ней и только её родным компилятором.

2) Расскажите, как вы перенесете код на С++11 на MS-DOS

3) Расскажите, как вы перенесете код на С++11 на https://ru.wikipedia.org/wiki/NeuroMatrix Если не путаю, в этом процессоре адресация только словная, то есть 32битные байты. И sizeof(int)==1. ну и компилятор там далеко не gcc, а какая-то разработка советских времен.

Особенно интересует NeuroMatrix, как действительно НОВАЯ платформа. Но с очень старым компилятором. :-) А то мне это ещё предстоит. :-)

> чем код повсеместно закладывающийся на конкретные версии компиляторов.
Вы понимаете что такое «СЛОЙ СОВМЕСТИМОСТИ»? В который уже раз о нем пишу. Не повсеместно, а в специальном слое совместимости.
Часть третья. Про сохраннность данных.

> Сохранность данных гарантирует аппаратные механизмы защиты памяти ОС. Это качественно иной уровень надежности, чем тысячи проверок в коде, и попытка спасать данные из корраптед кучи.

МДА… Приличных слов нету, неприличные писать не хочу… ну в общем каждый понимает в силу своей собственной квалификации. Вас я бы к себе в сотрудники не взял. :-)

1) Боитесь порчи данных — значит считайте их CRC при каждом изменении. При порче — CRC не сойдется. Не забывайте, что порча памяти может и не повредить кучу, зато — испортить данные.

2) Боитесь испорченной кучи — значит при каждом исключении от new проверяйте структуры кучи. В библиотеках для этого обычно есть что-то типа HeapWalk/HeapCheck.

3) Аппаратные механизмы — страхуют лишь от отдельных ошибок. Они используются, но панацеей от любых ошибок программиста не являются.

4) Проверки инвариантов — это лишь первый уровень защиты. Они нужны, чтобы случайный дятел не разрушил всю цивилизацию.

В целом ваше понимание — отражает то ли очень низкий уровень вашей квалификации, то ли мое неумение объяснять. Могу лишь ещё раз повторить.

Захват ресурса
Try
Try
Использование ресурса
Except
Выдача сообщений об ошибке (в лог, оператору — не важно)
Увеличение счетчика отказов
Finally
Освобождение ресурса
End

В такой схеме ресурс всегда освобождается и блокировок не возникает. Попытки свалить с больной головы на здоровую, то есть с клиента на сервер — не помогут. Все равно найдутся ресуры, которые вы не сможете освободить при завершении приложения.

В такой схеме, с гарантированным освобождением ресурсов, чем ближе к моменту возникновения ошибки мы её заметим — тем лучше.

Я понимаю, что про транзакции вы не знаете, потому почитайте https://ru.wikipedia.org/wiki/Транзакция_(информатика) и https://ru.wikipedia.org/wiki/ACID Все это применимо не только к СУБД, но и к написанию любых программ. Боюсь, что для вас понятие транзакции будет шоком.

Счетчик отказов — нужен для своевременного перезапуска компонентов подсистем, самих подсистем и перехода на резервный сервер. Иными словами — мы ведем подсчет отказов на трех уровнях. И такой же трехуровневый контроль зависаний — watchdog,

В целом ваше выступление — это оправдание на тему, почему вы пишите ненадежно. Ну пишите так, что любая ошибка для вас смертельна — ну пишите. Это проблема ваших заказчиков, а не моя, Только не надо убеждать, что надежность недостижима. Достижима. И вполне за разумные деньги.

Другой момент — где надежность нужна, а где нет. Вполне допускаю, что вашим заказчика на неё плевать.

В BTS (https://en.wikipedia.org/wiki/Bug_tracking_system ) есть такое понятие — степень важности ошибок

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

Так вот, при написании надежных программ мы уменьшаем степень важности на 1-2 ступени.

А что в вашей модели с легковесным бэкендом? А у вас все ошибки ФАТАЛЬНЫЕ. То есть 10 минут подготавливаем условия для сложной операции. Ну скажем фильтрации архива по большому набору условий. Запускаем фильтрацию — вылетели. Хорошо, перезапустили приложение, повторили — вылетели. После третьего раза — снесли приложение и поставили что-то нормальное.

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

> Это современный архитектурный подход к написанию надежных и портабельных приложений. GUI полностью изолировано от пользовательских данных
А в итоге — ни надежности, ни портабельности. 10 лет прошло — и все, НЕ РАБОТАЕТ. Современная инфраструктура веб-приложений требует смены техники каждые 2-3. А надежное — должно работать ХОТЯ БЫ лет 20, а лучше — 50.
Гм, в каком году мы проект на Северстали сдали? 2002? 2003? А он работает и умирать не собирается. И ещё лет 20 проработает. До самой смерти тех станов, что он обслуживает.

У меня Андроид 2009 года. Не работает — почти ничего из веб-приложений. Ни новые, ни старые. У старых — сломался бэкенд, у новых — не пашет фронтенд.

P.S. Для понимания. Надежное приложение и переносимое приложение — это две разные работы.
МДА… Приличных слов нету, неприличные писать не хочу… ну в общем каждый понимает в силу своей собственной квалификации. Вас я бы к себе в сотрудники не взял. :-)

Переход на личности первый признак того, что аргументы по существу кончились… Увы.


По существу ответ очень простой: Вы говорите, что важно улучшать количественную характеристику — писать много проверок в коде, на каждом чихе считать CRC, проверять this, и т.д.
С этим не спорю — в некоторых ограниченных случаях, такие подходы можно применять. Когда вы пишете программу для жестких условий эксплуатации, например, в которых не гарантируется сохранность памяти, скажем для спутников. Утрируя — там надо 2x2 вычислить 3 три раза и сравнить результаты вычислений.
В остальных случаях есть здравый смысл — доверие к базовой платформе.


Я же говорю, что в первую очередь надо стремиться улучшать не количественную характеристику, а качественную. Можно достичь невиданного мастерства затыкания дырок в старой гнилой лодке, и она будет оставаться на плаву, а можно обшить эту лодку современным легким и надежным материалом полностью. А еще лучше выкинуть старую гниль и сделать лодку из современных материалов.
Вы кажется за первый подход, считая его единственно верным, игнорируя наличия других. Извините, дальше спор кажется бессмысленным.

> Переход на личности первый признак того, что аргументы по существу кончились… Увы.
Аргументы я могу ещё раз повторить, но вы все понимаете в соответствии с собственной квалификацией. Или в соответствии с моим умением объяснять.

> скажем для спутников.
Космическая техника слишком слаба для завода. Не выпадает в космосе за месяц миллиметр металлической окалины даже в бытовых помещениях. Не идет дождь в 40 сантиметрах от работающего контроллера. Точнее дождь из дыры в потолке летел бы прямо на контроллер, но его прикрыли наклонным металлическим листом.

Глянул цены на спутники — тоже ничего особенного.http://www.insur-info.ru/press/60139/ — Глонасс-М — 30 млн долларов. Примерно 20-30 дней работы стана. Устроить аварию с месячным простоем стана — вполне реально, просто маловероятно. Зато у нас экономический эффект в одном из режимов, полученный при помощи системы — 480 тысяч долларов.

А что реально родним эту систему с космосом — максимально возможное время комплексной отладки на реальном оборудовании — 2 часа в месяц. ДВА ЧАСА, Карл! В МЕСЯЦ, Карл! Остальное, как и у космических систем — на имитаторах. Ну и реально мы это время не использовали — то есть проверили, что работает и сразу в опытную эксплуатацию.

> Вы говорите, что важно улучшать количественную характеристику
я ТАКОГО не говорил. Это вы поняли в меру собственной квалификации. Ну или в меру моего умения объяснять (хреновый я преподаватель).

> а можно обшить эту лодку современным легким и надежным материалом полностью.
ОТЛИЧНЫЙ образ. Но любая дыра в вашей обшитой резиновой лодке приводит к тому, что она потонет. Потому что камера у вас одна. От иголки вы ее сове обшивкой защитите. А от шила — уже нет.

А у меня лодка из пенопласта с тяжелым килем. Количество камер — очень велико. Хоть в 10 местах проткни насквозь — пенопласт будет плавать. Положительная плавучесть в любом положении, хоть килем кверху. Устойчивость. Завалили на бок — а она обратно килем вниз-встала, как ванька-встанька.

Вообще ванька-встанька — любимая модель для надежного ПО. Его сломать сложно. Толкаешь — а он не ломается, а просто качается.

> В остальных случаях есть здравый смысл — доверие к базовой платформе.
К чему-чему? КТО программирует, вы или платформа? Доверия нету к ПРОГРАММИСТУ.

Вообще защитное программирование не мой изобретено. И даже не фирмой Borland, хотя любое приложение на VCL будет достаточно отказоустойчивым.

Качественная разница между моделями такова.
У ВАС. Я отличный программист, я сумею отладить код так, что не останется ни одной ошибки. У меня самая безошибочная библиотека и самая надежная ОС, в них тоже ошибок не будет.

У МЕНЯ. я делаю ошибки. Они есть у меня в коде. Они есть в библиотеке, в ОС и даже в железе. Но я пишу так, чтобы изолировать ошибки.

У вас — НАДЕЖДА на непогрешимость в будущем. У меня — уверенность в настоящем. Это вот ключевое.

Ну да, это не похоже на стиль защитного программирования, например из этой статьи — https://habrahabr.ru/post/191548/ Ну как минимум проверок раз в 5 меньше. Собственно не так сложно посмотреть примеры в коде VCL.

Про методику — я уже писал много

1) Основное средство изоляции ошибок — это исключения, в том числе, ОБЯЗАТЕЛЬНО, и аппаратные.

2) Для их обработки — try except reraise finally, причем except — часть, нужна лишь для диагностики, она вводится в отдельных местах, где информации из самого исключения недостаточно для исправления ошибки по логу. В языках с автоматическими вызовами деструкторов (типа C++) можно отказаться от части finally, сделав её как деструктор локального объекта. Но это хуже, порядок вызовов деструкторов регламентируется неявно. Модель дельфи с явным вызовом деструкторов мне кажется удобней.

3) Для уменьшения влияния ошибок вводятся assert. Его роль — выявить ошибку пораньше к месту его возникновения. Частота — от 1 assert наf 10 строк кода до 1 на 100. Зависит и от специфики кода и от программера. Одна из особенностей — проверка идет в до установки значения переменной, а не перед использованием. Это уменьшает общее число проверок. Собственно проверке в одной подсистеме подлежат прежде всего данные. полученные от другой подсистемы.

4) Деструкторы должны выполняться без выхода исключений за пределы деструктора. И в любых возможных и большей части невозможных ситуаций. Вот тут реально добавляются проверки.

5) Контроль параметров работы подсистемы (WatchDog), осуществляется подсистемой более верхнего уровня. Он позволяет произвести перезапуск подсистемы при её зависании.

6) Счетчики ошибок устанавливаются на верхних уровнях обработки исключений. Не дать подсистеме постоянно перезапускаться, а при обильном повторе исключений и перезапусков — в качестве последнего средства сделать переход на резервный сервер, перезапуск приложения или перезагрузку машины.

Тонкостей довольно много, например — ошибки правятся иногда трижды:
— изоляция, то есть система должна работать после ошибки
— ранее обнаружение, как часть изоляции
— собственно исправление ошибки

Самое интересное — экономика. Если не заморачиваться с пересозданием объектов и перезапусков подсистем — экономика плюсовая для широкого класса программ.

Дело в том, что количество ошибок падает в программе уменьшается медленно. Если у вас 1 ошибка на 100 строк и 100 тысяч строк в программе — у вас 1000 ошибок. Если вы найдете за первый год 500, то за второй год 250, за третий — 125 и так далее. К полному устранению ошибок вы не придете никогда.

Но суть в том, что пользователю не так важно, есть ли ошибки. Важно, чтобы ваше приложение работало надежно, а это не одно то же. Робастное приложение работает, несмотря на ошибки, поэтому нуждается в меньшем времени отладки для той же надежности. Как я уже писал — у нас комплексной отладки на оборудовании просто не было. Запустили, проверили, пошли в опытную эксплуатацию.

Так что проигрывая процентов 10 на времени написания кода и на трудоемкости исправления ошибок мы сильно выигрываем во трудоемкости отладки, тестирования и сопровождения.

По моим оценкам в нашей системе осталось примерно 500 ошибок (1 ошибка на 2 тысячи строк кода). Но более 10 лет система работает 365*24.

На полную реализации методологии (с перезапусками подсистем, с 20 тредами в сервере) мы потратили примерно 30%, то есть 3 человеко-года из 10. Выиграли — не меньше 10 человеко-лет на отладке и сопровождении.

Границы применимости (то есть когда выгодно писать надежно):
1) Любые системы, могущие привести к гибели человека хотя бы раз в 100 лет.
2) Любые системы, могущие привести к убыткам более миллиона долларов или более стоимости фирмы-разработчика.
3) Любые системы более 70 тысяч строк.

Языки для написания должны иметь механизм исключений и поддержку аппаратных исключений. Из широкоизвестных — дельфи и Java. В С++ аппаратная поддержка исключений есть в С++ Bulder и включаетяс в VC++.
Это преимущество отрицательной кармы. Lingva latina non penis canina. Но реально — то ли ученик не понимает, что я объясню, то ли преподаватель из меня — тот самый penis canina. Но человека, настолько не понимающего мои объяснения — я в сотрудники точно не возьму. Ну хотя бы из-за психологии.

Я не против иной точки зрения, но когда человек постоянно все понимает превратно — честное слово, кажется, что он нарочно издевается.

Неужели я настолько непонятно излагаю свои мысли?
Часть четвертая. Про PDP-11 и вагон ошибок.

> Начиная от простейших LSI-11 с 32RAM, без MMU
LSI — это всего лишь https://en.wikipedia.org/wiki/Large_Scale_Integration то есть БИС. К LSI относится много машин, в том числе навороченные 11/73 (аналог — «электроника-87»). Отличительная черта моделей LSI — это QBus вместо Uniabus.

32RAM — это 60 килобайт, память тогда измерялась в килословах. 4 килобайт уходило на страницу вввода-вывода.

> На простейших LSI-11 работал только RT11SJ,
Опять глупости. Работал ещё DOS-11, RT11FB, RSX-11(A,B.C,S), даже RSX-11M можно было запустить, хотя он слишком тяжел для такой машины. Требовали MMU — RSX-11M+, RSX-11D, IAR, RSTS/E, RT11XM.

> На PDP-11 — RSX-11, RT-11XM да и тот же Unix:
И опять — вы путаете технологию процессора с возможностями машины. Грубо говоря LSI — это МикроЭВМ, предтеча нынешних персоналок. а не LSI — это миниЭВМ.

Как пример СМ-3. https://ru.wikipedia.org/wiki/СМ_ЭВМ " производительность — 200 тыс. оп/с, ОЗУ 56 Кб, в первых системах на ферритовых сердечниках."

> И только работа с физической адресацией 32КБ памяти.
И ОПЯТЬ ошибка. физическая адресация — 16 бит, то есть 64 килобайта. 4 килобайта на регистры ввода-вывода и 60килобайт памяти

Честно говоря — не ожидал от вас таких перлов.

Окей. Опять вспомним зеленую травку :)


Кажется, по существу, что PDP-11 это целое семейство, с принципиально разными окружениями, требующими разных подходов у вас возражений нет.


К деталям:


На простейших LSI-11 работал только RT11SJ,
Опять глупости. Работал ещё DOS-11, RT11FB, RSX-11(A,B.C,S), даже RSX-11M можно было запустить, хотя он слишком тяжел для такой машины. Требовали MMU — RSX-11M+, RSX-11D, IAR, RSTS/E, RT11XM.

И только работа с физической адресацией 32КБ памяти.
И ОПЯТЬ ошибка. физическая адресация — 16 бит, то есть 64 килобайта. 4 килобайта на регистры ввода-вывода и 60килобайт памяти

Вот возьмем БК-0010, "это простейшая LSI-11 c 32КБ ОЗУ", и входит в то самое семейство PDP-11.


У стандартной БК0010 было 32КБ ОЗУ.
На ней мог работать и нормально функционировать только RT11SJ. Бесспорно, если задаться самоцелью можно было попатчить собрать/запустить и другие мониторы, не требующие аппаратного MMU, однако в этом не было никакого практического смысла — ОС бы заняла большую часть ОЗУ, оставив несколько КБ для запускаемых приложений.


Вы называете это "работала", то ок, академически вы правы. Но мы тут про практику вроде говорим.

> Кажется, по существу, что PDP-11 это целое семейство, с принципиально разными окружениями, требующими разных подходов у вас возражений нет.

Разумеется есть. Окружение зависят от РОЛИ машины. На одной и той же машине и одной и той же RSX-11M можно было сделать
— Управление полетом ракеты (иди физическим экспериментом) в жестком реальном времени.
— Концентратор терминалов (штук 20) сети продажи билетов (с кэшированием и бизнес-логикой) — мягкое реальное время. Реально Сирена работала на СМ-2М, но это детали.
— Систему разработки программ на 5-6 рабочих мест (только терминалы надо ставить со строчным редактированием)
— Персональную миниЭВМ

Все это — можно без без MMU и на 60 кб оперативной памяти.

> Вот возьмем БК-0010, «это простейшая LSI-11 c 32КБ ОЗУ», и входит в то самое семейство PDP-11.

МДА. Вы действительно считаете, что система команд и архитектура ЭВМ — это одно и то же? Разочарую — https://en.wikipedia.org/wiki/Xerox_NoteTaker — это не IBM PC. Она выпущена на 3 года раньше и с другой архитектурой. Но процессор в ней то же.

Так что БК-0010 — не PDP-11. Не было в PDP-11 16К видеопамяти с прямым доступом. И замены ОЗУ на ПУЗ тоже не было. Точно так же УКНЦ — это не PDP-11.

Ну и в любом случае БК-0010 — это не LSI, то есть он не был построен на БИС. Там СБИС К1801ВМ1 (а в некоторых моделях — вроде и ВМ2 ставили). Принципиальное отличие — в LSI процессор не строится на одном чипе. Можете почитать https://ru.wikipedia.org/wiki/Электроника-60 — Процессоры M1 и M2 — это LSI.

> На ней мог работать и нормально функционировать только RT11SJ
Точнее — не мог. :-) Ибо ему нужен обычный текстовый терминал. Так что кто-то пропатчил.
С моей помощью туда FORTH воткнули — это и язык и полноценное окружение для разработки программ, фактически — простейшая операционка. Если помните игрушки «Ну-погоди», они на форте писались.

>ОС бы заняла большую часть ОЗУ, оставив несколько КБ для запускаемых приложений.
ОШИБАЕТЕСЬ. DOS-11, RSX-11 (A.B.C.S) встали бы прекрасно. Можете сами нагуглить размеры ядер. Ну разве что DOS-11 был нужен диск, которого на БК вроже не было. флопа ему было мало.

А вы что, никогда в жизни тесты не запускали? С перфоленты или магнитной ленты? Вот там как раз RSX-11 C (для перфоленты) и S (для магнитной ленты). Если не путаю — 16Кбайт им за глаза и за уши было. Или даже 8. Но на машинке с 16Кбайт лучше всего жил FORTH. Причем загружаемый с перфоленты.

> Но мы тут про практику вроде говорим.
Ну так приводите КОРРЕКТНЫЕ примеры,. Аналог 11/03 — это Электроника-60. Аналогичная по мощности машина — СМ-3.
Часть пятая. Про перенос.

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

Предположить — МОЖЕТЕ. Поскольку в книжке была речь ещё и про ЕС ЭВМ и БЭСМ-6 перенос под разные окружения там рассматривался. Но основная проблема переноса была не в этом а в разной реализации стандарта.

Чуток примеров.
1) СУЖЕНИЕ СТАНДАРТА
DOS-11 — оператор DATA должен был находиться перед всеми операторами.
RT11, RSX11 — сужений НЕТ.

2) РАСШИРЕНИЕ СТАНДАРТА. целые типы.
DOS-11 — INTEGER*2
RSX11 — INTEGER*2, INTEGER*4
RT11 — расширений нет

3) ПЕРЕНОС с фортрана СТ
DOS-11 — Массивы с числом измерений больше 3, не имеют аналога
RSX11 — Максимальное число измерений массива равно 7.
RT11 — как в фортране СТ

А окружения… Ну да, номера каналов были разные. Но это — мелкие детали.

Можете сами почитать. Горелик, Ушкова, Шура-Бура «Мобильность программ на фортране»
Ну например — http://bookre.org/reader?file=792219

> Сейчас тоже нельзя просто так взять программу на C++ для Linux, работающую на Cortex-A53, собрать под Bare Metal и запихнуть на STM32F101

Вашу? Нельзя, наверное. Мою? Ну я без особых проблем конверторв RTCM3 запустил. Причем прямо из Windows в STM32F4. В винде отладили, на STM — работает. Ничего сложного не вижу, слой совместимости и не такое может. Скоро основное приложение на STM32 запустится.
Если у нас было 3 метода и мы расширили интерфейс ещё одним методом, то получим 3+4=7 строк в vTable. С учетом скрытых трех методов — это будет (3+3)+(4+3)= 13 строк. А при наследовании 3+1 — 4 строки.

Это особенность конкретной реализации.

Это особенность IUnknown::AddRef — https://msdn.microsoft.com/en-us/library/windows/desktop/ms691379(v=vs.85).aspx

«Increments the reference count for an interface on an object. » — то есть счетчик ссылок имеется у интерфейса, а не у объекта. То есть в COM должна быть обеспечена возможность иметь несколько счетчиков ссылок у одного объекта.

Вот тут, например http://hghltd.yandex.net/yandbtm?fmode=inject&url=http%3A%2F%2Fwww.libros.am%2Fbook%2Fread%2Fid%2F105088%2Fslug%2Fsushhnost-tekhnologii-som-biblioteka-programmista&tld=ru&lang=ru&la=1471780992&tm=1472365095&text=Java%20-%20%D0%BB%D1%83%D1%87%D1%88%D0%B5%20%D1%80%D0%B5%D0%B0%D0%BB%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F%20COM-%D0%BC%D0%BE%D0%B4%D0%B5%D0%BB%D0%B8&l10n=ru&mime=html&sign=c0bd82c6b181e7e60849cc6d8fd33406&keyno=0
Утверждается, что JAVA реализует COM. К сожалению, сохраненная копия — доступ к оригиналу в РФ запрешен.
«Increments the reference count for an interface on an object. » — то есть счетчик ссылок имеется у интерфейса, а не у объекта. То есть в COM должна быть обеспечена возможность иметь несколько счетчиков ссылок у одного объекта.

Это написано про интерфейс, а не про реализацию. Да, пользователь интерфейса должен быть написан так, будто у каждого интерфейса свой счетчик — вызов у одного интерфейса AddRef, а у другого Release даст UB, даже если они указывают на один и тот же объект. Это сделано для того, чтобы можно было "наследоваться" от компонентов через агрегацию.


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

не приказывает. Но… интерфейс может требовать отдельного ресурса, не блокируемого всем объектом.

Например класс реализует ком-порт. Один интерфейс — для задания скорости, другой — для передачи данных. Открывается порт, когда мы открываем интерфейс передачи данных. А когда делаем Relaease — порт закрывается, зато можно получить интерфейс для изменения скорости.

То есть, если жестко использовать одну копию IUknown — мы лишаемся некоторых возможностей. Лишаемся — ради экономии десятка слов. А далее появляется какой-нибудь из стандартов (ну скажем один из https://ru.wikipedia.org/wiki/OPC ) завязанный, на раздельный подсчет ссылок. И всё, становится очень неудобно его реализовывать.

А предусмотреть на этапе разработки структуры vTable, что там будет в будущих стандартах — это сложно. Проще не выпендриваться и сделать как у всех.

> Но никто не приказывает тем, кто реализует интерфейсы, действительно использовать отдельный счетчик для каждого.
Вы действительно прочти все описания для всех имеющихся в данный момент COM-интерфейсов? :-) я только пару сотен читал.
Например класс реализует ком-порт. Один интерфейс — для задания скорости, другой — для передачи данных. Открывается порт, когда мы открываем интерфейс передачи данных. А когда делаем Relaease — порт закрывается, зато можно получить интерфейс для изменения скорости.

Весь мой опыт проектирования кричит мне, что передачу данных тут надо делать в отдельном объекте, а не в том же самом.

Два объекта для одного порта? А почему 2, а не 5 и не 10?

Очень интересно логику услышать. Почему во всех известных мне ОС COM-порт — это один объект (handle) со многими интерфейсами, а вы предлагаете несколько объектов.

Из-за какой причины вы решили избавится от инкапсуляции? Вашим двум (или больше) объектам придется показывать наружу очень многое. Что вы получите взамен?

xorl %edi, %edi

гарантированно обнуляет %rdi, потому что в IA32 поведение 32битных инструкций в long mode — zero extend.
Sign up to leave a comment.