Pull to refresh

Comments 23

Класс. Найти ошибку в файлах майкрософта предлагаю сделать программой минимум для испытания анализаторов) Например, в VS2017 в Community\Common7\IDE был отличный файл srcsrv.ini, примерно такого содержания:
;Path to tf.exe. We should have a better solution, and Dev15 Bug 194740 is tracking this…
Непонятно только, почему Вы везде пишете «ложное срабатывание». Мне кажется, что анализатор прав, он ведь работает не по документации, а по реальному заголовочному файлу, так? И не важно, что все эти стилистические изыски в коде не учитываются реальным ABI.
Это пользователь говорит «ложное срабатывание».
Возможно, мне показалось, что в конце статьи это написали Вы. Неважно, но приношу извинения.
Мне кажется, что истинный «пафос» статьи в том, что либо реальные системные библиотеки Windows собираются не с теми заголовками, которые поставляются в составе публичного SDK, либо собираются со значимыми предупреждениями, если не с ошибками компиляции. Казалось бы, при чем здесь Лужков?! (С).
Сам того не планируя, я начал писать статьи, посвященные развенчанию мифа, что статические анализаторы — это 90% шума и 10% пользы. Да, действительно, статические анализаторы в силу своей природы дают ложные срабатывания. Однако, подавляющее количество ложных срабатываний можно исключить, проведя минимальную настройку анализатора. Большинство бессмысленных срабатываний связано с неудачными макросами и опасным стилем кодирования (например, нет проверки указателя после malloc).

Примечание. Кстати, в срабатываниях на макросах анализатор часто формально прав. Но с практической точки зрения это никого не волнует и получается, что это всё равно ложные предупреждения :).

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

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

Андрей, подскажите, а не может ли возникнуть ситуации когда компилятор что-то «наоптимизирует» на основе некорректных const qualifiers, что в итоге приведёт к некорректному «с практической точки зрения» исполнению кода? Я имею в виду «сложные» оптимизации, в т.ч. основанные на undefined behavior и т.п. если что...

Я не могу дать точный ответ, но вижу ситуацию следующим образом. Совсем чисто теоретически такое можно представить. Например, компилятор, как и анализатору, может предположить, что переменная не изменится и удалит проверку, заменив её на true. На практике такое не случится, так как компилятор не видит внутренности тела функции и не рискнёт сделать такую смелую оптимизацию. А вдруг внутри функции кто-то снимет константность, используя const_cast.

Компилятор имеет право и может заменить это выражение на true.


const_cast снимает модификатор const со ссылки, но не позволяет писать в неё. Попытка сделать это приводит к undefined behavior.


Так что хоть я не смог заставить ни один компилятор на godbolt превратить эту проверку в true, полагаться на такое поведение компиляторов не стоит.

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

Если не менять объекты после const_cast, то тогда зачем вообще этот оператор нужен? :)

Даже разработчики компиляторов так делают. Вот первый попавшийся фрагмент кода из Clang:

static void getCLEnvVarOptions(
  std::string &EnvValue, llvm::StringSaver &Saver,
  SmallVectorImpl<const char *> &Opts)
{
  llvm::cl::TokenizeWindowsCommandLine(EnvValue, Saver, Opts);
  for (const char *Opt : Opts)
    if (char *NumberSignPtr = const_cast<char *>(::strchr(Opt, '#')))
      *NumberSignPtr = '=';
}

Ммм. Вы правы. Я неправильно интерпретиловал следующую цитату:


Even though const_cast may remove constness or volatility from any pointer or reference, using the resulting pointer or reference to write to an object that was declared const or to access an object that was declared volatile invokes undefined behavior.

А конкретно я упустил


using the resulting pointer or reference to write to an object that was declared const

Т.е. если у нас есть объект, который не объявлен как const, мы его скастовали в const, а потом кастом сняли const, в него можно писать и это не ошибка.


Так что кажется что ни в моём, ни в вашем примере UB нет.
Если функция получает объект по const ref, компилятор не знает как этот объект был объявлен. А значит компилятор не может предположить, что неконстантный объект, который мы передали по константной ссылке не может быть изменён.

