Pull to refresh

Comments 88

Зачётно, далеко не всё смог разгадать.

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

По стандарту так. https://en.wikipedia.org/wiki/Most_vexing_parse

Люди высокого уровня подскажите с чего начать изучать язык c++? Да знаю что он очень сложный, но для изучения выбрал его. Подскажите базу для начала?

Пишите код, читайте мертвого страуса, всё получится! Есть мнение, что можно неплохо прокачаться на комитах в опенсорс, например мой любимый foss проект 0A.D. https://play0ad.com/

Сравнивать плюсовые структуры и классы через memcmp плохая затея, ведь может прилететь в c2 отнаследованный класс, с другой vtbl.

Не факт что это ошибка, обычно считается что несовпадение типов ведёт к неравенству объектов.


Тут скорее проблема снова в выравнивании и прочем.

Знаете зачем программисту два монитора?

Незачем. Окно IDE на два монитора ре развернешь и больше кода не увидишь...

Хочу 32 дюйма 4к монитор - вот там ух! Но не дают. Говорят "Мы тебе дадим, а завтра у нас на пороге IT отдела будет целая очередь стоять из желающих требующих себе такие же!".

Открепите документ в отдельное окно, все IDE это позволяют.

А мне не понравилось, попробовал эту бандуру 34' Dell в повседневной работе. Начал быстро уставать, вечером побаливала голова и шея. Вернулся к двум монитора по 24дюйма после двух недель мучений

32" Dell (еще и кривой), 7 месяцев, полет нормальный. Вот эти 2 дюйма и играют разницу! :-)

А если серьезно, то боли у вас, возможно, от недостатка физической активности. Годовой абонемент в спортзал стоит дешевле топовых кресел, столов и затрат на лечение хроники опорно-двигательного аппарата после 35 и неврологии после 40

На одном мониторе одна IDE, на другом - вторая (разные).

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

Для запуска того, что отлаживаешь. Особенно если пишешь фронт.

Я пока не научился запускать софт написанный под микроконтроллер на мониторе 😁

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

На одном IDE, на втором - отладка, на третьем - доки и всё прочее. И да - я хардварщик при этом)

Ух, это 49'' как у меня)) 6 вкладок IDE на одном экране)

Получается, что почти все ошибки из-за программирования на си++ в стиле си.

Еще касательно вот этого кода:

  T& operator=(const T &rhs) {
    n = rhs.n;
    delete s1;
    s1 = new S(rhs.s1);
    return *this;
  }

Если исключения в проекте не отключены, то потенциально new может бросить std::bad_alloc. В этом случае в this->s1 остается старое и, возможно, ненулевое значение. Которое затем будет передано в delete в деструкторе. И получим double free.

Специфика области такая, что про исключения я забыл лет 10 назад. 4% перфа слишком дорогая цена, когда мы тут за 15мс на фрейм бьемся насмерть ;)

В данном случае, функция setDeltapPitch применяется для

А это бонусная очепятка?

UFO just landed and posted this here

Шикарная статья, спасибо! Все-таки С++, это С++.

С выкидыванием memset оптимизатором только не очень понятно. По идее же это вызов функции, которой передается кусок данных... Мало ли что она там делает? Получается, что оптимизатор знает что такое memset?

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

