Вебинар: C# разработка и статический анализ: в чем практическая польза? - 18.11
Поделитесь своим кодом с ошибкой и участвуйте в розыгрыше 10 книг "Вредные советы для C++ программистов" с подписью автора.
Аннотация: в книге читателю предлагается список "советов", которым не стоит следовать. Каждый совет сопровождается подробным разбором и рассмотрением неочевидных моментов. Эти пояснения будут полезны новичкам, изучающим программирование. Впрочем, книга развлечёт и профессионалов рассмотрением некоторых нюансов программирования на C++.
Автор: Карпов Андрей Николаевич, к.ф.-м.н. Более 15 лет занимается темой статического анализа кода и качества программного обеспечения. Автор большого количества статей, посвящённых написанию качественного кода на языке C++. С 2011 по 2021 год удостаивался награды Microsoft MVP в номинации Developer Technologies. Один из основателей проекта PVS‑Studio.
Опубликуйте свой код на этой странице до 30 декабря 2023 года, и в указанную дату наша команда отберёт 10 наиболее интересных ошибок. В январе мы свяжемся с победителями для отправки приза — книги "Вредные советы для C++ программистов" с подписью автора. При отправке примеров кода через форму ниже вы соглашаетесь на то, что мы используем эти материалы для отдельной публикации в нашем блоге.
Введите код на языке C или C++ в поле “Код”, укажите ваше имя (оно будет отображаться рядом с вашим кодом) и email (не будет публиковаться, нужен для связи с победителями). По желанию сопроводите свой код какой-то историей или описанием ошибки. Можете опубликовать несколько вариантов кода с разными и интересными ошибками, тогда шансов на выигрыш будет больше.
Внимание: код будет опубликован на сайте. Поэтому воздержитесь от публикации кода, если это нарушает какие-то условия, например NDA с вашим работодателем. Или перепишите код, чтобы он отражал суть, не раскрывая закрытую информацию, — являлся синтетическим примером.
#include <algorithm>
#include <vector>
#include <iostream>
std::vector<int> vec{1, 2, 3, 4, 5};
std::transform(vec.begin(), vec.end(), vec.begin(), [](auto& c){return c*2;});
std::cout << vec << '\n';
К концу очередного осеннего курса питона, после очередного объяснения, чем отличается список от массива numpy, решил для удовольствия написать умножение вектора на число на С++. Написал и уставился в список ошибок компилятора. Тупил долго, смеялся еще дольше. Стиль питона явно угадывался.
char* szOriginal = (char*)malloc(100);
strcpy(szOriginal, "ABCDEFGHIJKLMNOPQRSTUVWXYZ");
char* szShift = (char*)malloc(100);
strcpy(szShift, "ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ");
szShift += iShift;
//...
free(szOriginal);
free(szShift); // тут проблемка
Всего лишь в строке 10 память освобождается неверно. Перед 10-ой строкой надо было сделать 'szShift -= iShift;', т. е. обратить операию в пятой строке. Но самое интересное, что это вообще ни на что не повлияло, ведь код был написан в рамках тренировки к олимпиаде по информатике для школьников, и программа проходила все тесты, т. е. завершалась корректно и выдавала правильный ответ.
Debug() << "Test" << testName << (result ? "FAILED" : "successed");
Мы писали свою библиотеку для юнит-тестов, я добавил туда такую прекрасную строчку кода :) В итоге потом разбирались как мы так сервер сломали, что ни один тест не проходит, а на деле просто FAILED и successed местами перепутаны были, глупее и придумать нельзя)
algorithm.h
...
template <class _T, size_t _N>
inline constexpr _T* begin(_T (&_array)[_N]) { return _array; }
template <class _T, size_t _N>
inline constexpr _T* end(_T (&_array)[_N]) { return _array + _N; }
...
gtl\algorithm.h(9,30): error C2065: 'L': undeclared identifier
gtl\algorithm.h(9,30): error C2065: '_array': undeclared identifier
gtl\algorithm.h(9,24): error C7525: inline variables require at least '/std:c++17'
gtl\algorithm.h(9,48): error C2447: '{': missing function header (old-style formal list?)
gtl\algorithm.h(12,28): error C2065: 'L': undeclared identifier
gtl\algorithm.h(12,28): error C2065: '_array': undeclared identifier
gtl\algorithm.h(12,46): error C2447: '{': missing function header (old-style formal list?)
gtl\algorithm.h(15,25): error C2365: 'gtl::begin': redefinition; previous definition was 'data variable'
gtl\algorithm.h(24,25): error C2365: 'gtl::end': redefinition; previous definition was 'data variable'
Гдето по пути попалось включение виндовых хедеров а там _T это L"", и препроцессор радостно и с песней заменил эти _T в шаблонах
struct IFace { ..... };
class TheImpl : public IFace { ..... };
class TheFixture {
public:
IFace& iface() { return aggregate_; }
private:
TheImpl aggregate_;
// и ещё всякий клей, вспомогательные объекты, связи и функции
};
class TheProvider {
public:
std::shared_ptr<IFace> provide() {
if (need_create()) create_cached_object();
return cached_object_;
}
private:
std::shared_ptr<IFace> cached_object_;
bool need_create() { return !cached_object_ || ....; } // кеша нет, кеш протух, всё такое...
// вот тут живёт бажок!
void create_cached_object() {
auto cached_object = std::make_shared<TheFixture>();
cached_object_ = std::shared_ptr<IFace>{cached_object_, &cached_object->iface()};
}
};
shared_ptr позволяет владеть объектом-сборкой, отдавая интерфейс на агрегат. Например, указатель на узел дерева или даже на содержимое узла дерева. https://en.cppreference.com/w/cpp/memory/shared_ptr/shared_ptr - сигнатура номер 8, "aliasing constructor" Это даёт возможности очень гибко создавать всякие нетривиальные реализации интерфейсов. Проблема в том, что нет никакого контроля - ни типа, ни значения аргументов такого конструктора. Можно делать вот такие странные указатели: https://gcc.godbolt.org/z/ncz88ovW4 Тем не менее, заниматься самоприсваиванием с изменением указателя на агрегат - вполне легально и даже может иметь практический смысл: https://gcc.godbolt.org/z/edEzMe953 Но с такой же лёгкостью мы можем получить преждевременный конец времени жизни! Что я и получил, сделав опечатку в реальном проекте! Здесь привожу иллюстрацию без NDA-подробностей. Дистиллированный код - с багом и с исправлением: https://gcc.godbolt.org/z/x53daYc5P ----- Заодно запостил на RSDN: https://rsdn.org/forum/cpp/8644453.flat#8644453
time64_t time_us; // time64_t is int64_t, микросекунды
float LineConvert( float value, float x1, float x2, float y1, float y2);
// Где-то в коде
...
time_us -= LineConvert( phase, 0.0f, 360.0f, 0.0f, period );
...
Не учтено( либо упущено в результате рефакторинга) продвижение типов. В результате через ПАРУ ЧАСОВ терялись значимые микросекунды. код переставал работать, как от него ожидалось. p.s. Масло в огонь подливало то, что заход в данный участок кода зависело от настроек устройства, которое редко выставлялось.
QString logFileName="/Directory/Logs/Log.txt";
const char *fileName = logFileName.toStdString().c_str();
Ошибка использования указателя на временный объект. Проблема в выражении: const char *fileName = logFileName.toStdString().c_str(); Когда срабатывает метод toStdString(), создается временный объект типа std::string как возвращаемое значение данного метода. Потом для этого временного объекта вызывается c_str(). Полученный указатель запоминается в fileName, но затем временный объект удаляется (ведь работа выражения завершена, объект std::string ничему не присвоен а значит и не нужен), и начинается выполнение следующей команды. Таким образом, указатель fileName начинает указывать на память, которая была выделена объекту, но затем сразу после завершения команды была реаллоцирована под другие данные.
try
{
try
{
if( a != 0 ) throw Exception("a != 0");
else return;
}
catch(Exception& e)
{
// a != 0
}
}
__finally
{
// a != 0
}
Ожидалось, что в блок __finally попадём в любом случае. Оказалось, что при a == 0 выход происходит немедленно
// separate header with platform specific defines
#define MIN_PRIORITY 256
#define MAX_PRIORITY 0
// lines of usage in implementation
if (priority > MIN_PRIORITY && priority < MAX_PRIORITY)
{
SetPriority(priority);
}
На одной из платформ приоритетность реверсивная. Статистический анализ на ней не запускался на CI.
struct B
{
virtual ~B(){}
};
struct D : B {};
void f(B& obj)
{
throw obj;
}
void g()
{
D d;
try
{
f(d);
}
catch(const D&)
{
std::clog << "D has been catched (can't be catched)" << std::endl;
}
catch(const B&)
{
std::clog << "B has been catched (always here catched)" << std::endl;
}
}
EH не работает с динамическими типами объектов. Вместо этого тип исключения проводится через цепочку обработчиков статически на основе типа выражения throw.
OPENFILENAME ofn;
memset(&ofn, 0, sizeof(ofn));
// ...
ofn.lpstrFilter = _T("All Files (*.*)\0*.*");
// Правильно: ofn.lpstrFilter = _T("All Files (*.*)\0*.*\0");
В WinAPI есть структура OPENFILENAME, чтобы создавать диалог выбора файла. Так вот, там фильтр задаётся набором строк, разделённых терминальным нулём. А чтобы понять, что список фильтров закончен, надо два терминальных нуля подряд. Я в начале не разобрался и долго тупил, почему то вроде всё хорошо, а как заказчику показывать, то в диалоге мусор какой-то.
size_t allbits = ~0u;
Всё было хорошо и замечательно, пока это было в 32-битной программе. Было 0xFFFFFFFF. Казалось-бы в 64-битной значение будет 0xFFFFFFFFFFFFFFFF. Реальность: 0x00000000FFFFFFFF. Всё честно, но неожиданно.