Информация

Дата основания
Местоположение
Россия
Сайт
piter.com
Численность
201–500 человек
Дата регистрации

Блог на Хабре

Обновить

Как легко и просто модернизировать код на C++

Блог компании Издательский дом «Питер»ПрограммированиеC++Проектирование и рефакторинг
Автор оригинала: CppDepend Team
Привет, Хабр!

Предлагаем вашему вниманию перевод короткой практичной статьи по борьбе с избыточным legacy в коде на C++. Надеемся, будет интересно.

В последнее время в сообществе C++ активно продвигается использование новых стандартов и модернизация имеющейся базы кода. Однако, еще даже до выхода стандарта C++11 известные эксперты по C++, в частности, Андре Александреску, Скотт Майерс и Герб Саттер пропагандировали обобщенное программирование на C++, которое квалифицировали как «современный дизайн C++». Вот как высказался об этом Андре Александреску:

Современный дизайн C++ определяет и систематически использует обобщенные компоненты – очень гибкие артефакты для проектирования, которые можно смешивать и сочетать для получения насыщенных вариантов поведения в небольшом ортогональном фрагменте кода.

В этом тезисе интересны три утверждения:

  • Современный дизайн C++ определяет и систематически использует обобщенные компоненты.
  • Очень гибкий дизайн.
  • Получение насыщенных вариантов поведения при помощи небольшого, ортогонального фрагмента кода.

Модернизация кода, написанного на C++, не ограничивается внедрением новых стандартов, но и предполагает использование наилучших практик, применимых в любом языке программирования и помогающих улучшить базу кода. Для начала давайте обсудим некоторые простые шаги, позволяющие вручную модернизировать базу кода. В третьем разделе поговорим об автоматической модернизации кода.

Вручную модернизируем исходный код


Возьмем для примера алгоритм и попробуем его модернизировать. Алгоритмы применяются для расчетов, обработки данных и автоматического получения выводов. Программирование алгоритма – порой нетривиальная задача и зависит от его сложности. В C++ прилагаются значительные усилия для упрощения реализации и повышения мощности алгоритмов.
Давайте попробуем модернизировать эту реализацию алгоритма быстрой сортировки:

// Функция разделения
int partition(int* input,int p,int r){
        int pivot = input[r];
        while( p < r ){
                 while( input[p]< pivot )
                     p++;
                 while( input[r]> pivot )
                    r--;
                if( input[p]== input[r])
                    p++;
                elseif( p < r ){
                     int tmp = input[p];
                     input[p]= input[r];
                     input[r]= tmp;
                }
        }
         return r;
}
// Рекурсивная функция быстрой сортировки
void quicksort(int* input,int p,int r){
        if( p < r ){
              int j = partition(input, p, r);        
              quicksort(input, p, j-1);
              quicksort(input, j+1, r);
        }
}

В конце концов, у всех алгоритмов есть определенные общие черты:

  • Использование контейнера для элементов определенного вида и их перебор.
  • Сравнение элементов
  • Некоторые операции над элементами

В нашей реализации контейнером является необработанный массив целых чисел, и мы перебираем операции увеличения и уменьшения на единицу. Сравнение выполняется при помощи “<” и “>”, а также мы совершаем над данными некоторые операции, например, меняем их местами.

Давайте попробуем улучшить каждый из этих признаков алгоритма:

Шаг 1: Меняем контейнеры на итераторы


Если мы откажемся от обобщенных контейнеров, то будем вынуждены пользоваться лишь элементами определенного типа. Для применения того же алгоритма к другим типам нам придется копировать и вставлять код. Обобщенные контейнеры решают эту проблему и позволяют использовать элемент любого вида. Например, в алгоритме быстрой сортировки можно применить std::vector<T> в качестве контейнера вместо необработанного массива.

Необработанный массив или std::vector – это всего лишь одна возможность из разнообразных вариантов, позволяющих представить множество элементов. Тот же алгоритм применим и к связному списку, и к очереди, и к любому другому контейнеру. При работе с итератором лучше всего абстрагировать используемый контейнер.

Итератор – это любой объект, который, указывая на элемент в некотором диапазоне, может перебрать все элементы данного диапазона при помощи набора операторов (в который входят, как минимум, оператор увеличения на единицу (++) и оператор разыменования (*)). Итераторы подразделяются на пять категорий в зависимости от реализуемой ими функции: Ввод, Вывод, Однонаправленный итератор, Двунаправленный итератор и случайный доступ.

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

template< typename BidirectionalIterator >
void quick_sort( BidirectionalIterator first, BidirectionalIterator last )

Шаг 2: Делаем компаратор обобщенным, если это возможно


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

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