там оптимизации идут уже на уровне IR компилятора, с выкидыванием мертвых веток кода, почитать можно например тут (https://en.wikipedia.org/wiki/Dead-code_elimination). С мемсетом была похожая ошибка в md5, с точки зрения компилятора, это не влияет на производительность программы. Факт того, что личные данные останутся в памяти, не повлияет на работу программы.

void MD5::finalize () {
  ...
  uint1 buffer[64];
  ...
  // Zeroize sensitive information
  memset (buffer, 0, sizeof(*buffer));
}

но есть и обратная сторона, например на плойке все memcpy помечены как секьюрные и плоечный компилятор об этом знает, он их никогда не выкидывает и не оптимизирует, кроме того в самом memcpy сделаны проверки на пересечение с адресным пространством защищенных областей. В итоге сдкшный memcpy почти в два раза проигрывает наивной реализации копированию _m128 через регистры. Изза этой особенности все (с которыми я работал) игровые движки тащут свою имплементацию memcpy для ps5

кроме того в самом memcpy сделаны проверки на пересечение с адресным пространством защищенных областей

но это ведь реализация memmove(), которая, к слову, и должна быть медленной но безопасной, когда memcpy() должна быть быстрой но "на совести разработчика"

должна быть медленной но безопасной, когда memcpy() должна быть быстрой но "на совести разработчика"

Это в каком разделе стандарта такое написано?

ISO/IEC 9899:TC3 Committee Draft — Septermber 7, 2007 WG14/N1256
7.21.2.2 The memmove function
Synopsis
1 #include <string.h>
void *memmove(void *s1, const void *s2, size_t n);
Description
2 The memmove function copies n characters from the object pointed to by s2 into the object pointed to by s1. Copying takes place as if the n characters from the object pointed to by s2 are first copied into a temporary array of n characters that does not overlap the objects pointed to by s1 and s2, and then the n characters from the temporary array are copied into the object pointed to by s1.

В последних редакциях, возможно, нумерация сместилась.

Можно просто в man посмотреть, написано тое самое

NAME
memmove - copy memory area

DESCRIPTION
The memmove() function copies n bytes from memory area src to memory area dest. The memory areas may overlap: copying takes place as though the bytes in src are first copied into a temporary array that does not overlap src or dest, and the bytes are then copied from the temporary array to dest.

Понятно, что в реальной жизни эту операцию тоже постараются оптимизировать и не использовать промежуточный буфер, по возможности, но мы же не об этом.

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

Но dalerank другое имел в виду, говоря про защищённые области.

Тут ни про скорость, ни про безопасность ничего не говорится

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

Но dalerank другое имел в виду, говоря про защищённые области.

Почитал, интересно, спасибо, но не зная этих внутренностей, читается оригинальный комментарий совершенно иначе :)

да. Чтобы такого не было, можно например пометить буфер как volatile (тогда компилятор не станет оптимизировать никакие обращения к нему. Этот режим был придуман специально для буферов памяти всяких устройств, когда запись данных в адрес означает их появление на экране, на диске и т.п.), либо воспользоваться специальной версией memset. https://en.cppreference.com/w/c/string/byte/memset

const char* whichString = x ? "true" : "false";
const size_t len = strlen(whichString);

Напоминает старинный индусский метод определения true/false:
if( strlen(whichString) == 4 ) ... true

"Знаете зачем программисту два монитора?" - а ведь хорошо сказано :)

в русской локали и русской кодировке - "ложь" и "правда" /// "true" и "false"

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

Ну это не так. Тот код эквивалентен

{ auto _ = std::unique_lock(m);}

Мьютекс блокируется и сразу разблокируется и это не то же самое, что он вообще не блокируется.

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