Хотя нет, я вот на что среагировал:
Конкретно в данном случае излишняя интеллектуальность анализатора привела к тому, что из-за неправильного описания функции он начал выдавать ложное срабатывание.

А можно подробнее раскрыть утверждение: "Аргумент является указателем на константный объект. Получается, что с точки зрения анализатора функция GetNamedSecurityInfoW не может менять объект, на который ссылается указатель. "?


void foo(const int** pp){
const int* pi;
*pp = pi;
}
const int* p;
foo(&p);

Сигнатура функции гласит утрировано const T** p; Это же про константый объект, но не указатель на который указывает переменная. Откуда взялся вывод, что функция не может менять указатель, если указатель указывает на указатель, который не константый?

Легко могу ошибиться, но я понимаю проблему так:
#define PINT int*
void f(const PINT * pi);
// препроцессируется в псевдокод
void f(const (int *) * pi);
// east const rules!!!
void f( int * const * pi);

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


#define PINT int*
void foo(const PINT* pp){
    *pp = nullptr;
}

typedef int* pint;
void foo(const pint* pp){
    // *pp = nullptr;  
}

int main(int argc, char** argv)
{
    const int* p;
    foo(&p);

    int * const pc = NULL;
    foo(&pc);

    int* pi = NULL;
    foo(&pi); // Case 2.
    return 0;
}
Тут вообще разница в обработки объявлений сигнатуры функции и определения функции.
В C квалификатор в передающемся по значению аргументе не учитывается и часто является причиной предупреждения компилятора.
В C++ (если мне не изменяет склероз) такой квалификатор не учитывается, но допускается.
В определении функций квалификаторы имеют смысл и в C, и в C++, поскольку в определении функции аргумент является определением локальной переменной. В этом случае квалификатор const «играет», хотя в C его смысл и отличается от C++ (тут константа просто константа, а не readonly переменная).
Если бы advapi32.dll компилировалась с теми заголовками, в которых аргументы квалифицированы, как const, то было бы как минимум предупреждение компилятора о несоответствии прототипа определению функции.
Как-то так,
Искренне Ваш КО)
typedef int near *PINT;
foo(const PINT *x);
В функцию передаётся указатель на константный объект. Значит, функция не может изменить объект, который пришёл в функцию из вне. В данном случае из вне приходит объект типа PACL (который на самом деле указатель). И вот этот объект (указатель) менять нельзя.
А поменялось ли само предупреждение после исследования?
Возможно дополнительные сведения помогли бы пользователю разобраться самостоятельно.
Сколь часто Вы рассматриваете препроцессированный код единицы компиляции? Увы, я даже не помню, когда делал это сам, хотя с 1992 года пару раз делал, помню)))
Статья на самом деле классная в частности именно тем, что напоминает о важности этого «секретного приемчика» в разработке.
Первый и последний раз смотрел для файлов, сгенерированных flex и bison лет 5 назад, больше и не помню.
Эта простыня из заголовочных файлов помогает ощутить весь объём неиспользуемых зависимостей, можно открыть для себя что-то новое и неожиданное :)
Нет. К сожалению, сделать объясняющее предупреждение намного сложнее, чем может показаться на первый взгляд.

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

Кстати, здесь бы и путь не помог. Вот переменная равна 0. Вот вызывается функция, которая не меняет эту переменную. Вот всегда истинное условие. То, что анализатор посчитал, что функция не меняет значение, это и так было понятно. Если же анализатор ещё начнёт отправлять ко всем объявлениям функций в системных заголовочных файлах… Человек тогда вообще потеряется в обилии данных и удалит анализатор :).
И, что наиболее печально, нельзя его менять только с точки зрения статического анализатора (including but not limited to extended compiler wornings))). С точки зрения языка константность/волатильность аргумента, передаваемого по значению и/или возвращаемого значения не являются частью прототипа функции, не участвуют в overloading'ге и т.д.
Что меня всегда удивляло, так это то, что в C T const * и T * — разные типы в списке аргументов прототипа функции, но не в возвращаемом значении.
Если я не прав, то пусть лучше знающие стандарты C и C++ меня поправят.
Но ведь аргумент был передан не по значению, а по указателю…
Sign up to leave a comment.