Silver 27 января 2020

Почувствуй силу: cовременные инструменты С++

Подробно о том, как модернизировать старый проект с помощью встроенных инструментов VS 2019 и Clang Power Tools.

В этой статье рассмотрим современные инструменты, посредством которых можно улучшить старые проекты на С++. Благодаря новейшим компиляторам и оптимизаторам можно перелопатить кучу кода, не тратя на это полжизни.

Введение

Приходилось ли вам встречать ужасные строчки вроде этих?

            float* pfloats = new float[10]; // no delete [] later! :)
int x = pfloats[0];
        

Заметить ошибку нетрудно. Даже базовый компилятор услужливо предупредит о возможной потере данных при конвертации float в int. Но что делать с более сложным кодом в работающем проекте?

Visual Studio 2019 имеет встроенный анализатор кода, который будет давать довольно полезные подсказки. Рассмотрим следующий пример:

            #include <iostream>

class SuspiciousType {
public:
    SuspiciousType() { }
    ~SuspiciousType() { std::cout << "destructor!\n"; }

    int compute(int z) { return x + y + z; }

    int x;
    int y;
};

int main() {
    SuspiciousType st;
    float* pfloats = new float[10]{ 100.5f };
    int z = pfloats[0];
}
        

В VS 2019 мы можем настроить правила проекта под свои нужды. Включить все пункты или создать детальный профиль.

При включении анализатора сразу получаем ряд предупреждений.

Сначала для класса SuspiciousType:

            cpptests.cpp(5): warning C26495: Variable 'SuspiciousType::x' is uninitialized. Always initialize a member variable (type.6).
cpptests.cpp(5): warning C26455: Default constructor may not throw. Declare it 'noexcept' (f.6).
cpptests.cpp(6): warning C26432: If you define or delete any default operation in the type 'class SuspiciousType', define or delete them all (c.21).
cpptests.cpp(6): warning C26447: The function is declared 'noexcept' but calls function 'operator<<<std::char_traits<char> >()' which may throw exceptions (f.6).
cpptests.cpp(8): warning C26440: Function 'SuspiciousType::compute' can be declared 'noexcept' (f.6).
        

А потом и в функции main:

            cpptests.cpp(16): warning C26462: The value pointed to by 'pfloats' is assigned only once, mark it as a pointer to const (con.4).
cpptests.cpp(17): warning C26496: The variable 'z' is assigned only once, mark it as const (con.4).
cpptests.cpp(17): warning C26481: Don't use pointer arithmetic. Use span instead (bounds.1).
cpptests.cpp(16): warning C26409: Avoid calling new and delete explicitly, use std::make_unique<T> instead (r.11).
cpptests.cpp(16): warning C26400: Do not assign the result of an allocation or a function call with an owner<T> return value to a raw pointer, use owner<T> instead (i.11).
        

Как видите, среда разработки успешно обнаружила все существенные недочёты. Более того, если предупреждение связано с несоблюдением гайдлайна – в конце будет указан конкретный пункт, который легко поправить, найдя его в официальном руководстве.

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

Если вы не используете последнюю версию Visual Studio, то обратите внимание на Clang Power Tools. Это расширение для студии даёт приблизительно такой же набор функций.

Но разбирать откровенно дурной код это одно, а можно ли извлечь пользу из этих инструментов на реальном проекте?

Рассмотрим проект покрупнее

В декабре 2019-го я открыл свой старый проект со времён учёбы. Это визуализация алгоритмов сортировки, написанная в далёких 2005/2006 годах на старом С++, Win32Api и OpenGL. Код можно посмотреть в репозитории.

Программа принимает на вход массив значений и обрабатывает их со скоростью около 30 действий в секунду. Каждый элемент отрисовывается на диаграмме. Зелёным обозначается элемент, к которому производится обращение в данный момент, а голубым – сортируемая в данный момент часть массива. Демонстрацию на примере Quick sort вы видите в гифке выше.

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

Это довольно интересный опыт – изучать свой код, написанный 15 лет назад. Я решил переделать проект под VS 2019. Чтобы вспомнить, как всё работало, я реализовал Quick sort, которого изначально в программе не было.

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

Сообщения о проблемах

Когда я включил анализатор кода на полную, то получил 956 предупреждений... Этого стоило ожидать, давайте рассмотрим их подробнее.

Использование констант

Компилятор видит переменные, не меняющие значение в процессе выполнения, и автоматически предлагает использовать константы.

Например, для такого кода:

            case cmYawPitchRoll: {
    float r = cos(m_fPitch);
    float x = r*sin(m_fYaw);
    float y = sin(m_fPitch);
    float z = -r*cos(m_fYaw);
    m_vTarget = VECTOR3D(x, y, z);
    m_vUp = VECTOR3D(sin(m_fRoll), cos(m_fRoll), 0.0f);
    break;
}
        