Это тоже не верно, формальная ошибка заключается в том, что нельзя разыменовывать нулевой указатель. Мало того, современный приличный компилятор вот такой код (вместе с телом ифа) просто удалит ни на секунду не задумываясь как заведомое УБ.

 if (this == null) {...}

  1. Нет, не удалит если внутри логика, этот код ничем не отличается от любого другого и корректен

  2. Попробуйте скомпилить и запустить пример. (https://onlinegdb.com/YetgglcV-)

Что "всё же нет"? У вас во всех трёх примерах в baz нет вызова bar, а main так и вовсе пуст: DCE удалил единственный путь исполнения.

  1. Рекомендую ознакомиться с историей вопроса и пройти по всем ссылкам https://stackoverflow.com/questions/48067323/c-why-cant-this-be-a-nullptr в дополнение к стэку так же рекомендую заглянуть в современный стандарт https://en.cppreference.com/w/c/language/operator_member_access смотрите секцию dereferencing, а ещё вы можете найти здесь же на Хабре статьи про УБ + ещё здесь есть серия статей от компилятора строителя, можете написать в личку этим людям, а ещё можете написать вопрос в комитет для разъяснения, вот прям тут вызывайте российского представителя к сожалению не могу правильно написать имя юзера из мобильного Фокса, antoshka из Яндекса

  2. Даже не собираюсь, УБ по определению может сгенерировать любой код, в том числе тот на чье поведение вы расчитываете, но это не значит что код с таким поведением будет сгенерирован в другом компиляторе включая тот же самый с другими флагами.

Я знаком с вопросом более чем, и с Антоном мы уже обсуждали этот вопрос на с++russia 2019. Программа может быть написана так, что желание или нежелание избежать UB на это не влияет

Программа может быть написана так, что желание или нежелание избежать UB на это не влияет

Извините, я не понимаю, что вы хотите сказать.

Я знаком с вопросом более чем

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

Из опыта я знаю, что если люди допускают небольшие не точности ВМЕСТЕ с не корректными формулировками, то это верный индикатор не полного понимания проблемы.

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

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

A *a = nullptr;
a->bar();

Студия ветку не выкинула даже с оптимизациями, кланг выкинул, но с O0 оставил

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

Имеет место разыменование null-указателя, что является UB.

Как конкретно выглядит UB для конкретного компилятора конкретной версии, запущенного с определёнными флагами, не имеет существенного значения.

А зря. Должно иметь. Вообще не понимаю какого хрена компилятор что-то там делает с моим кодом, что меняет его поведение! Если там UB, то компилятор должен поступать просто: кидать ошибку и не компилировать, а не менять втихаря там что-то на свой вкус. Но почему-то так не делают...

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

Вообще не понимаю какого хрена компилятор что-то там делает с моим кодом, что меняет его поведение!

В том-то и дело, что поведение вашего кода не определено. Как можно изменить то, чего нет?

Что значит "не определено"? Вот возьмём код написанный выше:

A *a = nullptr;

a->bar();

Мы не знаем что делает этот код? Знаем: берёт нулевой указатель(который может быть как нулевой адрес, так и любой другой) приводит к типу объекта, затем вызывает функцию этого класса с нулевым указателем this. Если эта функция виртуальная - программа попробует извлечь указатель из памяти по нулевому указателю(которая может не существовать). Т.е. что делает код вполне определено.

А вот результат - не определен, т.к. по нулевому указателю не может(в теории) быть валидных данных.

И вот именно в таких случаях я хочу что бы компилятор не думал "А, тут хрень какая-то, кто в здравом уме будет по нулевому указателю читать, выкину-ка я это втихаря" говорил мне "Друг! У тебя тут UB! Я эту хрень компилировать не буду! Давай исправь!".

Нулевой указатель и указатель на нулевой адрес — это, с точки зрения языка, принципиально разные вещи.


Т.е. что делает код вполне определено.

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

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

Мне все равно кто на этот счёт мнения. Если я считаю это неправильным - беру и говорю. Имею право.

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

Удачи вам и дальше сидеть под веником.

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


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

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

Где я говорил, что пишу код который "компиляторы понимают неправильно"? Жду или цитата, или извинений.

я буду руководствоваться стандартом языка, а не своим мнением

"Руководствоваться своим мнением" и "Руководствоваться стандартом одновременно критикуя его" - это две разные вещи. Опять же - жду извинений.

Мое мнениепростое: UB в программе быть не должно. И компилятор если видит UB, намеренно сделанный или случайно, должен кидать ошибку и отказываться компилировать, а не прятать его. Давайте вообще ошибки из стандарта выкинем, а? Напишем что любая ошибка - это UB и компилятор может делать что хочет. Опечалатись в функции? Не беда! Пусть компилятор вызывает любую другую на его вкус!

Как вам такое? Нет, не хотите? А почему с UB такое хотите?

Мы не знаем что делает этот код? Знаем:

Операция -> разыменовывает указатель, потому что a->bar() является синтаксическим сахаром для варианта (*a).bar(), в котором уже точно невозможно отрицать наличие разыменования.

Разыменование null-указателя есть UB.

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

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

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

Во-первых, не все виды неопределённого поведения диагностируются компилятором. Garbage In - garbage out.

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

В-третьих, некоторые очевидные случаи компилятор трактует, как заведомо невозможные, решая задачу покрытия кода. Не компилировать вообще он не имеет права (синтаксически-то программа well-formed), но может нафигачить туда произвольный мусор. В частности, может нафигачить туда ловушек. Просто соберите проект с санитайзерами (конкретно, с ubsan), и тогда эти очевидные случаи будут взрываться немедленно и предсказуемо, а не творить дичь.

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

Самые яркие примеры последнего

  • this == nullptr. По стандарту невозможно, но активно эксплуатируется некоторыми легаси-библиотеками (MFC и ATL/WTL, например). Поэтому MSVC никогда не будет оптимизировать это место, а вот clang на определённых режимах сделает с большим удовольствием.

  • Арифметика и контроль переполнений (как через INT_MAX, так и за границы массивов) - традиционное для всех компиляторов (начиная с фортрана) место для жесточайших оптимизаций: упрощение формул, раскрутка циклов, вот это всё. И только пользователь может разъяснить: "да, в этом месте я никогда не вызову функцию, вылезающую за диапазон", или "ой я здесь имел в виду арифметику в кольце остатков", или "да не разыменовываю я! мне просто удобнее взять указатели далеко за границу вместо переводов в индексы и обратно"

  • union и всякие поздние инициализации. Специально для пуристов в стандарт затащили функцию "прачечная" std::launder, там чёрт ногу сломит в правилах времени жизни. Вот только весь легаси код, написанный до 2017 года, про эту прачечную не знает. Заставить переписывать?

Вот только визуализация не от heapsort, а от insertion sort.

Ваша правда, спасибо, исправил

Вот здесь, судя по всему, знающие люди собрались. Может кто-нибудь пояснить, а можно ли компилятор заставить ругаться каждый раз, когда он видит UB?

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

  1. -Wall/extra - больше предупреждений от встроеных анализаторов, включая некоторые для UB. Это не сделает UB ошибкой, но поможет выявить потенциально проблемные места.

  2. -Werror - сделает все предупреждения ошибками, можено включить в паре с -Wall для более строгой проверки.

  3. -fsanitize=undefined - включает AddressSanitizer и UndefinedBehaviorSanitizer в рантайме

  4. -pedantic-errors - злой флаг, включает параноик режим у кланга и все несоответствия стандарту выводит как ошибку.

  5. пройтись pvs-studio по проекту, отличный анализатор, ловит много чего, что пропускает компилятор

В дополнение к тому что выше, частично можно, пишите код в функциях/лямбдах с пометкой constexpr.

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

С этим советом нужно не забыть включить c++20, а то и c++latest (для ценителей, если конкретный компилятор уже осилил constexpr static_cast из void*), иначе что-то более-менее серьёзное написать будет невозможно.

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

Пара мыслей про описанные баги.

Кажется, что ошибки с забытой точкой запятой весьма распространены. Они часто упоминаются в разных статьях. Но, на самом деле, они встречаются очень редко. Более чем за 10 лет проверки открытых проектов мы нашли всего 3 таких ошибки. Думаю, сказывается, что все знают про эти ошибки и внимательны к ним. Плюс анализаторы и компиляторы, видимо, тоже хорошо помогают обнаруживать их.

И наоборот, ошибка с исчезновением memset кажется чем-то экзотическим и редким. Но это очень распространённая потенциальная уязвимость. Мы их обнаружили 100500 штук и продолжает обнаруживать. Хотя, казалось бы на эту тему тоже уже много написано: CWE-14, Zero and forget -- caveats of zeroing memory in C, Безопасная очистка приватных данных.

Видимо моя будет четвертой. У вас вышла мега отличная статья с антисоветами, благодарю. Тогда Всем отделом обсуждали несколько дней, но к счастью ничего словить не смогли. PVS стоит на страже. Слежу за Вашим блогом еще с По колено в Си++ г... коде.

Компилятор перехитрил сам себя. Итак, что происходит?

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

Кстати, кто-нибудь знает, может ли сгенерироваться подобный код функции, если компилятор увидит вместо неинициализированной переменной какое-нибудь memcpy(&x, src, sizeof(x))?

Конечно нельзя, я к тому что bool это не 0, 1 только, что и привело к такому.

Попробуйте получить значение bool со значением, отличным от true или false, и чтобы при этом одновременно отсутствовало UB.

В частности, распечатайте внутри serialize значение x и попробуйте добиться, передавая в эту функцию разные значения, чтобы распечаталось нечто отличное от true/false.

void serialize(bool x) {
	...
}

Также можно рассмотреть ассемблерный код такой функции:

bool fun(int const x) {
	return x;
}

чтобы понять смысл.

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

С точки зрения определения языка здесь нет неопределённого поведения.

Вам так кажется, или вы сверялись с первоисточником или его условным эквивалентом?
Вот описание конкретного случая, который так и называется: "Uninitialized scalar".

Не бывает условных эквивалентов первоисточника, и приведённое вами пережёвывание не исключение, тем более, что код С, а не С++, что бывает важно в ряде случаев.

Тем не менее, для данного случая и первоисточнике(черновике) указано не про unspecified value, как это мне запомнилось, а indeterminated value, то есть, unspecified or trap value. Только обращение к последнему и является неопределённым поведением. Однако clang для большинства платформ не определяет никакого trap value для целых, к каким относится и bool. Более того, само по себе обращение к неинициализированным переменным не детектируется при -fsanitize=undefined, что хотя бы и как-то, но говорит об отношении самих разработчиков компилятора к наличию trap value, а уж проверить его для bool было проще простого. Остаётся только unspecified value, которое не позволяет с ним вести себя как угодно.

Не бывает условных эквивалентов первоисточника

Похоже, вам действительно многое кажется, и вы верите в то, что вам кажется.

Однако clang для большинства платформ

В первоисточнике — не слова о clang'е, и вряд ли есть хоть слово о платформах.

Более того, само по себе обращение к неинициализированным переменным не детектируется при -fsanitize=undefined, что хотя бы и как-то, но говорит об отношении самих разработчиков компилятора к наличию trap value, а уж проверить его для bool было проще простого.

Какие ещё "-fsanitize=undefined", какие ещё разработчики?
В первоисточнике — ни слова ни о том, ни о других.

Обратимся к первоисточнику версии 20 (6.7.4 Basics/Memory and objects/Indeterminate values):

1 When storage for an object with automatic or dynamic storage duration is obtained, the object has an indeterminate value, and if no initialization is performed for the object, that object retains an indeterminate value until that value is replaced ([expr.ass]).

[Note 1: Objects with static or thread storage duration are zero-initialized, see [basic.start.static]. — end note]

2 If an indeterminate value is produced by an evaluation, the behavior is undefined except in the following cases:

Далее в исключениях идёт речь об ordinary character types и типе std​::​byte, то есть, это не наш случай, потому что в нашем случае — тип bool.

Итак, о чём говорит первоисточник, если оставить суть?
О том, что "object retains an indeterminate value", и что "If an indeterminate value is produced by an evaluation, the behavior is undefined".

И в нашем случае evaluation, несомненно, происходит.
В момент передачи значения при вызове функции.

Соответственно, в нашем случае the behavior is undefined.

В первоисточнике — не слова о clang'е, и вряд ли есть хоть слово о платформах.

Если вы продолжите знакомиться с первоисточником, то вы возможно узнаете, что в нём всё-таки что-то сказано о платформах. В частности, что наличие или отсутствие trap value определяется реализацией(за некоторым исключением гарантий его отсутствия). Дальше, возможно, сможете это сложить с тем, как это относится к clang и наличию или отсутствию у него trap value для bool.

"If an indeterminate value is produced by an evaluation, the behavior is undefined".

В рассматриваемом коде нет получения indeterminate значения через evaluation

В рассматриваемом коде нет получения indeterminate значения через evaluation

Чтение неинициализированной автоматической переменной при инициализации параметра функции и есть часть процесса evaluation. И в данном конкретном случае такое чтение вызывает неопределённое поведение. В стандарте даже есть схожий пример кода. basic.indet / Example 1:

unsigned char c;
unsigned char d = c;          // OK, d has an indeterminate value
int e = d;                    // undefined behavior

В неинициализированной локальной переменной значение не специфицированно, что всего лишь означает, что в ней, пусть и неизвестное, но одно из легальных значений, что для bool это true или false.

В черновике 17 стандарта есть даже сноска про то, что чтение неинициализированной bool переменной может вернуть нечто отличное от true и false. basic.fundamental / 50):

Using a bool value in ways described by this International Standard as “undefined”, such as by examining the value of an uninitialized automatic object, might cause it to behave as if it is neither true nor false.

Если вы продолжите знакомиться с первоисточником, то вы возможно узнаете, что в нём всё-таки что-то сказано о платформах. В частности, что наличие или отсутствие trap value определяется реализацией(за некоторым исключением гарантий его отсутствия).

Видите, там речь идёт о реализации.
Вам опять показалось, что о платформе.

Однако, как это относится к обсуждаемому вопросу о UB?
Это что-то проясняет по данному вопросу?
Или, всё-таки, нет?

Дальше, возможно, сможете это сложить с тем, как это относится к clang и наличию или отсутствию у него trap value для bool.

Об отношении стандарта к clang'у я уже писал.

Единственным первоисточником является стандарт, а clang — это вторичный инструмент, который стараются сделать соответствующим стандарту.

В рассматриваемом коде нет получения indeterminate значения через evaluation

По этому поводу вам уже ответили.

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

Если это — так, то — неинтересно.

Кстати, кто-нибудь знает, может ли сгенерироваться подобный код функции, если компилятор увидит вместо неинициализированной переменной какое-нибудь memcpy(&x, src, sizeof(x))?

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

const char* whichString = x ? "true" : "false";

Ох не стал бы я так писать. Вообще непонятно, как память в таком случае выделяется. Скорее в скомпилированном файле будут обе строки содержаться. Либо компилятор выкинет что-то при оптимизации, если известен x.

len = 5 - x

Откуда тут взялся 5, я понял, а откуда взялся -x, нет. Может, пример не полный?

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

Тут как раз вопросов нет, .data сегмент и создан для подобного.

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

А что именно тут непонятного-то? "true" и "false" — это литералы строк, для них память выделяется статически. Какие там дальше над указателями операции происходят — не важно, помять они не выделяют.

Если на этапе компиляции x будет известен, будет ли выделяться память под обе строки при оптимизации программы?

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

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

Продублирую код здесь:

#include <cstdint>
#include <cstring>
#include <cstdio>

char destBuffer[16];
static void serialize(bool x) {
	const char* whichString = x ? "true" : "false";
	const std::size_t len = strlen(whichString);
	memcpy(destBuffer, whichString, len);
	destBuffer[len] = '\0';
}

int main() {
	bool x{true};
	serialize(x);
	std::printf("%s\n", destBuffer);
}

Компиляторы clnag, gcc, и icx -- мало того, что не стали память выделять и потом оттуда копировать в буфер, сгенерировав вместо этого код, который прямо кладёт байты нужной строки в буфер, и эти байты являются непосредственным операндом ассемблерной инструкции (строка 4 в ассемблерном коде ниже), -- так ещё и функции подменили ( printf подменена на puts, строка 6 в ассемблерном коде ниже), пока программист не видит.

