ブログ 7 分で読める

C++コードをモダナイズする簡単なステップ

Share this article
C++コードをモダナイズする簡単なステップ

最近、C++コミュニティは、既存のコードベースをモダナイズするために新しい標準の使用を推進しています。しかし、C++11標準がリリースされる以前から、Andrei Alexandrescu、Scott Meyers、Herb Sutterなどの有名なC++エキスパートは、Modern C++ Designという旗印のもと、ジェネリックプログラミングを推進していました。Andrei AlexandrescuがModern C++ Designについて述べていることは次のとおりです:

Modern C++ Designでは、定義され、体系的に使用される ジェネリックコンポーネント — 小さく直交したコード本体で豊かな振る舞いを実現するために、組み合わせ可能な、高度に柔軟な設計要素。

彼の発言では、次の3点が際立っています:

  • Modern C++ Designでは、定義され、 体系的に使用される ジェネリックコンポーネント
  • 高度に 柔軟な 設計。
  • Obtain rich behaviors with a 小さく直交した コード本体で、豊かな振る舞いを実現する。

C++コードのモダナイズは、新しい標準を使うことだけではありません。コードベースを改善するために、ジェネリックプログラミングのベストプラクティスをいくつか使うこともできます。まず、コードベースを手動でモダナイズする簡単な手順を見て、第2セクションでは自動的にモダナイズする方法を探りましょう。

I. ソースコードを手動でモダナイズする

アルゴリズムを例に、そのモダナイズを試してみましょう。アルゴリズムは、計算、データ処理、自動推論に使用されます。それらをプログラミングするのは必ずしも容易ではなく、その複雑さに依存します。C++では、実装を簡素化し、より強力にするために大きな努力が払われてきました。

このクイックソートアルゴリズムの実装をモダナイズしてみましょう:

// The partition function
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;
}
// The quicksort recursive function
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);
        }
}

高いレベルで見ると、アルゴリズムには次のような共通の特徴があります:

  • 特定の要素型のコンテナを使用し、それらを反復処理する。
  • 要素間で比較する。
  • そしてもちろん、要素に対して何らかの処理を行う。

この実装では、コンテナはintの生配列で、インクリメントとデクリメントによって反復処理しています。比較には「<」と「>」を使用し、データの交換などの操作もあります。

これらの各側面を改善してみましょう:

手順1:コンテナをイテレータへ置き換える

非ジェネリックなコンテナを使用すると、特定の要素型を使わざるを得ません。同じアルゴリズムを他の型へ適用するには、コードをコピー&ペーストする必要があります。ジェネリックコンテナは、任意の要素型を使用できるようにすることで、この問題を解決します。たとえば、クイックソートアルゴリズムでは、生配列の代わりにstd::vector<T>をコンテナとして使用できます。

生配列やstd::vectorは、要素の集合を表す多くの方法の1つにすぎません。同じアルゴリズムを連結リスト、キュー、その他の任意のコンテナへ適用することもできます。この目的には、基礎となるコンテナを抽象化するイテレータが最適です。

イテレータは、範囲内の要素を指し、その範囲の要素を一連の演算子(少なくともインクリメント(++)演算子と間接参照(*)演算子)を使って走査できるオブジェクトです。イテレータは、実装している機能に応じて、入力、出力、前方向、双方向、ランダムアクセスの5つのカテゴリに分類されます。

このアルゴリズムでは、どの種類のイテレータを使用するかを指定する必要があります。そのためには、どの反復操作が使われているかを特定します。クイックソートアルゴリズムでは、インクリメントとデクリメントが適用されます。したがって、双方向イテレータが必要です。イテレータを使用すると、メソッドを次のように定義できます:

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>ヘッダーの標準実装を使用する方が適しています。

ここでは、独自の専用メソッドを作成する代わりに、STLのswapメソッドを使用できます。

std::iter_swap( pivot, left );

これら3つの手順後の変更結果は次のとおりです:

#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 >()
                );
    }

この実装には、次の利点があります:

  • 多くの要素型に適用できる。
  • コンテナは、vector、set、list、または双方向イテレータを持つ任意のコンテナにできる。
  • 十分に最適化・テストされた標準関数を使用している。

II. 自動モダナイズ

C++11/C++14/C++17の機能を使用できる場所を自動的に検出し、可能であればコードを自動的に変更できると便利です。このようなニーズには、 clang-tidy が適しています。clang-tidyは、古い標準向けに書かれたC++コードを、適切な場所で最新のC++標準の機能を使うように自動変換するスタンドアロンツールです。

clang-tidyがコードをモダナイズする機会を検出できる例は次のとおりです:

  • Override:基底クラスの仮想関数をオーバーライドしており、まだoverride指定子が付いていないメンバー関数に、override指定子を追加できる場所を検出する。
  • Loop Convert:for(…; …; …)のようなループを検出し、C++11の新しい範囲ベースループへ置き換え、対応する範囲ベースループ式を提供する。
  • Pass-By-Value:値渡しイディオムを使用する利点があるconst参照パラメータを検出する。
  • auto_ptr:非推奨のstd::auto_ptrの使用を検出し、std::unique_ptrへ置き換える。
  • auto指定子:変数宣言でauto型指定子を使用できる場所を検出する。
  • nullptr:該当する場合にnullptrへ置き換えるべきnullリテラルを検出する。
  • std::bind:このチェックは std::bind の使用を検出し、単純な使用をラムダへ置き換えます。必要な場合、ラムダは値キャプチャを使用します。
  • Deprecated headers:Cライブラリの一部のヘッダーはC++で非推奨となり、C++コードベースでは推奨されなくなりました。一部はC++では効果がありません。詳細については、C++14標準の[depr.c.headers]セクションを参照してください。
  • std::shared_ptr:このチェックは、 std::shared_ptr オブジェクトが、コンストラクタと new 式を明示的に呼び出して作成されているのを検出し、C++14で導入された std::make_shared
  • std::unique_ptr:このチェックは、 std::unique_ptr オブジェクトが、コンストラクタと new 式を明示的に呼び出して作成されているのを検出し、C++14で導入された std::make_uniqueへの呼び出しへ置き換えます。
  • raw string literals:このチェックは、エスケープ文字を含む文字列リテラルを選択的にraw文字列リテラルへ置き換えます。

Clangを使用している開発者は、clang-tidyを簡単に活用できます。Visual C++開発者や他のコンパイラのユーザーは、clang-tidyを統合している CppDependを使用できます。

Share this article