Как стать автором
Обновить

Комментарии 47

Даже в 23-м веке проблемы с памятью и указателями в C/C++ будут актуальны :)
«Малявата будет». Слишком мелко посты надроблены, никакого мяса.

Впрочем, добавить в этот пост можно было бы только проблемы с игнором malloc, с которым я уже могу поспорить. В последнее время развелось overcommitment для памяти, так что память выделяется, вот только пользовать её не получится когда она закончилась. То есть падение уже и так оттянуто до момента юза… :/ Так что проверять возврат маллока обычно бесполезное действие.
Так что проверять возврат маллока обычно бесполезное действие.
О! У меня отдельная статья на эту тему есть. Буду громить такой подход в хвост и гриву. Есть 4 причины, почему это неправильно. Но придётся немого подождать, это 6-ая часть.
Я знаю минусы этого подхода, да. Но вообще, в моём понимании переход с возврата ошибки на исключение вызван именно этим.
НЛО прилетело и опубликовало эту надпись здесь
а у memset аттрибут какой-то проставлен, что ее можно удалять?
Нет. Это просто функция. Она меняет объекты, которые затем не используются. Значит её можно удалить. Так компилятор поступает и с другими функциями. memset ничем не особенная функция. Просто один из методов оптимизации.

можете показать как правильно написать функцию чтобы компилятор ее вызов железно не удалил, там где удаляет memset?
Надо использовать специальные функции. Про них говорится в статье. Или написать свою, используя volatile. Специальные атрибуты мне не известны. Возможно, кто-то что-то на эту тему напишет здесь.
НЛО прилетело и опубликовало эту надпись здесь
специальная функция от memset отличается чем?
Тем, что она специальная. А как это достигается, прикладного программиста волновать не должно. Может использоваться какое-то непереносимое расширение компилятора.

Если говорить про самодельные функции, то в них всё вертится вокруг volatile. Раз что-то volatile, то это нельзя оптимизировать и выбрасывать. Но вообще самоделки — это плохо. Это на крайний случай.
НЛО прилетело и опубликовало эту надпись здесь
volatile object — an object whose type is volatile-qualified, or a subobject of a volatile object, or a mutable subobject of a const-volatile object. Every access (read or write operation, member function call, etc.) made through a glvalue expression of volatile-qualified type is treated as a visible side-effect for the purposes of optimization (that is, within a single thread of execution, volatile accesses cannot be optimized out or reordered with another visible side effect that is sequenced-before or sequenced-after the volatile access. This makes volatile objects suitable for communication with a signal handler, but not with another thread of execution, see std::memory_order). Any attempt to refer to a volatile object through a non-volatile glvalue (e.g. through a reference or pointer to non-volatile type) results in undefined behavior.

И из CWE-14: Potential Mitigations. Phase: Implementation. Store the sensitive data in a «volatile» memory location if available.
НЛО прилетело и опубликовало эту надпись здесь
Что мешает сделать так?
foo() {
   char *bla;
   volatile char *kostyl = bla;
   bar(kostyl);
}

НЛО прилетело и опубликовало эту надпись здесь
путь что «и волки сыты и овцы целы»
Это использовать такие функции, как memset_s. Компилятор знает, что от него хотят и обязательно затрёт память.
НЛО прилетело и опубликовало эту надпись здесь

Поддерживаю политику отказа от memset в плюсовом коде — это действительно тот еще рассадник ошибок.


Однако, лучше инициализировать массив конструкцией вида int a[10000] = {0};.
При ревью кода конструкцию вида int a[10000] = {}; можно не заметить, а если в контексте задачи инициализация массива нулями не требуется, то это незаметная конструкция сильно ударит по производительности кода...

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

С одной стороны вопрос, конечно, субъективный.
Но с другой стороны, предпочитаю, что бы код максимально сам себя описывал.
То, что конструкция '={0}' протрет массив нулями, на мой взгляд, интуитивно понятнее, чем '={}'.

То, что конструкция '={0}' протрет массив нулями, на мой взгляд, интуитивно понятнее, чем '={}'.
Ага, конечно. То, что вы явно задали значение одного поля из десятков так ведь важно.

Если кому-то «итуитивно понятнее», что один-единственный нолик там что-то меняет, то я вообще не уверен, что ему нужно на C++ программировать.

Потому что вот:

$ cat test.cc
#include <iostream>

int main() {
  int a[10] = {1};
  for (int i=0;i<10;++i) {
    std::cout << a[i] << "\n";
  }
}
$ g++ test.cc -o test
$ ./test
1
0
0
0
0
0
0
0
0
0