Ассемблерный код, сгенерированный clang'ом:

main:                                   # @main
        push    rax
        lea     rdi, [rip + destBuffer]
        mov     dword ptr [rip + destBuffer], 1702195828
        mov     byte ptr [rip + destBuffer+4], 0
        call    puts@PLT
        xor     eax, eax
        pop     rcx
        ret
destBuffer:
        .zero   16

Если убрать static у serialize, то обе строки появятся в ассемблерном коде.

мои пять копеек. тут вы сами подсказали компилятору, что не будете использовать serialize за пределами модуля a.obj, функция небольшая влезла в некоторый объем, который можно инлайнить и он целиком сделал её по месту вызова. если static убрать, то предполагается что функция может быть заиспользована в другом модуле b.obj, к которому доступ будет только на этапе линковки. Поэтому функцию надо делать отдельно

Я отвечал на такой вопрос:

Если на этапе компиляции x будет известен, будет ли выделяться память под обе строки при оптимизации программы?

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

Поэтому функцию надо делать отдельно

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

То, что функция была при'inline'нена, ничего не меняет в смысле выделения памяти под строки программы.

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

variadic template для функции НЕ принимает больше аргументов, чем указано в объявлении

в объявлении указано, что принимает N и при инстанции шаблона генерируется функция, которая принимает ровно необходимое количество аргументов

и что касается истории про Switch

по стандарту std::thread должен падать, если он еще работает, для него не был вызван join (или detach) и происходит разрушение объекта

соотвественно, после вызова деструктора мютекса, должен быть вызов std::terminate из деструктора thread

должен был, но не падал. Ошибка все еще жива в последних сдк

Sign up to leave a comment.

Articles