Хорошие новостиTM ждут пользователей gcc при переходе на версию 6.1 Код такого вида (взят отсюда):
class CWindow {
HWND handle;
public:
HWND GetSafeHandle() const
{
return this == 0 ? 0 : handle;
}
};
«сломается» — при вызове метода через нулевой указатель на объект теперь может происходить разыменование нулевого указателя, потому что компилятор теперь может просто взять и удалить проверку. Код, конечно, с самого начала сломан, а gcc 6.1 его только немного доломает.
Проверять удобно на gcc.godbolt.org В настройках укажем -O2
Код для начала будет такой:
struct CWindow {
CWindow() : handle() {}
int handle;
int GetSafeHandle() const
{
return (this == 0) ? 1 : handle;
}
};
int main()
{
CWindow* wnd = 0;
return wnd->GetSafeHandle();
}
Выбираем в списке gcc 6.1 и получаем…
main:
movl 0, %eax
ud2
ОХ ЩИ~ Сработала подстановка тела функции по месту вызова, gcc заметил разыменование нулевого указателя и добавил вызов __builtin_trap(), который затем привел к появлению в машинном коде «недопустимой инструкции», которая в свою очередь при работе программы должна приводить к ее аварийному завершению.
Для сравнения gcc 5.3 для того же кода выдает:
main:
movl $1, %eax
ret
gcc 5.З здесь не замечает разыменование нулевого указателя и компилирует «как написали».
Чтобы, наконец, получить код со сравнением указателя, добавим на CWindow::GetSafeHandle() атрибут __attribute__ ((noinline)), чтобы запретить подстановку кода в место вызова. Объявление метода будет выглядеть так:
int GetSafeHandle() const __attribute__ ((noinline))
Теперь gcc 5.3 выдает такой машинный код:
CWindow::GetSafeHandle() const:
testq %rdi, %rdi
je .L1
movl (%rdi), %eax
ret
.L1:
movl $1, %eax
ret
main:
xorl %edi, %edi
jmp CWindow::GetSafeHandle() const
Здесь в самом начале GetSafeHandle() выполняется сравнение указателя this с нулем (инструкция testq) и условный переход (инструкция je). Для сравнения gcc 6.1:
CWindow::GetSafeHandle() const:
movl (%rdi), %eax
ret
main:
xorl %edi, %edi
jmp CWindow::GetSafeHandle() const
Здесь никакого сравнения нет – сразу выполняется разыменование. Это и есть то самое отличие, которое доломает код, «успешно работавший» многие годы и десятки лет.
Отдельного внимания заслуживает использование регистра rdi. Вызывающий код обнуляет edi – половину rdi, а вызываемый код ДОВОЛЬНО НЕОЖИДАННО – использует наполовину обнуленный rdi. Конечно, в этом нет никакого смысла, но поскольку код изначально содержит неопределенное поведение (разыменование нулевого указателя), к компилятору никаких претензий быть не может – компилирует как сочтет нужным, Стандартом не запрещено.
Новое поведение задействуется по умолчанию, начиная с уровня оптимизации O1. Оно отключается параметром -fno-delete-null-pointer-checks – поведение становится таким же, как в gcc 5.3
Читатели, возможно, негодуют – опять компилятор «ломает» код и не выдает предупреждений! Выдает, но не очень. Предупреждение -Wnonnull-compare («заведомо ненулевой this сравнивается с нулевым указателем») выключено по умолчанию, его можно включить, указав -Wall. В коде ниже оно выдается в зависимости от наличия дополнительных пар скобок вокруг сравнения:
int GetSafeHandle() const __attribute__ ((noinline))
{
if (this == 0) // -Wnonnull-compare
return 1;
if((this == 0))
return 1; // нет предупреждения
return this == 0 ? 1 : handle; // -Wnonnull-compare
return (this == 0) ? 1 : handle; // нет предупреждения
}
Такое влияние скобок — это явно ошибка в gcc.
Кроме того, если убрать вызов GetSafeHandle() из main(), предупреждение также более не выдается – компилятор знает, что единица трансляции одна и этот код заведомо не вызывается, код функции удаляется раньше, чем отрабатывает поиск сравнений this с нулевым указателем. Решение, выдавать ли предупреждение, принимается «слишком поздно» — в момент, когда код удален.
Предупреждение -Wnonnull-compare не выдается, если используется параметр -fno-delete-null-pointer-checks
Теперь clang…
Начиная с версии 3.5 с настройками по умолчанию clang выдает -Wtautological-undefined-compare на фрагменты:
if (this == 0);
(this == 0) ? 1 : handle;
if(this != 0);
(this != 0) ? handle : 1;
и -Wundefined-bool-conversion на фрагменты:
if(this);
this ? handle : 1;
if(!this);
!this ? 1 : handle;
При этом до версии 3.8 включительно (3.8 — самая новая выпущенная версия на данный момент) сравнение не удаляется (кроме случая, когда код функции подставляется в место вызова и оптимизируется с окружающим кодом).
Компилировать старый или беззаботно написанный новый код с неопределенным поведением становится все интереснее и интереснее.
Дмитрий Мещеряков,
департамент продуктов для разработчиков
Комментарии (169)
grechnik
25.08.2016 02:47+5Отдельного внимания заслуживает использование регистра rdi. Вызывающий код обнуляет edi – половину rdi, а вызываемый код ДОВОЛЬНО НЕОЖИДАННО – использует наполовину обнуленный rdi.
Инструкцияxor edi,edi
в 64-битном режиме обнуляет весьrdi
, так что нет никакого «наполовину обнулённого» регистра.
В проекте он, конечно, описан, но ведь документацию читают только ламеры ©DistortNeo
25.08.2016 13:16+3Инструкция xor edi,edi в 64-битном режиме обнуляет весь rdi, так что нет никакого «наполовину обнулённого» регистра.
Да, любая инструкция с 32-битными операндами будет обнулять старшие 32 бита. Просто trade-off между скоростью работы и сложностью аппаратной реализации регистров.
А почему именноxor edi, edi
а неxor rdi, rdi
: да потому что первая команда на 1 байт короче.tyomitch
25.08.2016 13:34+3Это не trade-off — это фича, важная для OoOE. Если не обнулять верхнюю половину, то следующая после
xor edi, edi
инструкция, использующаяrdi
, будет иметь зависимость и отxor
, и от предшествовавшей инструкции, использовавшейrdi
. Лишние зависимости делают невозможными перестановки инструкций.DistortNeo
25.08.2016 13:48-1Это не trade-off — это фича, важная для OoOE. Если не обнулять верхнюю половину, то следующая после xor edi, edi инструкция, использующая rdi, будет иметь зависимость и от xor, и от предшествовавшей инструкции, использовавшей rdi. Лишние зависимости делают невозможными перестановки инструкций.
В таком случае, достаточно было бы обнулить верхнюю половину регистра самостоятельно (xor rdi, rdi).
Дело не столько в зависимостях, сколько в необходимости сохранения (=копирования) верхней части регистра одновременно с выполнением операции над нижней частью, а это усложнение конвейера и реализация дополнительных ФУ. Никогда не замечали, что операции с 32-битными целыми выполняются быстрее, чем с 8 и 16-битными целыми?DistortNeo
25.08.2016 15:56+2Несогласные — пишите, почему и где я не прав, мне самому уже интересно стало. Что из нижеперечисленного неверно?
1. Если бы xor edi, edi не обнуляла верхние 32 бита, то пришлось бы пользоваться xor rdi, rdi. И дело даже не в OoOE, а в том, что потом с нулём сравнивается rdi целиком, а не нижние 32 бита.
2. Зависимость от предыдущего значения регистра — это плохо. Из-за этого 8 и 16-битные команды могут работать медленнее, чем 32-битные.
3. Необходимость копирования частей регистров — это и есть дополнительные операции, генерируемые микрокодом и мешающие оптимизации выполнения команд.
4. В AVX похожая ситуация с обнулением верхней части регистров. И причина этому — регистры YMM были реализованы как пара виртуальных XMM-регистров, отмапленных на «физические» регистры, либо на нулевую константу. Из-за этого задача сохранения половины регистра усложняла поток операций, генерируемых микрокодом, в т.ч. из-за необходимости отдельного копирования половинок регистров.chabapok
25.08.2016 16:51Не претендую на точность, но лично мне логика подсказывает, что:
если операция movl (%rdi), %eax аппаратно не зависела бы от xor edi,edi — при реореринге в %eax появится значение взятое из предыдущего адреса, который был в регистре. Это наводит на мысль, что такое movl наверное зависит от такого xor. То есть это не оптимизация ООЕ, а просто байт сэкономили.
Antervis
25.08.2016 05:56+4а каким адекватным способом вообще можно докатиться до того, что this будет равен 0?
DmitryBabokin
25.08.2016 11:32+1A()->B()
Если в A() произошла ошибка, он может вернуть nullptr. B() может просто проверять this на nullptr и корректно отваливаться. Когда цепочка из большого количества вызовов, это удобно. Альтернатива — это исключения возбуждать, либо проверять после каждого вызова результат. И то, и другое решение так себе. В итоге люди проверяют this на ноль.DistortNeo
25.08.2016 14:52+4Если в A() произошла ошибка, он может вернуть nullptr. B() может просто проверять this на nullptr и корректно отваливаться.
А тут опа — множественное наследование (см. одну из ссылок в посте). И B() уже работает с this по адресу 4 или 8, а не 0.DmitryBabokin
25.08.2016 15:08+1Это всё понятно. Но когда такое используют, то обычно понимают такие моменты. Вообще, очень часто множественного наследование запрещено в coding standards.
Я не спорю, что тут есть свои подводные камни, я просто привожу пример когда это вполне уместно, если не считать что это UB.DistortNeo
25.08.2016 15:14Что же это за coding standards, которые разрешают сравнение this с нулём и исключения в деструкторах?
В VCL, кстати, множественное наследование разрешено, но только если оно не меняет адрес объекта (т.е. VCL объект должен идти всегда первым), а остальные классы не имеют полей.DmitryBabokin
25.08.2016 15:19Про исключения в деструкторах я не говорил. Мало ли какие поля не инициализированы (и это нормальное состояние структуры данных). Тут всё зависит от предметной области и такое бывает вполне оправдано, не все поля бывают прибиты гвоздями к полу и инициализированы. Хотя лично я бы постарался это обрабатывать как-то иначе.
Jef239
25.08.2016 13:49А это стандартная штука. Уже описывал чуть выше. Есть класс, представляющий окно. И есть го члены — указатели на объекты полей ввода и кнопок.
Словили исключение посредине конструктора — и всё. Половина указателей nil — половина инициализировано.
А ещё хуже — исключение из деструктора, когда сам объект ещё числится в списках. Из списков его должен был вычистить деструктор, а деструктор до конца не отработал. Как вариант — памяти не хватило (она бывает нужна для изменения числа элементов в динамическом массиве).
Это примеры из библиотеки VCL из Dephi и C Builder. Такие методы — не панацея, но сильно увеличивают шансы, что локальная нехватка памяти не приведет к завершению программы.DistortNeo
25.08.2016 14:27Нехватка памяти практически всегда фатальна. В этом случае нужно как можно корректнее завершить приложение, а не пытаться продолжать работать.
А ещё хуже — исключение из деструктора, когда сам объект ещё числится в списках.
Обращение программистам: никогда не бросайте исключения в деструкторе. Даже если очень хочется. Создайте метод Close или Dispose, но руки прочь от деструкторов. Рассматривайте деструкторы как механизм аварийного удаления объекта.
Принцип RAII хорош, но не может быть применён для всего, чего только можно.Jef239
26.08.2016 06:59+1> никогда не бросайте исключения в деструкторе
+100500. Но это не повод переписывать сотни строк библиотеки чужой библиотеки.
Исключение в деструкторе обычно имеет причиной порчу данных до деструктора. И для надежной программы полезно не только исправить ошибку, но и обработать это исключение. Точнее сначала написать восстановление после такой ошибки, а уж потом — исправлять её. Подробности я написал чуть ниже.
olegator99
25.08.2016 18:23+3Исключение посредние конструктора мой любимый вопрос на собеседованиях. Если конструктор выкидывает исключение, то объект считается не созданным, и грубо говоря обращение к этому объекту это UB.
Если в конструкторе выделяется память, и внезапно произошло исключение, то это обязанность конструктора исключение перехватить, освободить, все что уже успел выделить, и прокинуть исключение наверх.
А если у вас в проекте есть код, который перехватывает исключение, кидаемое конструктором, и начинает потом разбираться с объектом, который не создан я очень рекомендую его переписать.
Jef239
26.08.2016 06:48+2Все верно, за исключением нескольких моментов.
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? Да ещё не в вашем коде, а в вызове чего-то системного?
olegator99
26.08.2016 08:34Последний раз что то делал на паскале и borland c++ 3.1 20 лет назад. Не буду врать, что и как было — не помню.
- Что бы не писать кучу проверок возможно использовать 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++ и использование его на конкретной платформе с конкретным процессором. Вариантов может быть много.
Jef239
26.08.2016 10:09+12) Не всегда помогает. Если у вас десяток объектов и они имеют ссылки друг на друга — все так просто не получится. Ещё раз повторю, что это я про оконную библиотеку и конструкторы классов, представлющих окна. При всем великолепном дизайне 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» есть везде, где есть виртуальная память.
tyomitch
26.08.2016 12:55Word — как раз хороший пример, как большая корпорация с кучей грамотных программистов не смогла этого сделать,
Не «не смогла сделать», а «не стала делать».
Цена сбоя в Ворде — не рулон стали, а всего-навсего чертыхнувшаяся секретарша.
Не думаю, что много бы нашлось желающих покупать «неубиваемый Ворд» по цене софта для управления производством 24*365.Jef239
26.08.2016 13:29Стоимость разработки Word — в тысячи раз больше, чем нашего софта 24*365. У нас там всего лишь десяток человеко-лет. Word — это 20 лет разработки и тысячи программеров. Стоимость всего нашего проекта — это годовая зарплата ведущего программера в микрософте.
При том, что Windows сама по себе поддерживает структурные исключения, их реализация в VC++ — не больше человеко-года. Реализация в прикладном коде — человеко-месяцы.
Так что про подъем цены — не надо. В ворде огромное количество редко используемых фичей. Они многократно делали то, что нужно лишь 1% пользователей. Например — купили программу для ввода математических формул.
Так что именно, что не смогла. Потому что после десятого вылета ворда — любой покупатель начинает смотреть в сторону конкурентов. Так что маркетинговое преимущество тут сильное. Скорее всего ни наткнулись на амбиции компиляторщиков.grechnik
26.08.2016 15:42+1Какие ещё «амбиции компиляторщиков»? В VC++ есть преобразование SEH -> C++ exception: _set_se_translator.
В Word просто пошли по другому пути: не делать сложных манипуляций при access violation — тем более, что если AV вообще произошло, то в данных вполне может быть каша, а получить документ с кашей намного неприятнее, чем потерять последний сеанс работы — а периодически делать временную копию, пока данные гарантированно целы, при AV вылетать, а после перезапуска предлагать её восстановить.Jef239
26.08.2016 16:20СПАСИБО, не знал.
То есть самого преобразования нету («Не существует функции-преобразователя по умолчанию».), но его можно написать. Вопрос лишь в том, в какой версии VC++ оно появилось. Видимо слишком поздно, когда уже слишком дорого было рефакторить.
Ну при порче памяти шансы получить кашу есть и без всякого AV. И большие. Если у объекта 70% данные, а 30% указатели — то шансы 70%, что локальная порча одного слова памяти не приведет к AV.
Так что это ложные страхи.
grechnik
26.08.2016 16:50Просто перехватывать SEH-исключения на С и C++ можно было ещё с момента появления этих самых SEH-исключений в Windows 95, через синтаксис __try / __except / __finally.
Jef239
26.08.2016 21:38Глянул — аж с 1993его года https://accu.org/index.php/journals/1771
Ну тогда я разработчиков Word вообще не пониманию. Неужели они надеялись отладить свой код полностью? Или у них к 1993ему году настолько много было унаследованного кода, что они так и не смогли решиться на рефакторинг?
Как вариант — они так и не перетащили ворд на C++ с обычного Си.
Гм, прямо хоть приятелю в микрософт пиши…
olegator99
26.08.2016 13:37Так кроме unique_ptr еще есть weak_ptr и shared_ptr, совместно на них можно строить весьма сложные конструкции. Про VCL тоже врать не буду — последний раз что то на ней делал 12 лет назад…
Даже если по каким то причинам использовать умные указатели не представляется возможным/не хочется, то всегда можно написать руками код, который будет гарантированно корректно работать (исходя из предположения, что никто другой не портит кучу и в ОЗУ не случается аппаратных ошибок). Если у вас код в других местах портит кучу, то это повод задуматься о тотальном рефакторинге — хоть в тысяче мест вставляйте проверку на 0, не поможет.
А если аппаратные проблемы с ОЗУ, то тут даже не знаю что сказать.
- Как вы верно заметили, сигналы это элемент стандарта POSIX. Есть куча платформ, начиная от win32, кончая железками на cortex-m где нет POSIX, и как следствие сигналов.
Про АДА — в разных языках — разный подход. Но кажется, к обсуждаемому C++это отношения не имеет.
Система типов включены в стандарт языка С. Аппаратные исключения — нет. Такой вот стандарт.
Математические вычисления не зависят от ОС, а вот обработка исключений еще зависит и от ОС.
Есть аппаратная платформа, есть ОС, есть user space, есть GUI фреймворк и наконец есть язык программирования с его рантаймом.
Вы по большей части описывается специфичное для вас сочетание вышеперечисленных компонентов.
Но в любом сочетании нельзя писать программу, которая может нечаянно обратиться к произвольному адресу, перехватить исключение, и дальше продолжить работу.
Нет никаких гарантий, что обращение
B *b = nullptr; b->xxx = 123;
вызовет исключение, например если размер класса B большой, и смещение xxx больше размера защитной страницы, то эта конструкция отработает не вызвав исключения, однако попортив что угодно в памяти!
Jef239
26.08.2016 15:17-2>Если у вас код в других местах портит кучу, то это повод задуматься о тотальном рефакторинге
Тотальный рефакторинг из-за каждой мелкой ошибки? Ошибку нужно просто ловить и править. А ДО исправления сделать так, чтобы она не разрушала всю программу. Типичные сценарии порчи кучи — это ссылка на удаленный объект, двойное выполнение деструктора и так далее. В хорошо отлаженный проектах этого нет. А на этапе отладки — бывает.
По опыт работы с 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 показывает, что во всех крупных проектах есть ошибки. Более того, есть достаточно много ошибок.
И по мере роста размера проекта — становится выгоднее вкладываться в общую надежность, чем в поиски редких и редчайших ошибок.
А гарантии дает только страховой полис. В программировании гарантий нет, есть только вероятности.
olegator99
26.08.2016 16:01Списочек можно? Только реальный. На 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 для микроконтроллеров.
Jef239
26.08.2016 22:16Ну не могу сказать, что возведение в вещественную степень на машине без аппаратной плавающей точки — сильно компактная либа. Я уж не говорю о библиотеке ввода вывода (<< и >> или printf). С другой стороны, поддержка _try _except _finally — намного компактней, чем эмулятор плавающей точки. Так что это не аргумент. Не хотите — не используйте.
мы на FreeRtos используем упрощенный вариант printf, ибо библиотечный мало того, что огромен, он ещё и требует наличия new, то есть кучи. А кучи у нас там нет.
Да и с одинаковым исполнением на разных машинах у С++ засада. Ибо размер int — везде разный. И начинаются танцы с бубнами типа stdint. И такая же засада с long double, где количество бит — от 64 до 128, в зависимости от железа.
Не говоря уж о том, что в каждом компиляторе — свои погремушки.
Так что пока не вижу у вас серьезных аргументов, почему вещественном переполнении программа должна вылетать.
Сами прерывания плавающей точки — определены в стандарте https://en.wikipedia.org/wiki/IEEE_floating_point#Exception_handling А вот их обработка — уже идет сигналами.
Как в анекдоте про вильку, тарельку и бутильку «умом понять это невозможно, это можно только запомнить».olegator99
26.08.2016 22:59В системе целочисленных типов в C++11 все очень четко прописано. Если требуется конкретная разрядность используется intXX_t/uintXX_t, если требуется переменная, с которой процессору удобнее работать, используйте int. Где засада то?
Речь шла об исключениях вызванных UB. Историю про FP исключения наверно тоже можно обсудить, но это уже далеко выходит за исходного обсуждения про UB.
Пытаюсь вас убедить, что код, ловящий null pointer derefernce через исключения операционной системы в какой то момент может сломаться, а вы лишитесь возможно важного круга защиты вашей программы, ибо по стандарту языка null pointer dereference это UB, а не сегфолт.
Jef239
27.08.2016 13:50>Где засада то?
В 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) Ошибки исправлютсяDistortNeo
27.08.2016 15:29Знаете, вещественная арифметика ещё как ломается.
Либо вы смиряетесь с тем, что железка и/или компилятор под неё имеют ошибки в реализации и пишете код с учётом этих ошибок, либо не используете аппаратный FP.
Можно ещё писать код из предположения, что память или устройство хранения данных может аппаратно сбоить.
Почти любая ошибка у меня будет поймана, корректно обработана и не повлияет на работу программы.
Вопрос исключительно в стоимости написания кода (дополнительная логика для обработки ошибок) и его производительности (куча проверок против скорости выполнения) против ущерба от ошибок.
Слои защиты
1) assert — это вывод отладочного сообщения + abort, а не продолжение работы программы.
2) У меня другой подход к данному вопросу. Исключения не должны использоваться для обработки штатных ситуаций. Например, ошибка передачи данных по сети — штатная ситуация, а нехватка памяти — нештатная, т.к. в большинстве случаев приводит к невозможности продолжения штатной работы. Поэтому оператор new должен выдавать исключение, а не nullptr.
3) Да, нужны.
4) Если у вас может быть вызван десктруктор с this == nullptr, вы что-то делаете совсем не так.
5, 6) Возможно, но это все равно может привести к частичной потере данных. Ворд, например, так и делает.
7) Не всё программирование серверное.Jef239
27.08.2016 23:22> Либо вы смиряетесь с тем, что железка и/или компилятор под неё имеют ошибки в реализации
Либо исправляем железку. За ЕС-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) Не всё программирование серверное.
Перезапуск приложения можно и на клиенте сделать. Только от потери питания спасать не будет. И от аварии жесткого диска. Но от программных ошибок — спасти может.
mayorovp
28.08.2016 06:09«Ох уж эти сказки, ох уж эти сказочники...» (с) падал прошлогодний снег
http://delphi-box.ru/assert-delphi.html
procedure Assert (expr: Boolean [; const msg: string]);
Если проверяемое утверждение будет ложным, то процедура прекратит работу и сгенерирует исключение EAssertionFailed с выдачей ошибки в сообщении.Давайте все-таки использовать общепринятые термины, а не то как их поняли разработчики конкретной библиотеки?
to assert в переводе с английского — "утверждать". Когда программист пишет assert — он утверждает что некоторое высказывание истинно. Утверждения не надо проверять, им принято верить. Assert — это разновидность комментария, пригодная для автоматического анализа.
То, что утверждения иногда проверяются во время работы — это особенность некоторых библиотек, а не Assertов как таковых.
Jef239
28.08.2016 10:02-1> Давайте все-таки использовать общепринятые термины, а не то как их поняли разработчики конкретной библиотеки?
«Общепринятые» это для какого языка? Когда пишешь на дюжине языков и читаешь полсотни — привычки отдельных любителей VC++ как-то не принимаешь во внимание.
Рекомендую ознакомиться хотя бы с вики — https://en.wikipedia.org/wiki/Assertion_(software_development)
Как правило, assert вызывает исключения, в С за неимением полноценных исключений, использовались сигналы, а в С++ просто оставили сишный вариант.
> То, что утверждения иногда проверяются во время работы — это особенность некоторых библиотек, а не Assertов как таковых.
static_assert — это отдельная сущность, введенная в С++11.
Да, конечно, можно рассматривать assert как разновидность контракта. Но в большинстве языков это именно динамически проверяемая разновидность.
Хотите — сделайте сравнительный обзор языков программирования по семантике assert.mayorovp
28.08.2016 10:40+1C++: макрос assert из стандартной библиотеки отключается в релизной сборке (при определенном макросе NDEBUG)
C#: метод Debug.Assert отключен в релизной сборке (без определенного ключа DEBUG)
Java: ключевое слово assert ничего не делает если при запуске не указать параметр -enableassertions
Javascript: тут console.assert не отключается. Но при этом он (в браузерном варианте) и не останавливает работу скрипта — то есть, опять-таки, ничего не защищает.
Какие там еще популярные языки есть?
Jef239
28.08.2016 11:37-2> C++: макрос assert из стандартной библиотеки отключается в релизной сборке
Плиз, описание «релизной сборки» из стандарта. :-) Релизная сборка — особенность VC++, в gcc её нету
> Какие там еще популярные языки есть?
Гм, я вам про Фому, вы мне про Ерёму.
Я вам про выполнение assert во время исполнения программы, а не во время компиляции. А вы про то, что его можно отключить. Да, отключить можно. Но если программист писал assert грамотно — это не нужно.
Как сказал кто-то из классиков «У любителей отключать проверки после отладки надо отключать тормоза на машине после обучения вождению».DistortNeo
28.08.2016 12:04+1Релизная сборка — это не особенность VC++, это просто автоматизация рутинных действий по выставлению ключей компилятора и значений условной компиляции.
При реализации вычислительных алгоритмов, например, обработки изображений, разница между версиями с включёнными и выключенными проверками может быть очень существенна.
Схема такая: программа упала или тесты показали неправильный результат — включаем проверки — ищем ошибку — исправляем ошибку — запускаем заново.Jef239
28.08.2016 12:58-2> Релизная сборка — это не особенность VC+
Ну куча juniorов считают её вообще особенностью языка Си. :-)
> разница между версиями с включёнными и выключенными проверками может быть очень существенна.
ну чайники ещё и не такие ошибки делают. Чтобы разница была существенной, нужно воткнуть assert внутрь часто исполняемого цикла. Или внутри assert вызвать что-то трудоемкое. Для junior простительно, для midle уже баг.
> Схема такая: программа упала или тесты показали неправильный результат — включаем проверки
Вы на машине так же ездите? Пока не попали в аварию — тормоза отключены?
> включаем проверки — ищем ошибку
И сколько времени вам потребуется, чтобы найти ошибку. проявляющуюся раз в 3 года? Как только у вас большая система с десятком нитей — вы можете искать до посинения.
Я уж не говорю о том, что при отключении оптимизации многое ошибки просто пропадают. Дело не только в ошибках оптимизатора. Бывает, например, что оптимизатор превращает переменную в константу. Передаем указать на int32, функция трактует его как указатель на int64 и портит следующее слово. Но с оптимизацией и без неё — это разные слова.
Мы как-то 2 часа убили, пытаясь понять, почему закомментирование одного, не выполняемого в тесте куска кода влияет на выполнение совсем другого куска. Как оказалось — при закомментировании убиралось присваивание и компилятор превращал переменную в константу.
Даже просто изменение скорости машины влияет на ошибки. Как пример — одна из функций в VCL в delhpi 7 сбоила только на медленных машинах. Чуть побыстрее — и все проходило штатно.
Но плюс в вашей схеме есть — она позволяет разработчикам и тестерам работать менее эффективно и тем самым — получать больше денег при том же уровне отлаженности приложения.DistortNeo
28.08.2016 13:42+1ну чайники ещё и не такие ошибки делают. Чтобы разница была существенной, нужно воткнуть assert внутрь часто исполняемого цикла. Или внутри assert вызвать что-то трудоемкое. Для junior простительно, для midle уже баг.
Простой пример: обработка изображений, а именно проверка на выход за границы изображения при доступе к пикселю. Это очень часто встречающаяся ошибка, поэтому приходится втыкать assert внутрь GetPixel.
Но для хорошо отлаженных алгоритмов: свёртка, сумма, фурье там всякие — эта проверка избыточна и должна быть отключена. А вот для новых алгоритмов — включена.
Вы на машине так же ездите? Пока не попали в аварию — тормоза отключены?
Только это авария с нулевыми потерями — как в играх, а не реальной жизни.
И сколько времени вам потребуется, чтобы найти ошибку. проявляющуюся раз в 3 года? Как только у вас большая система с десятком нитей — вы можете искать до посинения.
Мне не нужна поддержка кода. Написал код — получил результат — забыл о коде.
Бывает, например, что оптимизатор превращает переменную в константу. Передаем указать на int32, функция трактует его как указатель на int64 и портит следующее слово
А зачем пытаться изменить константу, да и вообще, делать действия, не предусмотренные стандартом, а затем бороться с багами оптимизатора? Просто интересно.
Даже просто изменение скорости машины влияет на ошибки. Как пример — одна из функций в VCL в delhpi 7 сбоила только на медленных машинах. Чуть побыстрее — и все проходило штатно.
Попробую угадать: в те времена процессоры были однопоточные, нормальных высокоуровневых средств синхронизации не было, вот программсты и писали кривой многопоточный код.
Но плюс в вашей схеме есть — она позволяет разработчикам и тестерам работать менее эффективно и тем самым — получать больше денег при том же уровне отлаженности приложения.
Типичная проблема современности: заказчик хочет сырой код с багами прямо сейчас, а не отлаженный код потом, и платит за это деньги. Его право.Jef239
28.08.2016 14:20> проверка на выход за границы изображения при доступе к пикселю.
4 assert на крайние точки. Исполняются однократно. Остальное — математикой.
> Только это авария с нулевыми потерями — как в играх, а не реальной жизни.
Цена остановки стана — 40 тысяч долларов (рулон стали) улетает в брак. Худшее, что можно сдедать — это взорвать печь, работающую на водороде. С учетом того, что собственное рабочее место при отладке над печью, шанс выжить отрицательные.
И это мы ещё не управляли станом, а лишь делали «черный ящик» (+ система визуализации и отладки) для контроллеров, станом управляющих. Но шансы при ошибке вмешаться в управление — были
> Мне не нужна поддержка кода. Написал код — получил результат — забыл о коде.
Ну если ваш код одноразовый — то бывает и так. Ну или если клиенты одноразовые. Продали лоху халтуру и пошли искать следующего лоха.
> А зачем пытаться изменить константу,
Читайте ВНИМАТЕЛЬНО — «оптимизатор превращает переменную в константу».
.
было bool fatsMode = false; Потом в одной из веток fatsMode = true; Когда закомментарили эту ветку, компилятор перестал выделять память под fatsMode. Ну и все выделение памяти — поехало.
«Попробую угадать: в те времена процессоры были однопоточные, нормальных высокоуровневых средств синхронизации не было»
В те времена процессоры были 2-4 ядерные, а высокоуровневые средства были придуманы лет 20 назад.
Но вот в РЕАЛИЗАЦИИ высокоуровневого средства (ожидания завершения треда) — коллеги из Borland лажанулись. Причем лажанулись так, что у них, на быстрых машинах все хорошо было. А вот на медленных — ошибка встала в полный рост.
> заказчик хочет сырой код с багами
Прямо так в договоре и написано — не меньше 50 багов на модуль? :-)
Такое впечатление, что вы на PHP пишите.
УВЫ, у нас УГОЛОВНЫЙ КОДЕКС.
Ну вот, например
«УК РФ, Статья 217. Нарушение правил безопасности на взрывоопасных объектах
1. Нарушение правил безопасности на взрывоопасных объектах или во взрывоопасных цехах, если это могло повлечь смерть человека либо повлекло причинение крупного ущерба, — 3. Деяние, предусмотренное частью первой настоящей статьи, повлекшее по неосторожности смерть двух или более лиц, — наказывается… либо лишением свободы на срок до семи лет...»
Ну как пример. На одном цементном заводе решили ввести встрой цементный фильтр, не дожидаясь готовности программы. Слава богу, решили САМИ, ни одной наши подписи не было. Итог — не уследили, фильтр щабился цементной пылью и упал. Цементный фильтр — это такая огромная дура на ножках. Под фильтром — рабочие места ЧЕТЫРЕХ человек.
Один был в туалете, один в курилке, двое удрали из под падающего фильтра.
А если бы НЕ УДРАЛИ?
Слава богу — ни одной нашей подписи, разрешающей эксплуатацию без ПО не было. А кто подписал — пошли под суд. Вместе с тем, кто вовремя не проверил фильтр.
Вот один раз в такой ситуации побудете — отучитесь писать код с багами. :-)
DistortNeo
28.08.2016 17:17+14 assert на крайние точки. Исполняются однократно. Остальное — математикой.
Что мешает комбинировать оба подхода: debug-режим — все проверки, в том числе и внутренние, включены, release — включены только проверки входных параметров (например, размеры изображений, выравнивание).
Не очень понимаю, как математика защищает от программистских ошибок.
Цена остановки стана — 40 тысяч долларов (рулон стали) улетает в брак.…
Вот один раз в такой ситуации побудете — отучитесь писать код с багами. :-)
Опять же: вопрос исключительно в цене ошибки. Задачи разные бывают.
В моём случае разработки носят преимущественно теоретический характер. Попробовал метод, применил — не понравилось, закодил следующий.
Задачи в области обработки медицинских изображений, где цена ошибки уже высока, до реальной практики так и не дошли — нет заинтересованных заказчиков.
было bool fatsMode = false; Потом в одной из веток fatsMode = true; Когда закомментарили эту ветку, компилятор перестал выделять память под fatsMode. Ну и все выделение памяти — поехало.
А можете пояснить, каким образом это могло повлиять на работу программы? Желательно конкретным примером.
Например, если вы принимаете на вход функции &int32 и преобразовываете внутри функции в &int64, то будьте готовы к тому, что это UB со всеми вытекающими.
А ещё есть такая подлая штука в оптимизаторах, как strict type aliasing, например.Jef239
28.08.2016 20:58-1> Что мешает комбинировать оба подхода:
МНОГО что
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, это просто БАГ — порча памяти при записи или мусор при чтении. Но баг, проявляющийся в зависимости от закомментирования совсем другого куска кода (куда программа не заходит) это нечто.
DistortNeo
28.08.2016 22:12+2Это я уже давно всё понял — у нас просто сильно разные задачи.
3) Динамическое включение отладки — ненамного дороже включения при помощи дефайнов, зато позволяет прямо в момент ошибки включить отладку и посмотреть, что происходит.
Отладочный вывод — это, несомненно, хорошо. Но для вычислительных задач с детерминированными входными данными без случайных внешних событий это попросту не нужно. Это будет либо убийство производительности (проверка условия на логирование на каждом чихе), либо низкая информативность. Все равно что на JavaScript программировать серьёзные вещи (гусары с nodejs — молчите).
Поэтому для задачи обработки изображений я предпочитаю статическую настройку параметров отладки. Ну а для серверного кода — да, логирование со всеми уровнями важности, по-другому никак.
А выравнивание зависело от той переменной, которую gcc превращал в константу.
Для таких целей обычно используют alignas, а не делают паддинг переменными, делая код крайне непереносимым и зависимым от конкретного компилятора и его настроек.
Это не UB, это просто БАГ — порча памяти при записи или мусор при чтении.
Это UB по стандарту, а не баг.
Решение проблемы:
union { int64 value64; struct { int32 value32; bool b; } }
и передача в функцию &value64, а не &value32.Jef239
29.08.2016 10:59-2> Но для вычислительных задач с детерминированными входными данными без случайных внешних событий это попросту не нужно.
Ну до тех пор, пока не окажется, что на отладочной сборке все хорошо, а на релизной — ИНЫЕ результаты. Например из-за оптимизации. И вот тогда после вставки отладки советую её не убирать, а просто отключить. Ибо один такой баг — это сигнал, что их может быть ещё десяток.
> Для таких целей обычно используют alignas,
КОНЕЧНО. Именно как отсутствие выравнивания это и было квалифицировано. Заменили char objStorage[4096] на что-то вроде double objStorage[512] и все пошло.
> а не делают паддинг переменными,
ЖУТЬ.
> Это UB по стандарту, а не баг.
Баг это. Неверное описание типа. У части API — void *, в обертках надо правильно писать тип.DistortNeo
29.08.2016 12:01+1Ну до тех пор, пока не окажется, что на отладочной сборке все хорошо, а на релизной — ИНЫЕ результаты
Обычно это является проявлением ошибок в программе.
Я только один раз сталкивался с действительно багом компилятора — C++ Builder неправильно генерил код для работы с многомерным массивом (писал 4-байтный float в ячейки, а должен был 8-байтный double).
Заменили char objStorage[4096] на что-то вроде double objStorage[512] и все пошло.
И таким образом, вы снова пришли к UB, т.к. выравнивание типов — implementation specific. Вы надеетесь на компилятор, что он выровнит double по размеру типа, что, вообще говоря, он делать не обязан.
Баг это. Неверное описание типа. У части API — void *, в обертках надо правильно писать тип.
Устранение переменной для экономии памяти — это не баг. Полагаться на расположение переменных в памяти — это тоже UB. Я привёл решение по его исправлению, рекомендуемое по стандарту (union).
Ну а кривое API — да, проблема, но к оптимизатору не относится.Jef239
29.08.2016 12:48> Обычно это является проявлением ошибок в программе.
Чаще — агрессивная оптимизация. Тот же алиасинг.
> Я только один раз сталкивался с действительно багом компилятора
см. выше. Компилятор (плюс библиотека плюс линкер) при одних и тех же настройках сделал два взаимоисключающих действия.
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).
мягко говоря, ваше решение не к той задаче.
0xd34df00d
29.08.2016 23:29Вы на машине так же ездите? Пока не попали в аварию — тормоза отключены?
Я пишу код, который в памяти дробит примерно 120 гигабайт чисел. Прямо сейчас я упёрся в то, что код вида
if (a [i] < threshold) ++firstCounts [b [i]]; else ++secondCounts [b [i]];
является самым узким местом. До этого я выкинул и переписал много чего, чтобы получить текущий уровень производительности, но он меня до сих пор не устраивает. Как думаете, какое влияние неубранные ассерты окажут?Jef239
30.08.2016 02:11-2> какое влияние неубранные ассерты окажут?
НИКАКОГО. Внутрь цикла асссерты ставят только школьники. Профи если и ставят, то под if (i & 0x1000), то есть на каждый 4096ой оборот цикла.
> является самым узким местом.
Прочтите про SSE2 и попробуйте переписать на SSE2. Как минимум — будут полезны инструкции управления кэшем.
Если SSE2 не подходит, то делайте так
Counts[b[i]<<1+(a [i] < threshold)]++;
Тут четные элементы — это secondCounts, а нечетные firstCounts. Выигрыш будет за счет отказа от if, то есть оптимизации наполнения конвеера.
А это что, курсовая какая-то? А то когда я вижу, что человек надеется на оптимизатор и не выносит общие подвыражения явно — мне сразу видится студент. Да и if из цикла старшекурсники должны уметь убирать.
Попробуйте прислать мне в личку, что вы хотите сделать — может и придумаю, как это ускорить.Оптимизатор дает максимум процентов 20, а вот переделка алгоритма иногда и в 1000 раз ускоряет. Правда обычно за счет потребления памяти.0xd34df00d
30.08.2016 02:59+1НИКАКОГО. Внутрь цикла асссерты ставят только школьники. Профи если и ставят, то под if (i & 0x1000), то есть на каждый 4096ой оборот цикла.
Есть функция, которая считает энтропию некоторого массива величин. Её надо ассертом бы покрыть, что массив непустой, не правда ли?
Вот только проблема, что она вызывается в цикле, и что, хотя log2 всяко дольше всяких проверок, но код с ассертом уже будет совсем не то, не правда ли?
Прочтите про SSE2 и попробуйте переписать на SSE2. Как минимум — будут полезны инструкции управления кэшем.
Я даже прочитал. Про conditional moves и всякое такое сначала. И даже уже было начал переписывать код, а потом вспомнил, что i не идёт по всем элементам массива, а только по некоторым, которые едва ли будут идти подряд.
Тут четные элементы — это secondCounts, а нечетные firstCounts. Выигрыш будет за счет отказа от if, то есть оптимизации наполнения конвеера.
А это можно. У меня были похожие идеи, как избавиться от бранчинга, но на момент написания исходного комментария я ещё не вспомнил, что i идёт не по всем элементам массива, поэтому больше думал про SSE.
А это что, курсовая какая-то? А то когда я вижу, что человек надеется на оптимизатор и не выносит общие подвыражения явно — мне сразу видится студент. Да и if из цикла старшекурсники должны уметь убирать.
У меня примат-образование, но не суть. А надеялся бы я на оптимизатор — не долбился бы в профайлер, или долбился бы, но увидел бы этот цикл в качестве горячего места и не думал бы дальше.
Попробуйте прислать мне в личку, что вы хотите сделать — может и придумаю, как это ускорить.
Да можно и не в личку. Random forest я делаю, в данном конкретном месте есть матрица флоатов ||x_{ij}||, где i — объект, j — признак, и есть соответствующий вектор классов y_i. j зафиксировали каким-то образом, теперь для каждого v из множества значений Val(j) этого признака1 пробегаем по объектам и смотрим, сколько объектов каждого из классов имеют значение меньше v (это firstCounts), а сколько — больше (и это secondCounts). Пробегать надо, причем, не по всем объектам, а лишь по некоторым, что определяется неким массивом, из которого берутся i.
1 На самом деле для каждого v из множества { (w_i + w_{i + 1}) / 2 | w_i \in Val(j), i \in {1, .., |Val(j)| — 1} }, но не суть.Jef239
30.08.2016 13:42-1> Есть функция, которая считает энтропию некоторого массива величин. Её надо ассертом бы покрыть, что массив непустой, не правда ли?
Почему? Если не зашли в цикл подсчета энтропии — пусть выдает -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 не лезут в кэш, а получившиеся массивы влезут в кэш — то может ускориться. А если такое «пробегание» используется в нескольких циклах — ускорение может стать приличным.
DistortNeo
30.08.2016 16:27+1Если не зашли в цикл подсчета энтропии — пусть выдает -NaN, получите exception при использовании результата
Вот в этом и заключается отличие debug и release версий.
В release режиме, предлагаемом вами, ошибка поймается где-то позже, причём причину ошибки (конкретная итерация цикла, где произошёл вызов) будет искать довольно трудоёмко.
Если добавить отключаемые assert (=debug версия), то ошибка поймается внутри вызова функции, дальше будет легко подняться по стеку.
Но вас вариант с отключаемыми на этапе компиляции проверками не устраивает по причине возможного внесения оптимизатором багов в код.Jef239
30.08.2016 21:18-2> В release режиме, предлагаемом вами, ошибка поймается где-то позже, причём причину ошибки (конкретная итерация цикла, где произошёл вызов) будет искать довольно трудоёмко.
НАОБОРОТ. Этот вариант применяется когда мы в том же обороте цикла делаем вычисления с вычисленной энтропией. То есть при исключении — мы будем иметь ссылку на строку кода сразу за вызовом подсчета энтропии.
> Если добавить отключаемые assert (=debug версия), то ошибка поймается внутри вызова функции, дальше будет легко подняться по стеку.
Ну может вам и легко понять, в каком из пяти циклом с десятком вызовом данной функции был отказ. А мне — сложно. Я предпочитаю иметь точку отказа там, где произошла ошибка. И если что — вставить в сообщение об ошибках — значения переменных.
> Но вас вариант с отключаемыми на этапе компиляции проверками не устраивает по причине возможного внесения оптимизатором багов в код.
Это лишь одна из причин.
Основная разница ИНАЯ
1) Ваши проверки нужны, чтобы показать заказчику. что ОШИБОК НЕТ.
2) Мои — чтобы НАЙТИ ОШИБКУ, а в идеале — надежно работать НЕВЗИРАЯ на ошибки.
Зачем вам диагностика на машине? Раз в год заехал на СТО, проверился — и всё. А автопроизводители — такие же дураки, как и я, они постоянно проверяют узлы машины.
Вы давно видели падающий лифт? Не видели? Ну и я не видел. Так давайте выкинем из лифта тормоза, сделанные на случай обрыва троса.
Система оцинковки стали — не лифт и не машина. Цена ошибки — НАМНОГО больше. В день выпускается продукции больше миллиона долларов. Это уже ближе к самолетной надежности.
Но надежность самолета — это очень дорого. А то, что я предлагаю (без перезапусков серверов и систем) — стоит КОПЕЙКИ.До 1% времени выполнения + экономия времени программиста на отладке, которая покрывает затраты на времени написания.
Когда вы неделями будете безуспешно воспроизводить какой-нибудь баг, случившийся у заказчиков — вспомните, что могли бы получить от заказчиков не смутные описания, а кусок лога, в котором указана строка кода с ошибкой (а иногда — и со значениями переменных).
А когда вы в очередной раз услышите про глючность микрософт — вспомните, что глючность эта идет от схемы debug-release.
Посмотрите, например, как пишет Бйорн Струстрап — https://habrahabr.ru/company/pvs-studio/blog/270191/ Кусочек называется «This is the place for paranoia». Ну и цитата из Страстрапа " Я считаю, что все серьезные приложения должны использовать такой «параноидальный тест» для отлова «невозможных» ошибок."
DistortNeo
30.08.2016 22:55+2НАОБОРОТ. Этот вариант применяется когда мы в том же обороте цикла делаем вычисления с вычисленной энтропией. То есть при исключении — мы будем иметь ссылку на строку кода сразу за вызовом подсчета энтропии.
Вы же предлагали поставить assert после цикла? А если assert ставить внутрь цикла, то не логичнее ли его разместить один раз внутрь вызываемой функции, а не писать однотипный код после её вызова?
Насчёт всего остального: логи дополняют систему с ассертами, а не заменяют, логи — для удалённого анализа кода. К тому же assert можно поставить в узкое место, а запись в лог — нет.
assert — это не средство проверки, это средство отладки. Грубо говоря, assert-ом проверяются условия, которые по логике никогда не должны случиться, но из-за бага могут.
Пример: проверка граничных условий при обращении к элементу массива. Можно сделать полную валидацию входных значений, но допустить баг либо в самом алгоритме, либо в проверке условий. Тогда assert здесь будет последним рубежом.Jef239
01.09.2016 01:24-2>А если 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% от ущерба.DistortNeo
01.09.2016 01:37+1> Исключение — это в данном случае не assert, это аппаратное прерывание FPU
Чем включение прерываний FPU отличается от включения ассертов?
> Опять сказки, сказки, сказки… нету никакой разницы между assert(ptr == NULL) и if (ptr == NULL) LogPrintf(.....)
Разница в условной компиляцииJef239
01.09.2016 02:27-2> Чем включение прерываний 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, попавших в циклы. Вот ИХ — лучше исключить, заменив на менее трудоемкие варианты. Или под условную компиляцию, если они поставлены по делу.DistortNeo
01.09.2016 02:38+2А теперь поясняю: в большинстве современных языков программирования прерывания FPU недоступны by design. Стандарт IEEE 754 также не рекомендует их использовать.
Включая прерывания FPU, вы рискуете поломать существующий код, написанный в соответствии со стандартом. Но вам ничто не мешает включать прерывания для отладки кода.
Ситуация аналогична ассертам: в релиз-сборке не должно быть ни ассеров (=abort при ошибке), ни прерываний. Но для отладки оба инструмента хороши.Jef239
01.09.2016 03:16-1А если без сказок — читайте 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
Ну есть такое в любой профессии. Иногда люди бояться, что неофиты наступят им на пятки. И чтобы остаться уникальными специалистами — дают заведомо вредные советы. Может не стоит такие книжки читать?
Или вы всерьез верите, что можно написать программу без ошибок?
DistortNeo
01.09.2016 09:52+1> ОК, назовите примеры языков, где 3.5/0. не вызовет исключения. :-)
Java, C#
0xd34df00d
30.08.2016 20:52+1Почему? Если не зашли в цикл подсчета энтропии — пусть выдает -NaN, получите exception при использовании результата. Хотя… это же у вас C++, то есть изначально SIGFPE будет…
Не всегда. Я вот прям сейчас столкнулся с проблемой, когда код рефакторил с вещественных меток классов иunordered_map<Label_t, size_t>
как хранилище их счётчиков на целочисленные метки иvector<size_t>
. Иногда какой-то метки не было вообще, и если for-цикл по мапе просто не попадал в эту метку (ну, её в мапе нет), то цикл по вектору считал логарифм от нуля и получал -NaN. Причём, это был quiet nan, то есть, никаких исключений и падений.
А я ещё долго удивлялся, чего у меня после рефакторинга классификация сломалась (на самом деле не долго).
Придется включать преобразование аппаратных исключений в программные.
Кстати, не знаю, где оно тут включается в моих линуксах.
Хотя затраты на вызов — чуть больше, чем на ассерт.
Так оно инлайнится, я проверял.
И там так сложно все написано, что при любом вызове может быть 0 элементов?
Пример я вам дал выше. Это мне ещё повезло, что я пишу код без абсолютно случайного random seed для воспроизводимости, например, и специфика задачи позволяет такую лажу быстро выявлять. Так может быть не всегда.
Третье решение — это математически верифицировать цикл. То есть доказать, что он не может вызывать энтропию для пустого массива.
Пойду расчехлю Idris, ага. Считать будет до второго пришествия, правда, ну да ладно.
Пятое решение — пусть энтропия для пустого массива выдает -INF. А после окончания вашего цикла — вставить assert на то, правдоподобен ли полученный результат.
Единственный адекватный моей задаче вариант, пожалуй. Правда, тогда это тоже придётся использовать в цикле, потому что уровнем выше отличить адекватность от неадекватности будет существенно сложнее.
Кодировать все-таки должны программеры.
Ну, на плюсах я при этом пишу лет с 12-13 :)
Основных режимов два. Первый — по таймеру посмотреть, в каком месте исполняется программа, второй — насытить код вызовами подсчитывающих процедур.
Можно ещё hardware performance counters дёргать, например.
А если такое «пробегание» используется в нескольких циклах — ускорение может стать приличным.
Неа, только один раз для каждого набора индексов, к сожалению.
Кстати, я тут переписал код с магией в [] вместо ифа, и получилось даже медленнее процента на три, хехе.Jef239
30.08.2016 22:01> Причём, это был quiet nan, то есть, никаких исключений и падений.
А это определяется режимом сопроцессора. см. http://pubs.opengroup.org/onlinepubs/9699919799/functions/fesetexceptflag.html
> А я ещё долго удивлялся, чего у меня после рефакторинга классификация сломалась (на самом деле не долго)
При моем подходе сначала будет понятно, где точка поломки, а уж потом — а что мы этим сломали и сломали ли вообще.
> Ну, на плюсах я при этом пишу лет с 12-13 :)
я даже системного программиста с дипломом литфака педиинститута видел. Ещё на ЕС-1022. Исключения разные бывают.
> Неа, только один раз для каждого набора индексов, к сожалению
А запись пары a и b — не сильно труднее записи индекса. Может от индексов вообще отказаться? Или этот набор индексов для чего-то нужен?
> Кстати, я тут переписал код с магией в [] вместо ифа, и получилось даже медленнее процента на три, хехе.
Оптимизатор мог примерно ту же магию применить. Иногда бывает, что он лучше оптимизирует, чем ручками.
Можно попытаться вывернуть алгоритм (как того пингвина в гвинпина). Сейчас у вас видимо последовательность циклов по объему памяти, превышающему кэш. А можно попытаться все загнать в один цикл — но больше будет процент попадания к кэш. Идея очень сырая, конечно.0xd34df00d
02.09.2016 06:50+1А это определяется режимом сопроцессора. см. http://pubs.opengroup.org/onlinepubs/9699919799/functions/fesetexceptflag.html
А это уже не очень стандартная штука в смысле стандарта C++.
Я, кстати, благодаря этой дискуссии быстренько пробежал стандарт и понял, что не понимаю формулировки error reporting'а для операций с плавающей точкой там. Экзепшоны упоминаются, но quiet или signaling nan (равно как и возможность их настройки вроде указанной вами) — нет.
А запись пары a и b — не сильно труднее записи индекса. Может от индексов вообще отказаться? Или этот набор индексов для чего-то нужен?
О да, ещё как нужен (см. ниже).
Сейчас у вас видимо последовательность циклов по объему памяти, превышающему кэш.
Да, много этих самых гигабайт.
Есть матрица из, скажем, 200 тысяч строк и 40 тысяч столбцов. Каждая строка — объект в обучающей выборке, столбец — определенный признак. Строится решающее дерево, которое должно объекты соотносить классам. Дерево, естественно, рекурсивное, и может либо выдать ответ на текущем уровне, либо передать его одному из дочерних узлов в зависимости от значения данного признака (фиксированного для текущего узла).
И вот когда мы это дерево строим, чтобы найти, с одной стороны, сам признак, максимизирующий осмысленность разбиения, и, с другой стороны, значение этого признака, которым выгоднее всего разбивать, надо пройтись по всем возможным объектам в обучающей выборке (и в кэш оно заведомо не влезет), которые дошли до этого уровня, а затем, после разбиения, рекурсивно запустить процедуру построения дочерних деревьев на подмножествах объектов обучающей выборки (сиречь на подмножествах строк матрицы), соответствующих разбиению, получаемому из выбранного на этом уровне признака и его значения (поэтому индексы нужны, чтобы управлять тем, какие именно объекты рассматривать на текущем уровне, не копировать же эти гигабайты строк матрицы туда-сюда всё время).
Jef239
26.08.2016 15:22Дополню каплю. Страховку в вашем примере дает как раз проверка b на NULL. В большинстве случаев — нулевым является this. Поэтому полезно в отдельных местах проверять, что this не NULL. И если NULL — кинуть исключение. А уж потом программер по логам найдет ошибку и исправит её.
Это как раз та проверка, что выкинута в новом gcc.olegator99
26.08.2016 15:24Не дает.
Вот наглядный пример, почему не дает https://habrahabr.ru/company/abbyy/blog/308346/#comment_9771370Jef239
26.08.2016 15:34Те, кто используют множественное наследование — сами себе злобные буратино.
Но ваша идея понятна. Вы считаете, что надежность программ на С++ зависит только от ошибок программистов. А поскольку ошибки всегда есть — все программы на С++ ненадежны. То есть там, где нужна надежность — там С++ не место. ОК, понял.
marsianin
26.08.2016 22:03this может принять значение 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(); }
Jef239
28.08.2016 00:35Вы так уверены, что сможете вставить ВСЕ проверки? Ну возьмите ну скажем код VCL (оконная библиотека Delphi), вставьте те проверки, что там нету, а я покажу, где реально может возникнуть NULL.
А если ваш собственный код больше 100 тысяч строк — предлагаю спор на миллион долларов. Подменяем new, чтобы оно рандомно выдавало исключение и проверяем, что ни в один метод не придет NULL вместо this. Если у вас хоть какая-то обработка исключений реализована — я выиграю.
В отличие от вас — я не считаю ни себе, ни своих сотрудников гениальными программистами. Поэтому знаю, что ошибки в наших программах есть. А надежность — она от механизмов противодействия ошибкам, а не от веры в собственную непогрешимость.marsianin
28.08.2016 22:20+1Ещё раз: если вы попали в метод экземпляра класса, то this != nullptr. Так должно быть по стандарту. И компилятор имеет право генерировать код исходя из этого условия. Если программист допустил преобразование nullptr к указателю на объект, а потом вызвал метод у этого объекта, то этот программист сам себе злобный Буратина.
А отлавливать такие вещи можно если gcc подать опцию -fsanitize=undefined. Это заставляет компилятор вставлять рантайм-проверки во все места, где может возможно undefined behavior. Естественно, это имеет смысл применять только в дебажных сборках.ZyXI
29.08.2016 01:51-2Я не понимаю, зачем вы об этом спорите. Jef239 говорит о том, что
- Программисты делают ошибки.
- Специфика его сферы деятельности предполагает написание кода, максимально защищённого от сбоев, в том числе сбоев вследствие ошибок программистов.
- В результате ошибок программиста nullptr может попасть в this и это «штатная» нештатная ситуация, от которой нужно защищаться, потому что «ой, программист ССЗБ, что написал код, который при исключении в new использует nullptr как указатель на обьект» — это слабое утешение для ситуации «в результате программного сбоя потеряно N тысяч долларов [… M раз подряд]».
- Дебажная сборка не имеет смысла, т.к., во?первых, она отличается от релизной, и, во?вторых, в тестах все ситуации не предусмотришь, а необработанное UB вроде nullptr в this и крах всего приложения в релизе — большие убытки.
Короче, ССЗБ или нет, если от внезапного nullptr в this можно защититься, и на практике такое хотя бы изредка случается, то от него нужно защититься. А компилятор со своими изменениями в обработке UB вставляет палки в колёса.
Jef239
29.08.2016 10:51-2> Так должно быть по стандарту.
Вы готовы поставить миллион долларов своих денег (или денег своей фирмы) на то, что компилятор всегда действует по стандарту и не имеет ошибок? Что все библиотеки не имеют ошибок? Что все ваши сотрудники пишут без ошибок?
я живу в реальном мире, где в библиотеках и компиляторах есть ошибки. И эти ошибки — проще обойти, чем добиваться их исправления.
> этот программист сам себе злобный Буратина.
да не себе, а ДРУГИМ!!! В том же gcc бывали вылеты из-за обращения к нулевому указателю.
> Это заставляет компилятор вставлять рантайм-проверки во все места, где может возможно undefined behavior.
Вы же понимаете, что лукавите? Не во ВСЕ, а лишь в ту небольшую часть, которая есть в исходном коде. Какой там размер libc? Миллионы строк? Ну вот и поймите, что в исходном коде у вас лишь несколько процентов. А все остальное — в объектниках.
Передача NULL как первичная ошибка программиста — это огромная редкость. Как вторичная ошибка, то есть при отработке исключения — уже чаще. Как третичная (исключение у вас + баг в библиотеке) — достаточно типична.
Типичный сценарий. Есть экранная форма — это класс из библиотеки. Она использует объекты, как библиотечные, так и написанные вами. В какой-то момент происходит ошибка, ну скажем new выдает исключение. Отрабатывает деструктор формы. И вызывает ваш класс с nil вместо this.
Вы можете долго орать, что разработчики библиотеки — сами для вас буратино. Можете кричать, что это UB. Можете ждать годами, когда они починят. Если проект опенсорсный — можете и сами исправить. И долго пропихивать исправление. Или править код в каждой новой версии. А можете — обойти ошибку. Ваш выбор?
Так вот, именно обходу ошибки и мешает новое поведение gcc.
Так вот, gcc
Jef239
29.08.2016 11:21-2Когда вы на машине влетите в аварию — вы можете гордиться, что все сделали по ПДД. Вот только погибших этим не вернешь. :-( А можете — нарушить ПДД и обойтись без аварии.
Декабрь 1983года, Питер, проспект Смирнова. Ребенок перебегает дорогу прямо перед колесами. Водитель резко сворачивает на тротуар, умудряется не задеть никого на автобусной остановке и останавливается за 10 сантиметров от стены дома.
Грубое нарушение ПДД? Конечно.Вот только выжили ВСЕ. Ни синяков, ни травм, лишь валидола всем захотелось Даже ГАИ не вызывали.
я правильно понимаю, что в этой ситуации вы предпочли бы сбить ребенка и потом долго доказывать всем, что действовали прямо по ПДД? Вот только от ответственности соблюдении ПДД не спасает. Автомобиль — средство повышенной опасности, его водитель практтически всегда отвечает за ущерб.
marsianin
26.08.2016 20:18В вашем примере необязательно проверять указатели на равенство nullptr в обработке исключений — оператор delete должен работать корректно, если ему передадут nullptr, то есть ничего не делать.
olegator99
26.08.2016 21:15В 99.99% — да, согласен с вами.
Но!
- В стандарте C++03 про удаление nullptr написано невнятно, однозначность про удаление nullptr появилась только в стандарте C++11
- Если оператор 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 остается на плечах аллокатора, Который, в каком нибуть странном окружении может оказаться взрывоопасным.
marsianin
26.08.2016 21:33+1GCC и не должен вставлять проверку — это задача того, кто написал кастомный operator delete.
nikabc555
25.08.2016 08:31-1if (this == 0)
В нормальном коде никогда этого не встретить
Надо вообще компилятору выдавать ошибку «пересмотрите свой код немедленно» при обнаружении такогоmayorovp
25.08.2016 08:40+3В те времена, когда оператор new возвращал 0 при нехватке памяти — такие проверки были нормальным защитным программированием.
nikabc555
25.08.2016 08:43+1а что мешало проверить указатель перед его разыменовыванием, а не после?
т.е.
MyType* p = new MyType; if(p == 0) onMemoryLack(); else p->someMethod();
mayorovp
25.08.2016 10:04+6Если бы программисты никогда ничего не забывали — программы можно было бы вовсе не отлаживать :)
nikabc555
25.08.2016 08:48такие проверки были нормальным защитным программированием
А в те времена стандарт это не считал UB?mayorovp
25.08.2016 10:01+1В те времена считалось, что операторы в программе всегда выполняются последовательно, и одного тестового запуска под отладчиком достаточно, чтобы Undefined behavior превратился в Implementation-defined behavior.
Antervis
25.08.2016 10:03+1так смысл любого предупреждения компилятора в том, чтобы программист немедленно пересмотрел свой код. Имо код должен в режиме -Wall -Wpedantic собираться без предупреждений
murzilka
25.08.2016 11:08+1А люди должны платить налоги и соблюдать пдд.
Интересно, есть ли хоть один более-менее большой проект (>10.000loc, к примеру), который собирается без предупреждений.Antervis
25.08.2016 11:26Тем не менее к этому надо стремиться. В особо грустных случаях (например, предупреждения во внешних модулях) ворнинги отключаются для отдельных модулей/подпроектов/файлов.
tyomitch
25.08.2016 13:45+3Типичный сценарий — «Написали кучу кода, он компилировался без ворнингов. Перешли на новую версию компилятора, высыпалось более 9000 ворнингов. Править кучу кода некогда, просто отключили конкретные ворнинги для конкретных файлов.»
Если у проекта долгая история, то этот сценарий повторяется многократно, и в итоге из каждого файла ворнинги сыплются как из рога изобилия, так что -Wall -Wpedantic на уровне проекта становится бессмысленным.Antervis
29.08.2016 05:44да, сценарий популярный и не особо приятный. Однако если подумать, откуда могут взяться «новые» ворнинги:
а. компонент стал deprecated — наверное, его изначально не стоило использовать
б. было использовано доколе «безопасное в силу реализации» ub, а теперь компилятор может сломать код оптимизациями (как в описанном в статье случае). Придется как минимум тестировать, высоковероятно — править код.
в. новый компилятор более умный и подсвечивает подозрительные места. Наверное, стоит глянуть
г. была использована нелегальная с точки зрения языка конструкция, которую старый компилятор почему-то пропускал (например, gcc 4.8 пропускает неявный каст значения одного enum class в другой). Еще более поздний компилятор может на этом месте нарисовать ошибку.
Выходит, что игнорировать ворнинги при переходе на новый компилятор — не такая уж и замечательная идеяZyXI
29.08.2016 09:46в) иногда звучит как «компилятору добавили забагованную диагностику». Например, «компилятор начал видеть -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
?Antervis
29.08.2016 12:24Емнип, по умолчанию для енумов выбирается signed тип минимального необходимого размера чтобы вместить все значения. Вы можете указать underlying type enum'a явно, и тогда касты не потребуют конверсии.
Булевы выражения в с++ не являются интами, но определен неявный promotion bool -> int. Так или иначе, если вы не согласны с GCC-шным -Wconversion, (или другой «забагованной» диагностикой) то, как я уже сказал, можете отключить её отдельно, например: -Wno-conversionDistortNeo
29.08.2016 12:30Я вообще выступаю за то, чтобы неявная конвертация bool <-> int была невозможна, но это поломает кучу старого кода.
ZyXI
30.08.2016 00:32В коде значения из 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.
Antervis
30.08.2016 06:09у меня такое возникало из-за того что пришлось дублировать енум класс — значения нового не инициализировались значениями предыдущего без явного приведения к инту (в gcc 6.1, 4.8/5.3 пропускали). И по стандарту такое поведение является корректным. В конце концов, enum class для того и придумали, чтобы нельзя было вместо его значений подставлять инты.
В C++ булевы выражения, может, и не int, но C99 явно говорит, что операторы сравнения дают int.
ну так там булей нет
dreamer-dead
26.08.2016 11:53Они, конечно же, есть.
Например, Chromium: там warning == error в коде самого Хромиума, для third_party могут быть исключения.
А кода там не 10К, а гораздо больше.
monah_tuk
26.08.2016 13:41Предупреждения сделанные при помощи
#warning
как замена всяким TODO считаются? :)
Не, конечно я согласен, что TODO всякое должно быть заменено на тикет в системе учёта. Но бывают вещи вроде "сделать более элегантно", а по тикету тебя ещё менеджмент постоянно дёргать будет или закроют нафиг.
alexeyknyshev
25.08.2016 10:37+2https://gcc.gnu.org/gcc-6/changes.html
The default mode for C++ is now -std=gnu++14 instead of -std=gnu++98.
Т.е. по новому стандарту это UB, а как следствие, компилятор может выпиливать подобные проверки.
А добавление скобок, для подавления предупреждений — стандартная практика.
Andrey2008
25.08.2016 16:16В защиту программистов скажу, что на самом деле такой код встречается редко. Т.е. такие ошибки однозначно есть, но их не так уж много, относительно ошибок других типов.
P.S.Покупайте наших слонов.Используйте анализатор PVS-Studio для защиты от этой и многих других проблем. :)
Jogger
25.08.2016 16:43Ещё бы кто-нибудь объяснил мне, зачем такой код было писать… Ну, кто-то же это придумал, у него же были какие-то мотивы, кроме мизантропии?
Andrey2008
25.08.2016 17:01+2Историческая справка. В достандартную эпоху это вообще было нормально. :) Более того, не только сравнивали this с 0, но и меняли его. См. главу «Интересные наблюдения» в статье "К тридцатилетию первого C++ компилятора: ищем ошибки в Cfront".
marsianin
25.08.2016 16:43-3Не следует писать код, приводящий к undefined behavior. Собственно, в чём смысл этой статьи? Показать какой GCC плохой — считает что в пользовательском коде никогда не встретится UB? Ну так он по стандарту должен это делать.
Кстати, какая альтернативно одарённая личность могла догадаться использовать в продакшене GCC с минорным номером версии меньше двойки?
olegator99
25.08.2016 16:44Увольнять надо программистов, которые "беззаботно пишут" такой новый код на 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 (); }
Jef239
26.08.2016 15:54Достаточно уволить любителей множественного наследования не по делу.
Удивительно кривая конструкция по сравнению с аналогом в JAVA. В java мы можем придумать интерфейс. Ну скажем для списка — количество элементов, взять элемент по индексу. добавить-удалить элемент. А потом, на произвольном уровне иерархии добавить нужные методы в класс и сказать, что класс реализует этот интерфейс. И всё, никакого множественного наследования уже не нужно. Аналогично, но менее элегантно можно и в дельфи.DistortNeo
26.08.2016 22:57+1Сразу видно человека, который не знаком с тем, как интерфейсы реализованы на низком уровне.
Интерфейс — это абстрактный класс без полей + немного кодогенерации.Jef239
28.08.2016 00:24На низком уровне интерфейс — это «вторая» 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 у каждого интерфейса свой.DistortNeo
28.08.2016 00:41+2При наследовании мы дополняем vTable новыми методами, а при расширении — создаем новую vTable.
Так оно и есть. Но основная причина создания второй vTable — это наличие полей, а не различия в логике дополнения/расширения.
Если в классе есть поля, то мы вынуждены создавать копию vtable в каждом объекте. Если же полей нет, то нет необходимости её дублировать — достаточно разместить её как часть основного vtable. Именно по такому принципу и работают некоторые компиляторы.Jef239
28.08.2016 09:10vTable не создается в экземплярах объектов. vTable находится в классе. Если у нас есть 10 объектов класса TABC, то каждый объект имеет все поля, но ссылку на единую vTable.
Если речь о статических полях (поля класса) — то в интерфейсах есть только константы. А с обычные статические поля должны быть в общими и для базового класса и для его наследников. То есть обычно реализуются как глобальные переменные со сложным именем.DistortNeo
28.08.2016 12:08vTable не создается в экземплярах объектов.
При множественном наследовании (C++) и наличии полей таки приходится создавать vtable в экземплярах классов, иначе буде невозможен доступ к полям.
Статические поля, понятное дело, вообще никак не участвуют в полиморфизме и ни на ято не влияют.Jef239
28.08.2016 12:35+1> При множественном наследовании (C++) и наличии полей таки приходится создавать vtable в экземплярах классов
ЗАЧЕМ? Зачем хранить дублирующуюся константную информацию, когда можно хранить ссылку? Понятно, что при множественном наследовании одной ссылкой не обойтись из-за необходимости обеспечить приведение ко всем базовым классам. Ссылок будет по числу классов, это понятно.
Если есть какие-то ещё тонкости — прошу пояснений. В моих компиляторах ООП не было. :-)DistortNeo
28.08.2016 12:49Зачем хранить дублирующуюся константную информацию, когда можно хранить ссылку?
Представьте себе вызов по базовому классу. Передаваемый указатель содержит vtable и поля класса, причём одним куском памяти. Вот из-за требования одного куска памяти и приходится дублировать vtable.
И vtable при множественном наследовании, кстати, будут разные в зависимости от смещения класса.Jef239
28.08.2016 13:03> Передаваемый указатель содержит vtable
ЧТО? Указатель содержит адрес. Если это указатель на метод конкретного экземпляра — то два адреса (this и адрес метода).
Можете перевести ваше высказывание на понятный язык?DistortNeo
28.08.2016 13:57Указатель содержит адрес объекта, передаваемого функции.
Вот структура типичного объекта в памяти:
[vtable][fields]
Адрес этой структуры и передаётся в функции.
Вот структура объекта с множественным наследованием:
[vtable1][fields1][vtable2][fields2]
Объединить два vtable в один:
[vtable12][fields1][fields2]
невозможно, т.к. непонятно, как взять адрес от второго объекта.
С другой стороны, если у объекта нет полей, то указатель будет указывать на объект, содержащий только vtable. Соответственно, нет нужды размещать vtable в каждом объекте, когда можно сделать его глобальным.Jef239
28.08.2016 14:30> Вот структура типичного объекта в памяти:
ОШИБАЕТЕСЬ. Вместо самой vTable в объекте указатель на неё. Это вы книги для чайников читали, там действительно иногда так рассказывают.
Для проверки — посмотри sizeof объекта. Потом добавьте туда новый виртуальный метод. И посмотрите, изменился ли размер самого объекта.
> Объединить два vtable в один:[vtable12][fields1][fields2] невозможно, т.к. непонятно, как взять адрес от второго объекта.
C этим почти согласен. Там две ссылки. Просто иногда объединенная таблица идет одним куском памяти, а вторая ссылки ведет на её середину. Можно это трактовать как одну таблицу. можно — как две подряд.DistortNeo
28.08.2016 17:20+1ОШИБАЕТЕСЬ. Вместо самой vTable в объекте указатель на неё. Это вы книги для чайников читали, там действительно иногда так рассказывают.
Видимо, надо действительно быть чайником, чтобы нормально объяснять :)
Для меня настолько очевидно, что [vtable] — это указатель на таблицу, а не сама таблица, что по-другому быть и не может.
C этим почти согласен. Там две ссылки.
Именно это и я хотел объяснить. Есть поля во втором объекте — придётся вставлять вторую ссылку на таблицу, тратя память на каждый объект. Нет полей — память тратить не нужно.Jef239
28.08.2016 19:23> Для меня настолько очевидно, что [vtable] — это указатель на таблицу, а не сама таблица,
> Соответственно, нет нужды размещать vtable в каждом объекте, когда можно сделать его глобальным.
Сколько вас под одним ником пишет? :-) vTable — всегда глобальный, общий для всего класса. Одна там секция или несколько — не важно.
> Нет полей — память тратить не нужно.
Опять ошибка. Передаем указатель на производный объект в процедуру, которой нужен второй базовой класс. И эта процедура — хочет вызвать виртуальный метод. Под каким номером она будет его брать из vTable? под тем, что в базовом классе. А для первого базового класс? Да тоже самое. А для третьего? Опять то же
Так что тратить память на указатель на вторую vTable придется. Ну кроме ситуации, когда объект не виден извне модуля, а модуле — ко второму базовому классу не приводится. Но тут и vTtable не нужна и так понятно, какой метод вызывать.DistortNeo
28.08.2016 22:20Опять ошибка. Передаем указатель на производный объект в процедуру, которой нужен второй базовой класс. И эта процедура — хочет вызвать виртуальный метод. Под каким номером она будет его брать из vTable?
Она будет его брать из vtable для второго объекта. Просто указатель на vtable второго объекта будет храниться не как поле объекта, а как поле основного vtable, который у нас в единственном экземпляре.Jef239
29.08.2016 10:28-1this — это указатель на объект. **this — это vTable (обычно делают так для ускорения).
Вы хотите сказать, что в качестве this в базовый класс передастся указатель на " поле основного vtable, который у нас в единственном экземпляре".
Делаем 2 объекта производного класса. Преобразуем их ко второму базовому классу. Для обоих — получится одинаковое значение this — указатель на " поле основного vtable, который у нас в единственном экземпляре"
1) Как реализовать сравнение объектов на == и !=???
2) Как реализовать приведение от второго базового класса к исходному объекту?
Проверьте свою идею на компиляторе.
1) Если вы получите, что все this одинаковы — вы правы.
2) Если вы получите, что при добавлении второго базового класса sizeof не увеличился — вы правы.
По мне — так ОЧЕНЬ сомнительно. ОЧЕНЬ. Хотя из такого, что сравнение объектов классов на == должно работать.DistortNeo
29.08.2016 12:28Как реализовать сравнение объектов на == и !=???
При множественном наследовании адреса объектов и не будут совпадать — это нормально.Jef239
29.08.2016 12:54-1ЕЩЁ РАЗ. Сколько вас под одним ником пишет? Вы пишете взаимоисключающие вещи. ПОЧЕМУ?
> указатель на vtable второго объекта будет храниться не как поле объекта, а как поле основного vtable, который у нас в единственном экземпляре
> При множественном наследовании адреса объектов и не будут совпадать
ОДНОВРЕМЕННО оба высказывания не могут быть истинными. ИЛИ одно — ИЛИ другое.
ЕЩЁ РАЗ. Речь о ситуации, когда вы привели объект ко второму базовому классу и передали его в процедуру, которая знает только про второй базовый класс. ЧЕМУ будет равен адрес объекта в этой процедуре?DistortNeo
29.08.2016 13:27ОДНОВРЕМЕННО оба высказывания не могут быть истинными. ИЛИ одно — ИЛИ другое.
Могут, потому что это зависит от компилятора.
ЕЩЁ РАЗ. Речь о ситуации, когда вы привели объект ко второму базовому классу и передали его в процедуру, которая знает только про второй базовый класс. ЧЕМУ будет равен адрес объекта в этой процедуре?
Можно быть более-менее уверенным только в том, что при множественном наследовании, если у обоих классов есть поля, адреса объектов будут разные.
Java, C# и Delphi принципиально разделяют классы и интерфейсы (схема один базовый класс + много интерфейсов). В них указатели на vtable интерфейсов размещаются в основной vtable, поэтому адреса объектов и интерфейсов будут совпадать.
В C++ же классы и интерфейсы не разделяются. Единственный признак — наличие или отсутствие полей, а дальше всё ложится на усмотрение компилятора.
Visual C++ и GCC не будут реализовать класс без полей специальным образом и при любом раскладе заведут второй указатель на vtable в объекте, при этом адреса при использовании подобных «интерфейсов» не будут совпадать.
А вот C++ Builder определяет в процессе компиляции логику работы (класс/интерфейс) и генерит разное размещение для случая классов и интерфейсов. Там адреса объектов совпадут, если используются только интерфейсы.Jef239
29.08.2016 14:32-2> Могут, потому что это зависит от компилятора.
Ох уж эти сказки, ох уж эти сказочники… Вы сколько компиляторов сами написали? Компилятор — такая же программа, не сложнее других. Несколько штук у меня написано. Некоторые вещи просто быстрее выполняются, если их компилировать.
Объяснить на уровне третьеклассника?
Есть базовый класс 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-интерфейс.
DistortNeo
29.08.2016 16:22Иными словами, приведение от 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-объекты?Jef239
29.08.2016 18:52-1> И оно работает, но только с помощью 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 не сделать.DistortNeo
29.08.2016 19:22+1не надо путать 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-совместимым, но для меня этот факт не имеет никакого значения.Jef239
29.08.2016 20:22-1> Необходимое условие работы 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;
Вот такого кода достаточно, чтобы вызвать деструктор у класса. И ровно то же самое — на С++. Это и есть отличие в семантике.
DistortNeo
29.08.2016 22:18НЕТ. 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 в качестве базовых. Другое дело, что без счётчика ссылок действительно будет плохо.Jef239
30.08.2016 12:39-2> корректная работа 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, а вот в классах — обычное множественное наследование.
Тогда вы во многом были правы.
Jef239
29.08.2016 15:16-1> В них указатели на vtable интерфейсов размещаются в основной vtable, поэтому адреса объектов и интерфейсов будут совпадать.
И опять вы не совсем правы. Интерфейс приводится к классу, поэтому обнаружить отличие сложно. Но оно есть.
http://dit.isuct.ru/content/view/138/55/ — посмотри на рисунок в разделе 6.15
Это нужно для того, чтобы передать интерфейс в dll, работающую с COM-объектами. Ну или наоборот, работать с интерфейсом, который дает dll (или EXE), например с тем же word.
Поэтому в классе два указателя — один на VMT (vTable для сишников), а второй на IMT. Просто IMT обычно общий для все реализуемых интерфейсов. Не помню, можно ли там прямо прописать dispid (номер метода), но если можно — то придется делать в классе несколько IMT.DistortNeo
29.08.2016 16:33Мне кажется, что дальнейшее обсуждение этого не имеет никакого значения.
Важно просто понимать, что нет единого стандарта низкоуровневой реализации механизмов объектно-ориентированного программирования. В разных языках и/или при использовании различных компиляторов представление объектов в памяти может быть различным.
Понимание того, как оно устроено на низком уровне, несомненно, нужно, так как может позволить избежать проблем, связанных с падением производительности.
До тех пор. пока вы привязаны к конкретному компилятору и конкретной железке, вы можете писать код, учитывая особенности конкретного компилятора и архитектуры. Если же вы хотите иметь переносимый код, то придётся отказаться от всего, что выходит за рамки стандарта языка.Jef239
29.08.2016 18:13> Мне кажется, что дальнейшее обсуждение этого не имеет никакого значения.
На вашем уровне понимания механизмов компиляции — да, не имеет. С авторами компиляторов вполне можно это обсудить.
>В разных языках и/или при использовании различных компиляторов представление объектов в памяти может быть различным.
Только когда дело не касается 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 выкинули из языка вообще все. И ничего, никому это не помешало. Ну кроме школьников, конечно.DistortNeo
29.08.2016 19:08Разные бывают задачи, разные. Если нет нужды писать код, совместимый со старыми компиляторами, то и не надо этого делать. Сейчас с поддержкой стандартов дела обстоят чуть лучше.
И выработать свой набор конструкций, одинаково работающий на всем зоопарке компиляторов с разницей в выпуске в 25 лет.
У меня возникало такое ощущение, когда я изучал файлы STL или Boost. Впрочем, и в них в последних версиях совместимость со старыми компиляторами совсем поломана.
Если у вас зоопарк меньше — давайте уж я вам прочту лекцию про переносимость, а не вы мне.
Реквестирую увлекательный пост на эту тему — было бы интересно.Jef239
29.08.2016 19:30> Сейчас с поддержкой стандартов дела обстоят чуть лучше.
Именно, что ЧУТЬ. Команда разработчиков компиляторов с АЛГОЛА-68 опытным путем выяснила, что они используют 30% возможностей языка. Команда эта занималась портирование компилятора на разные военные процессоры, а компилятор был написан на том же алголе-68. То есть на этапе, когда сам компилятор компилируется и работает — 70% языковых возможностей ещё не проверено.Тесты помогают, но не сильно.
Так что в простых случаях — все работает стандартно. А вот сложные комбинации — не всегда.
Стандарты хороши, когда прошло уже лет 10 и все споры о реализации закончились. Ну скажем С99 и С++98 — достаточно стандартны. POSIX.1-2008 — тоже вполне стандартен. А вот С++11… думаю, что можно нарыть немало примеров, когда реализации отличаются достаточно сильно из-за разного прочтения стандартов.
> Реквестирую увлекательный пост на эту тему — было бы интересно.
я не считаю себя эталоном в переносе. Так что увлекательного поста не получится, а будет нудное описание прописных истин. :-( Ну в смысле прописных для тех, кто уже занимался переносом. Собственно все просто — пишем под один компилятор, потом переносим под другой, потом под третий. Под остальные — уже переносить просто.
В качестве байки. В FORTRAN-66 оператор END в конце программы определялся как «буквы E, N, D следующие в указанном порядке. Это чтобы дать возможность в последней перфокарте указать конец файла не только компилятору, но и ОС
olegator99
01.09.2016 10:05+4Стандарты — дело десятое. Не так важно, что в них. Важно — что реализовали авторы компиляторов. Была в >свое время замечательная книжка по переносу программы на FORTRAN-66 между различными ОС на PDP-11. >Все компиляторы — одной фирмы, стандарт — один и тот же, а вот прочтения этого стандарта — РАЗНЫЕ.
Стандарты это самое главное. Важно что бы все из соблюдали, и авторы компиляторов и разработчики. Иначе будет бардак.
Вот вы приводите пример из времен, когда травка была зеленее, а солнце краше. Прекрасно, но ваш пример не про стандарты в компиляторах, а про разницу в окружениях.
PDP-11 это целое семейство. Начиная от простейших LSI-11 с 32RAM, без MMU, кончая навороченными PDP-11 с огромной по тем временам памятью 1+MB, и сверх прогрессивным MMU. Общего у них только система команд, а в остальном между ними пропасть — примерно такая же как сейчас между ARM Cortex-M0 и ARM Cortex-A53.
- Операционные системы для PDP-11 отличались друг от друга, так же, как сейчас отличаются ОС для Cortex-M0 и Cortex-A53:
На простейших LSI-11 работал только RT11SJ, в котором нет многозадачности, а по сути только дисковые/терминальные драйвера и поддержка простейшей файловой системы. И только работа с физической адресацией 32КБ памяти.
На PDP-11 — RSX-11, RT-11XM да и тот же Unix: Это ОС другого класса: с вытесняющей многозадачностью и виртуальным адресным пространством.
- Могу предположить, что в книжке про которую вы говорите речь шла вовсе не про реализации самого языка, а именно про особенности программирования и переноса кода под два принципиально разных окружения.
Сейчас тоже нельзя просто так взять программу на C++ для Linux, работающую на Cortex-A53, собрать под Bare Metal и запихнуть на STM32F101. Про подходы к портированию кода на C++ на разные ARM сейчас тоже можно многотомнички писать. Но при этом стандарт языка и реализации будут абсолютно стандартными.
Ну в смысле прописных для тех, кто уже занимался переносом. Собственно все просто — >пишем под один >компилятор, потом переносим под другой, потом под третий. Под остальные — уже переносить >просто.
Прописная истина, кажется, в другом. Надо не "писать под один компилятор, а потом переносить под другой", а писать по стандартам языка и не закладываться на заведомо частно платформенные решения типа win32 API, в случае если можно использовать общепринятые стандарты типа POSIX
А начинать надо даже не с написания кода, а с проектирования архитектуры, выбора компонентов и фреймворков, которые будут использоваться.
В случае с GUI приложением, если бы подумали о портировании до начала разработки, то возможно вместо VCL использовали другое решение, более адекватно отвечающее требованиям переносимости и надежности.
Вот возьмем к примеру вашу историю про "не потерять пользовательские данные при крахе GUI приложения".
Современный подход к решению этой проблемы:
разделить приложение на две части: легковесный надежный бэкенд, который отвечает за сохранность данных, и работающий с ним по сети фронт (обычно это Web приложение на JS, но с таким же успехом фронт можно написать и на VCL/Qt/something else).
Это современный архитектурный подход к написанию надежных и портабельных приложений. GUI полностью изолировано от пользовательских данных.
Сохранность данных гарантирует аппаратные механизмы защиты памяти ОС. Это качественно иной уровень надежности, чем тысячи проверок в коде, и попытка спасать данные из корраптед кучи.
mayorovp
28.08.2016 06:23Если у нас было 3 метода и мы расширили интерфейс ещё одним методом, то получим 3+4=7 строк в vTable. С учетом скрытых трех методов — это будет (3+3)+(4+3)= 13 строк. А при наследовании 3+1 — 4 строки.
Это особенность конкретной реализации.
Jef239
28.08.2016 09:24Это особенность 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. К сожалению, сохраненная копия — доступ к оригиналу в РФ запрешен.mayorovp
28.08.2016 10:44«Increments the reference count for an interface on an object. » — то есть счетчик ссылок имеется у интерфейса, а не у объекта. То есть в COM должна быть обеспечена возможность иметь несколько счетчиков ссылок у одного объекта.
Это написано про интерфейс, а не про реализацию. Да, пользователь интерфейса должен быть написан так, будто у каждого интерфейса свой счетчик — вызов у одного интерфейса AddRef, а у другого Release даст UB, даже если они указывают на один и тот же объект. Это сделано для того, чтобы можно было "наследоваться" от компонентов через агрегацию.
Но никто не приказывает тем, кто реализует интерфейсы, действительно использовать отдельный счетчик для каждого.
Jef239
28.08.2016 11:25не приказывает. Но… интерфейс может требовать отдельного ресурса, не блокируемого всем объектом.
Например класс реализует ком-порт. Один интерфейс — для задания скорости, другой — для передачи данных. Открывается порт, когда мы открываем интерфейс передачи данных. А когда делаем Relaease — порт закрывается, зато можно получить интерфейс для изменения скорости.
То есть, если жестко использовать одну копию IUknown — мы лишаемся некоторых возможностей. Лишаемся — ради экономии десятка слов. А далее появляется какой-нибудь из стандартов (ну скажем один из https://ru.wikipedia.org/wiki/OPC ) завязанный, на раздельный подсчет ссылок. И всё, становится очень неудобно его реализовывать.
А предусмотреть на этапе разработки структуры vTable, что там будет в будущих стандартах — это сложно. Проще не выпендриваться и сделать как у всех.
> Но никто не приказывает тем, кто реализует интерфейсы, действительно использовать отдельный счетчик для каждого.
Вы действительно прочти все описания для всех имеющихся в данный момент COM-интерфейсов? :-) я только пару сотен читал.mayorovp
28.08.2016 18:37Например класс реализует ком-порт. Один интерфейс — для задания скорости, другой — для передачи данных. Открывается порт, когда мы открываем интерфейс передачи данных. А когда делаем Relaease — порт закрывается, зато можно получить интерфейс для изменения скорости.
Весь мой опыт проектирования кричит мне, что передачу данных тут надо делать в отдельном объекте, а не в том же самом.
Jef239
28.08.2016 18:49-1Два объекта для одного порта? А почему 2, а не 5 и не 10?
Очень интересно логику услышать. Почему во всех известных мне ОС COM-порт — это один объект (handle) со многими интерфейсами, а вы предлагаете несколько объектов.
Из-за какой причины вы решили избавится от инкапсуляции? Вашим двум (или больше) объектам придется показывать наружу очень многое. Что вы получите взамен?
kibb
25.08.2016 16:44xorl %edi, %edi
гарантированно обнуляет %rdi, потому что в IA32 поведение 32битных инструкций в long mode — zero extend.
BratSinot
А можно вопрос? В чем смысл статьи?
Я может и простой студент, а не программист в крупной компании, но подобный «работающий» код никогда не должен писаться. Т.е. сначала используют undefined behaviour, пытаясь сделать его «defined», а потом удивляются и жалуются что компилятор совместимость со старыми программами ломает! Радуются Go, Rust'ам и другим новым языкам, когда в старых языках можно писать аккуратнее, без всяких хаков (например не убираем «ненужные» круглые и фигурные скобки).
Наоборот, новый компилятор помогает находить неправильные участки программы. Не у всех-же есть крутые инструменты как PVS-Studio.
Error1024
Подобный код не редкость в больших проектах с долгой историей.
Помниться сравнение this с NULL было в недрах MFC.
Ddnn
Интересный факт: сравнение this с нулем есть даже у Страуструпа в его «Programming — Principles and Practice Using C++», причем в относительно свежем (2014 г) издании.
DmitryMe
На Google Books есть электронный вариант с частичным предпросмотром, поиск по «this» позволяет найти пример кода с реализацией функции Link::insert() на 619-й странице, где выполняется сравнение this с nullptr.
tyomitch
Смысл статьи — «если в вашем проекте эн лет висело ружьё, то теперь-то оно выстрелит вам в ногу»
Ogra
Эту статью через полгода кто-нибудь нагуглит, в попытках понять, почему же старый код, который прекрасно комплировался gcc5, не компилируется на gcc6.
DmitryMe
На gcc 6.1 код по-прежнему компилируется, просто может работать по-другому. Значительно сложнее заметить.
monah_tuk
Если оптимизатор видит такое, то должен смочь вывести и предупреждение. Другой вопрос — что делать, если в проекте и так 100500 предупреждений при сборке.
Jef239
Интересный вопрос, что кому код должен? :-)
Один из вариантов — во время работы конструктора произошло исключение (например, нехватка памяти). Деструктор выполняется при не полностью созданном объекте. При этом часть вложенных объектов — вообще не инициализированы. Как пример — большое окно с сотней таких CWindow. И вот тут подобная проверка полезна, она добавляет надежности, больше шансов, что деструктор выполнится до конца. Подобноые решения есть во многих оконных библиотеках.
Так что зря они совместимость поломали. Оконные библиотеки при нехватке памяти начнут рушиться как карточные домики.
DmitryMe
В таком случае нельзя полагаться, что в них будут именно нулевые значения.
Jef239
> В случае исключения деструкторы выполняются только для тех объектов (и подобъектов), которые были полностью сконструированы к моменту возникновения исключения.
я отстал от жизни? Конструктор объекта выделил память одним new, вторым, третьим и на четвертом словил исключение. И что, не запустится деструктор и выделенная память утечет? Я чуть нечетко написал. Речь о ситуации, когда в классе мы имеем ссылки на объекты, которые создает конструктор этого класса. То есть класс — это экранная форма, и в нем указатели на кнопки и поля ввода, кторые создает конструктор класса.
> В таком случае нельзя полагаться, что в них будут именно нулевые значения.
Можно. Если даже компилятор не инициализирует память объекта нулями, то нам ничего не мешает присвоить полям-указателям NULL, а потом — вызвать new.
DmitryMe
Тогда они перестанут быть «вообще не инициализированными».
Jef239
> Если исключение покинуло конструктор, деструктор объекта вызван не будет.
Значит что-то сильно поменялось в С++ за 25 лет. В Borland C++ 3.1 он вызывался, насколько помню.
> Тогда они перестанут быть «вообще не инициализированными».
Это тонкости. У Borland память объекта обнуляется до конструктора, поэтому неинициализированный член класса — это NULL.
DistortNeo
Когда я попытался в нём ради интереса написать, я просто не смог его скомпилировать. Это не C++.
Неинициалилизированный — значит, не имеет никакого значения. Если член класса имеет значение NULL, значит, он уже инициализирован. Нулём.
DistortNeo
Видимо, отстали. Современное программирование на C++ предполагает использование умных указателей. Если в коде нет ни одного delete — это хороший код, если ни одного new (кроме случаев, где он оправдан, например, в низкоуровневых библиотеках) — это прекрасный код.
Если конструктор бросает исключение, то будет вызван не пользовательский деструктор, а деструктор по умолчанию. Он просто вызовет деструкторы для всех полей и освободит память под текущий объект. При этом, если исключение было выброшено не в теле функции конструктора, а инициализатором поля, т.е. класс не полностью инициализирован, то деструкторы будут вызваны только для уже инициализированных полей.
Если вы вручную выделяете память, то обязаны вручную же её и удалять.
У простых полей деструктор по умолчанию пустой. Если вы выделяете память в инициализаторах полей, то будет утечка.
Если же вы хотите сначала полностью проинициализировать поля класса нулями, а выделять память в конструкторе вручную, то придётся позаботиться и о ручном освобождении памяти, поставив catch и запихнув в него код очистки.
Jef239
> При этом, если исключение было выброшено не в теле функции конструктора, а инициализатором поля,
Нет, именно в теле функции. С инициализаторами полей автоматика достаточно хорошо разбирается
> кроме случаев, где он оправдан, например, в низкоуровневых библиотеках
Ну оконные библиотеки — достаточно низкий уровень.
Ну в общем в очередной раз убедился. что лучше на C++ не писать без особой нужды.
> Современное программирование на C++
Современное — синоним низкой переносимости под разные ОС и разные железки. Лично нам лучше держаться стандартов 25летней давности. Ну или на 10 лет вперед продумывать, на какие железки придется портировать, а на какие нет.
DmitryMe
DistortNeo
Низкий уровень — это контейнеры. А библиотеки — это прикладной код высокого уровня.
Так оно и есть.
Возможно, нужно было оставить труп в покое и развивать новый язык, а не городить костыли в языке. Но результат был бы тот же: старый стандарт бы просто умер из-за отсутствия поддержки.
Jef239
Ну С99 не умер и вполне поддерживается. как и С++98. Но мы держимся ещё ниже, потому что вдруг придется реанимировать версию на MS-DOS? У нас не то, чтобы совсем embeded, но машинки слабенькие. А главное — выбор процессора за заказчиком.
Сейчас вот — страшный зверь — lcc++ МСВС на МЦСT. и не поменять — концепция МСВС не разрешает установку своего софта.
olegator99
Имхо, под железки лучше писать на plain c. Да больше возни, но зато точно знаешь что ожидать от кода.
На "современном C++" можно запросто писать под железо, и при этом код будет очень хорошо переносим. Но конечно, да — скртых способов выстрелить себе в ногу на плюсах больше.
Какая у вас железка, если не секрет, что вы так переживаете за совместимость?
Передовой край для железок сейчас что то типа Rust, но его далеко не везде применишь — llvm backend есть не под все архитектуры, да и с поддержкой либами железа у него пока грустно
Jef239
Ну почти на Си и пишем. Комментарии, например, везде С++ные.
У нас планида такая — своими технологиями затыкать дыры в чужих проектах. Конкурсы выигрывают одни, подрядчиками записаны другие, а фактически — делаем мы. А потом читаем победные статьи и улыбаемся. Высокоточный GNSS (GPS/ГЛОНАСС) у нас. Измерение расстояний между антеннами с СКО 5 миллиметров.
Основные оптимизации — идут от алгоритма, а не от качества компиляторов. И основные ошибки — от программистов, а не от недочетов языка. Смена языка и компилятора — может дать процентов 5-10, максимум 20.Смена алгоритма или программиста — выигрывает разы. Так что менять язык на что-то современное — есть смысл лишь для больших проектов.
monah_tuk
или для новых. Очень понравилось использовать C++11 для Baremetal под ARM926E-JS (Cypress FX3). Ограничение по памяти: 300кБ под рантайм (стандартный выкинут, заменён своей лайт-версией), RTOS (ThreadX) и логику. В 300 кБ сейчас влазим с дебаг сборкой, правда не со всеми строками (логи приходится отключать).