Что там ваша интуитивность про ={1} говорит?
Ага, конечно. То, что вы явно задали значение одного поля из десятков так ведь важно.

Не вводите людей в заблуждение. Рекомендую прочитать reference перед тем, как громко что-то утверждать.


Конструкция a[10000] = {0} гарантированно инициализирует нулями весь массив на c++.

Дело не в этом. А в том, что изменение константы с одного(почему-то специального) значения, на другое (раньше вычисляли суммы — нейтральный элемент был 0, а теперь надо произведения и элемент 1) меняет поведение в корне.

На мой личный взгляд интуитивнее писать = {} для инициализации нулями, чтобы не возникло у кого-нибудь желание «просто поменять константу».

P.S. и да, остаётся открытым вопрос — а как другими( не 0) значениями вы бы инициализировали большой массив? Кроме fill особо ничего на ум не приходит.
Дело не в этом. А в том, что изменение константы с одного(почему-то специального) значения, на другое (раньше вычисляли суммы — нейтральный элемент был 0, а теперь надо произведения и элемент 1) меняет поведение в корне.
Спасибо за то, что обьяснили что именно меня в этом выражении раздражает. Да, именно так. Тот факт, что 0 заменить на 1 нельзя. Для людей, которые об этом знают — 0 там не нужен, они знают, что двух фигурных скобочек достаточно, для тех, кто этого не знает — он просто опасен, так как возникает соблазн «в случае чего» поставить там что-то другое… и потом долго ловить глюки. Так для кого он там?
P.S. и да, остаётся открытым вопрос — а как другими( не 0) значениями вы бы инициализировали большой массив? Кроме fill особо ничего на ум не приходит.
Увы. Clang позволяет писать вот так, но, увы, GCC это поддерживает только в C, а многие другие компиляторы — не поддерживают такой синтаксис вовсе.

Но собственно уже наличие подобных (пусть и нестандартных) расширений указывает на то, что в рамках стандарта хорошего решения нету…
Этот синтаксис тоже плох, так как размер указывается дважды и оба раза явно.
Размер можно опустить (в описании массива) — тогда он будет вычислен так, чтобы все индексы «влезли». Или можно в правой части можно использовать arraysize. Конечно нельзя сделать и то и другое одновременно: где-то же ведь размер должен быть указан!

По большому счёту единственная проблема — то, что этого нет в стандарте… даже в проекте C++20…

Некоторые старые компиляторы не поддерживают пустые скобки (не знаю что там со стандартами но речь не про них а про практику). Я например и не знал что так можно, пока не прочёл эту ветку комментариев. Пробовал когда-то и выдало syntax error, ну нельзя так нельзя, поставим первый ноль для успокоения компилятора, а так как это вещь не особо важная то разбирательства не последовало.

Вот это — уже другая история. Возможно и в Chromium из-за совместимости со старыми компиляторами такое делали изначально, а потом вступило в силу правило if you are editing code, take a few minutes to look at the code around you and determine its style.

Но говорить, что это — что-то, что вот прямо лучше читается… жуть.
Конструкция a[10000] = {0} гарантированно инициализирует нулями весь массив на c++.
А почему, собственно?

Рекомендую прочитать reference перед тем, как громко что-то утверждать.
Читаю: All array elements that are not initialized explicitly are initialized implicitly the same way as objects that have static storage duration.

В вашем примере происходит следуюшие:
1. Один первый элемент вы инициализируете нулём явно.
2. 9999 элементов получают нулевое значение вследствие неявной инициализации.

Если вы уж доверили компилятору инициализировать почти весь обьект в «значение по умолчанию» — то нафига вам отдельно указывать что один специально выделенный, первый, элемент вы тоже хотите в ноль превратить???

Единственный вариант — чтобы привлечь внимание. В смысле надёжности и понятности = {} и = {0} идентичны, только второй вариант поднимает вопрос: а автор точно знает как C++ вообще работает? Зачем он один-единственный элемент (из десятков, а, возможно, и тысяч) инициализировал явно-то? Какой в этом тайный смысл?

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


Ради интереса погрепал исходники хромиума, который разбирается в статье. Авторы в 10 раз чаще используют подход arr[]={0}, чем arr[]={}


bash-3.2$ find chromium/ -name "*.h" -or -name "*.cc" | xargs -n1 grep "] = {}" | wc -l
       5
bash-3.2$ find chromium/ -name "*.h" -or -name "*.cc" | xargs -n1 grep "] = {0}" | wc -l
      59