Предупреждение выглядит так:

            Warning    C26496    The variable 'r' is assigned only once, mark it as const (con.4).
        

Это касается и функций:

            // ang * M_PI / 180.0f
inline float DegToRad(float a) { return a*0.01745329252f; };  
// rads * 180.0f / M_PI
inline float RadToDeg(float a) { return a*57.29577951f; };    
        

Предупреждение:

            Warning    C26497    The function 'DegToRad' could be marked constexpr if compile-time evaluation is desired (f.4).
        

Неинициализированные переменные

К сожалению, тогда в моём коде это было распространённой ошибкой. Например, для CGLFont я забыл про m_fSize.

            CGLFont(): m_FontMode(fmNone), m_iList(0), m_iTexture(0) { }
        

И получил такое предупреждение:

            Warning    C26495    Variable 'CGLFont::m_fSize' is uninitialized. Always initialise a member variable (type.6).
        

Избыточное использование указателей

В 2005 я не особо много знал об умных указателях и везде использовал new и delete.

В современном C++ мы должны избегать такого подхода. VisualStudio заботливо найдёт места, нуждающиеся в модернизации:

            g_Algorithms[ABUBBLE_SORT] = new CBubbleSortAlgorithm();
g_Algorithms[ASHAKER_SORT] = new CShakerSortAlgorithm();
        

И предупреждение:

            Warning    C26409    Avoid calling new and delete explicitly, use std::make_unique<T> instead (r.11).
        

К тому же компилятор может находить проблемы с null pointer. Например, в таком коде, я получил уведомление о непроверенном на null указателе:

            Render(CAVSystem *avSystem) {
    ColorType ct;
    avSystem->BeginDrawing(1.0, (int)m_vArray.size());
    ...
        
            Warning    C26429    Symbol 'avSystem' is never tested for nullness, it can be marked as not_null (f.23).
        

А дальше мне надо решить, добавить проверку или пометить указатель, как not_null.

Переход на nullptr

В этом нет ничего сложного, заменить все NULL из моего кода на nullptr из C++ 11 – тривиальная задача. Clang-tidy даже может сделать это автоматически.

Использование noexept

В C++ 11 мы получили спецификатор noexept для оптимизации генерируемых бинарных файлов. Естественно, в своём старом коде я этого использовать не мог и получил кучу предупреждений.

Например, для такого кода:

            void SetTempoBPS(double fTempo) { m_fTempo = fTempo; }
void SetTempoBPM(double fTempo) { m_fTempo = fTempo/60.0; }
double GetTempoBPS() { return m_fTempo; }
double GetTempoBPM() { return m_fTempo*60.0; }    
        

VisualStudio дала следующее предупреждение:

            Warning    C26440    Function 'CBeat::SetTempoBPS' can be declared 'noexcept' (f.6).
        

И да, геттеры должны быть константными...

Больше noexept

Иногда добавлением спецификатора проблему не решить. Приходится рассматривать вариант полного обновления функции. Вот что я получил:

            Warning    C26447    The function is declared 'noexcept' but calls function 'Destroy()' which may throw exceptions (f.6). 
        

Для кода:

            CGLApp::~CGLApp() {
    Destroy();
}
        

Использование override

С override похожая история. В 2005 у нас не было такого модификатора, поэтому для интерфейса, определяющего три чисто виртуальные функции:

            // в интерфейсе
virtual void Init(CViData *viData) = 0;
virtual void Step() = 0;
virtual void Stop() = 0;
        

у меня не было возможность выразить это в производном классе и приходилось писать так:

            // в производном:
void Init(CViData *viData);
void Step();
void Stop();
        

C++ 11 дала нам возможность это изменить:

            // в производном:
void Init(CViData *viData) override;
void Step() override;
void Stop() override;
        

Правило нуля

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

            Warning    C26432    If you define or delete any default operation in the type 'class CCamera', define or delete them all (c.21).   
        

Заключение

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

Конечно, вы можете обновить весь свой код, полагаясь только на свою «силу», знания и опыт, но ведь можно использовать современные инструменты и сохранить кучу времени? Visual Studio только один из многих.

А вы рефакторите свой старый код? Какие инструменты используете для этого?

Источники

РУБРИКИ В СТАТЬЕ

МЕРОПРИЯТИЯ

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

ВАКАНСИИ

Lead Python Programmer
по итогам собеседования
Unity Developer
Москва, по итогам собеседования
Ruby on Rails developer
от 140000 RUB до 200000 RUB

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

BUG