Воспользовавшись обобщенным компаратором, можно модифицировать определение, вот так:

template< typename BidirectionalIterator, typename Compare >
void quick_sort( BidirectionalIterator first, BidirectionalIterator last, Compare cmp )

Этап 3: Заменяем имеющиеся операции стандартными


В большинстве алгоритмов используются повторяющиеся операции, например, min, max и swap. При выполнении таких операций лучше не изобретать велосипед и использовать стандартную реализацию, существующую в заголовке <algorithm>.

В нашем случае можно использовать метод swap из стандартной библиотеки STL, а не создавать наш собственный метод.

std::iter_swap( pivot, left );

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

#include <functional>
#include <algorithm>
#include <iterator>
 
template< typename BidirectionalIterator, typename Compare >
void quick_sort( BidirectionalIterator first, BidirectionalIterator last, Compare cmp ) {
    if( first != last ) {
        BidirectionalIterator left  = first;
        BidirectionalIterator right = last;
        BidirectionalIterator pivot = left++;
 
        while( left != right ) {
            if( cmp( *left, *pivot ) ) {
                ++left;
            } else {
                while( (left != right) && cmp( *pivot, *right ) )
                    --right;
                std::iter_swap( left, right );
            }
        }
 
        --left;
        std::iter_swap( pivot, left );
 
        quick_sort( first, left, cmp );
        quick_sort( right, last, cmp );
    }
}
 
template< typename BidirectionalIterator >
    inline void quick_sort( BidirectionalIterator first, BidirectionalIterator last ) {
        quick_sort( first, last,
                std::less_equal< typename std::iterator_traits< BidirectionalIterator >::value_type >()
                );
    }

У данной реализации есть следующие достоинства:

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

Автоматическая модернизация


Интересно автоматически выявлять места, где можно использовать определенные возможности C++11/C++14/C++17 и, если условия благоприятствуют, автоматически менять код. Для таких целей существует полнофункциональный инструмент clang-tidy, используемый для автоматического преобразования кода C++, написанного в соответствии со старыми стандартами. После такого преобразования в коде, там, где это уместно, используются возможности из более новых стандартов.

Вот некоторые участки, на которых clang-tidy предлагает модернизировать код:

  • Переопределение: найдите места, где можно добавить указатель переопределения для функции экземпляра, переопределяющей виртуальную функцию в базовом классе, при этом еще не имеющей такого указателя
  • Преобразование циклов: найдите циклы вида for(…; …; …), чтобы заменить их новыми циклами на основе диапазона, в которых можно указать начало и конец области для перебора, а далее пользоваться новым выражением для этой цели.
  • Передача по значению: найдите параметры const-ref, которым пошла бы на пользу идиома передачи по значению.
  • auto_ptr: находите в коде выходящие из употребления std::auto_ptr и заменяйте их std::unique_ptr.
  • Авто-указатель: находите места, где можно использовать указатель типа auto в объявлениях переменных.
  • nullptr: находите нулевые литералы, чтобы заменять их nullptr там, где это уместно.
  • std::bind: такая проверка позволяет найти случаи использования std::bind и заменить простые случаи такого рода лямбдами, там, где это уместно. Там, где требуется, лямбды будут использовать захват значения.
  • Устаревшие заголовки: Некоторые заголовки из C были выведены из употребления в C++ и больше не приветствуются в базах кода на этом языке. Некоторые не действуют в C++. Подробнее об этом рассказано в соответствующем разделе стандарта C++ 14 [depr.c.headers].
  • std::shared_ptr: такая проверка позволяет выявить случаи создания объектов std::shared_ptr путем явного вызова конструктора и с выражением new, после чего заменяет их вызовом std::make_shared.
  • std::unique_ptr: такая проверка позволяет выявить случаи создания объектов std::shared_ptr путем явного вызова конструктора и с выражением new, после чего заменяет их вызовом std::make_unique, появившимся в C++14.
  • Литералы неформатированной строки: такая проверка выборочно заменяет строковые литералы, содержащие экранированные символы, на литералы неформатированной строки.

Разработчики, освоившие Clang, легко научатся обращаться и с инструментом clang-tidy. Но при работе с Visual C++, а также с другими компиляторами можно использовать CppDepend, в состав которого входит clang-tidy.
Теги:C++clangрефакторингlegacy-кодпрограммирование
Хабы: Блог компании Издательский дом «Питер» Программирование C++ Проектирование и рефакторинг
Рейтинг +6
Количество просмотров 4,1k Добавить в закладки 45
Комментарии
Комментарии 12

Похожие публикации

Лучшие публикации за сутки