Лично для меня важнее заметить факт непредвиденной инициализации большого массива 0, для вас, что бы автор кода понимал, детали подкапотного действа в компиляторе.
Проблема в том, запись вида {0} даёт очень много поводов неправильно воспринять код. Можно посчитать, что заменив на 0 на 1 вы получите правильную инициализацию, можно посчитать, что убрав этот несчастный ноль вы позволите компилятору не тратить время на обнуление массива… Много способов воспринять код неправильно.

Хуже того: не зная «деталей подкапотного действа» очень легко «увидеть», что инициализован тут только один элемент и «поправить» код всунув сюда memset — а это явно не то, чего мы хотим, ведь правда?

И да — это вопрос вкуса. Для меня самое главное — чтобы код был не был понят неправильно. А для вас — чтобы у читателя глаз «не зацепался», а если он чего-то неправильно понял и из-за этого машина вьедет в столб… ну чего такого — мы покойнику бесплатно patch вышлем!
Конструкция a[10000] = {0} гарантированно инициализирует нулями весь массив на c++.

Конструкция a[10000] = {} вызовет конструктор по умолчанию для всей 10000 элементов. Конструкция a[10000] = {0} вызовет конструктор с одним аргументом для первого элемента (если он определён), и конструктор по умолчанию для оставшихся 9999 элементов. Лично мне, первый вариант кажется более правильным, т.к. он обрабатывает все элементы одинаково. Во втором же случае, мы неявно подразумеваем, что конструктор по умолчанию эквивалентен конструктору с одним параметром. А это может быть совсем не так. Да и в принципе это нарушает семантику: мы хотим чтобы элементы обработались одинаково, но почему-то делаем первый элемент особенным. Это не правильно.
Здесь вообще обнуляется только первый элемент массива и один байт во втором элементе.

Не отрицаю, что у memset есть и другие проблемы, но конкретно эта (на мой взгляд самая частая) решается запретом на её использование без sizeof() в третьем аргументе либо просто параметром, либо в качестве множителя. Причём, по возможности, не sizeof(тип) а sizeof(переменная) или sizeof(*переменная) — подсмотрел такой приём в исходниках FreeBSD и с тех пор везде стараюсь так делать. После чего про неё можно забыть. То же самое касается и memcpy.

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


не sizeof(тип) а sizeof(переменная)

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


sizeof(*переменная)

это плохо, потенциально можно словить nullptr dereference (что, конечно, маловероятно на практике, но тем не менее без проверки переменная != nullptr — эта конструкция является UB)

Если там null то он и в первом аргументе memset окажется (имелось ввиду что надо либо её размер либо размер её элемента (для массива) проверять). Так что UB будет и так и так.
Но с другой стороны мне сложно представить, какими мотивами могли руководствоваться авторы компилятора, который будет делать частное dereference внутри такого sizeof.

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

Хотя я предпочитаю писать sizeof переменная (без скобочек) — сразу ясно, что мы не с типом работаем.
Примечание. sizeof не вычисляет выражение. Вычисляется только тип. sizeof(*переменная) не может привести к nullptr dereference.

Согласен, прямо сейчас ни одна реализация не вычисляет, но стандарт c++17 допускает Temporary materialization prvalue в sizeof.
Да, *ptr это конечно не prvalue, но тем не менее — в какой то момент код очень похожий на sizeof *ptr может сломаться от nullptr в ptr.

Отлично! Меня радуют такие новшества в C++. С ними, анализатор PVS-Studio всегда будет нужен. :)

Пока будет нужен С++ :)

Temporary materialization occurs in the following situations:
  • when sizeof is applied to a prvalue (this is part of an unevaluated expression)

это плохо, потенциально можно словить nullptr dereference

Неа, nullptr dereference вы не словите, даже если если там null. sizeof() даже не пытается вычислять значение аргумента, а просто по честному подставляет его размер на эта преобразования в ассемблер. Именно по этому я пишу обращения в функции malloc и ей подобным в духе:
int *ptr = malloc(100 * sizeof(*ptr));
А у вас были статьи с рейтингом наиболее качественных проектов по мнению PVS? Было бы интересно почитать.
Нет. Часто проекты большие и сложно внимательно изучить все предупреждения. Соответственно, для них невозможно указать плотность ошибок. Для небольших проектов, таких как Notepad++, мы указываем плотность ошибок. И возможно при случае напишем статью сравнение. Но жаль, что в ней будут только такие небольшие проекты.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий