CodeZine(コードジン)

特集ページ一覧

ラムダ式でステップアップ! C++のプログラムから汎用的なアルゴリズムを切り出し利用してみよう

  • LINEで送る
  • このエントリーをはてなブックマークに追加
2014/09/25 14:00

目次

4. 汎用的なアルゴリズムの分離

 さて、前述のプログラムをもう一度よく眺めてみましょう。 互いに独立な2つの関数をRun関数から分離できたように見えます。

 ですが、この2つの関数は、次のようなものに依存しているので、汎用的なアルゴリズムを分離したとは言い難いのです。

  1. コレクションの要素の型に依存している
  2. コレクションの構造(リストか配列かなど)に依存している
  3. 抽出条件が何であるかに依存している
  4. それぞれの要素をどうするかに依存している

 これでは、この2つの関数は、この文脈でしか使うことができません。即ち、「抽出(Filter)」は、書籍のリストの中から、「書籍のタイトルまたは出版社の名前に "リ" を含むもの」を抽出するのにしか使えませんし、「表示(Show)」は、書籍リストのそれぞれの書籍を表示するのにしか使うことができません。

 もっと汎用的なアルゴリズムを切り出すことはできないでしょうか? もっと汎用的に抽出のアルゴリズムや繰り返し処理のアルゴリズムは書けないものでしょうか?

 もし仮に、C++でそれができないとすると、C++には汎用的なアルゴリズムの記述能力がないことになりますが、もちろんそんなことはありません。

 これらの依存を関数から分離して、汎用的なアルゴリズムにするには、例えば次のようにすれば良いのです。

依存の内容とそれに対する戦略
番号 依存の内容 戦略
1 コレクションの要素の型に依存している ジェネリックプログラミング(C++ではtemplateの使用)
2 コレクションの構造(リストか配列かなど)に依存している ジェネリックプログラミング(C++ではtemplateの使用)とイテレーターパターン
3 抽出条件が何であるかに依存している 抽出条件を関数で表し、それを渡す
4 それぞれの要素をどうするかに依存している どうするかを関数で表し、それを渡す

 これらを使って、2つの関数を次のような汎用的なアルゴリズムにしてみましょう。

2つの関数のbefore/after
before(様々なものに依存した処理) after(汎用的なアルゴリズム)
書籍のリストの中から、「書籍のタイトルまたは出版社の名前に "リ" を含むもの」を抽出する コレクションから或る条件に合致した要素だけを抽出する
書籍リストのそれぞれの書籍を表示する コレクションのそれぞれの要素に対して特定のことをする

 実際にやってみると、例えば次のようになります。

「コレクションから或る条件に合致した要素だけを抽出する」AppendIf 関数と、「コレクションのそれぞれの要素に対して特定のことをする」ForEach 関数

MyCollection.h
#pragma once

namespace MyCollection {
    // コレクションから或る条件に合致した要素だけを抽出する
    template<class IteraterType, class FunctionType, class DestinationType>
    inline void AppendIf(
        IteraterType     first      , // コレクションの先頭を指すイテレーター
        IteraterType     last       , // コレクションの末尾の次を指すイテレーター
        DestinationType& destination, // 抽出した要素の追加先のコレクション
        FunctionType     isMatch      // 抽出条件を表した関数
    )
    {
        for (; first != last; ++first) {
            if (isMatch(*first))
                destination.push_back(*first);
        }
    }

    // コレクションから或る条件に合致した要素だけを抽出する
    template<class SourceType, class FunctionType, class DestinationType>
    inline void AppendIf(
        SourceType       source     , // コレクション
        DestinationType& destination, // 抽出した要素の追加先のコレクション
        FunctionType     isMatch      // 抽出条件を表した関数
    )
    { AppendIf(source.begin(), source.end(), destination, isMatch); }

    // コレクションのそれぞれの要素に対して特定のことをする
    template<class IteraterType, class FunctionType>
    inline void ForEach(
        IteraterType first , // コレクションの先頭を指すイテレーター
        IteraterType last  , // コレクションの末尾の次を指すイテレーター
        FunctionType action  // それぞれの要素に行うことを表した関数
    )
    {
        for (; first != last; ++first)
            action(*first);
    }

    // コレクションのそれぞれの要素に対して特定のことをする
    template<class CollectionType, class FunctionType>
    inline void ForEach(
        CollectionType collection, // コレクション
        FunctionType   action      // それぞれの要素に行うことを表した関数
    )
    { ForEach(collection.begin(), collection.end(), action); }
}

関心事の分離

 これらの関数は、ほぼそのアルゴリズムだけを記述しています。アルゴリズムで必要な部分だけを記述し、アルゴリズムではない部分をあまり記述していません。

 そのアルゴリズムという関心事を高凝集に分離したことになります。

 また、他への依存も少ないです。要素の型やコレクションの種類、抽出条件や各要素に対してやることを、それほど選びません。

 そのため、これらのアルゴリズムを様々なプログラムから利用できるはずです。

5. 汎用的なアルゴリズムの試用

 では、これらを元のプログラムで使ってみる前に、これらのC++の関数として書かれたアルゴリズムが本当に汎用的に使えるかどうか試してみましょう。

intのlistの中から偶数だけをコンソールに表示する

 まず、intのlistの中から偶数を抽出し、コンソールに表示してみます。

要素の型 int
コレクション list
抽出条件 IsEven(偶数であること)
各要素に対してやること Show(コンソールに表示)
Program.cpp
#include <iostream>
#include <list>
#include <string>
using namespace std;

#include "MyCollection.h"
using namespace MyCollection;

class Program
{
public:
    void Run()
    {
        list<int> collection = { 1, 4, 9, 16, 25 }; // int の list
        list<int> selectedCollection;

        // 汎用的なアルゴリズムを利用
        AppendIf(collection, selectedCollection, IsEven); // 偶数だけを抽出
        ForEach(selectedCollection, Show<int>); // 抽出されたそれぞれの int を表示
    }

private:
    static bool IsEven(int number)
    { return number % 2 == 0; }

    template <class T>
    static void Show(T item)
    { wcout << item << endl; }
};

int main()
{
    Program().Run();
    return 0;
}

 実行結果は、次の通りです。うまくいきました。

実行結果
4
16

double  の配列の中から正数だけを表示する

 コレクションの構造や要素の型、抽出条件を変えてみましょう。

要素の型 double
コレクション 配列
抽出条件 IsPositive(正数であること)
各要素に対してやること Show(コンソールに表示)
Program.cpp
#include <iostream>
#include <vector>
#include <string>
using namespace std;

#include "MyCollection.h"
using namespace MyCollection;

class Program
{
public:
    void Run()
    {
        // double の配列
        double         array [] = { -3.0, 0.1, -0.04, 0.001, -0.0005, 0.00009 };
        vector<double> selectedCollection;

        // 汎用的なアルゴリズムを利用
        AppendIf(array, array + sizeof(array) / sizeof(array[0]),
                 selectedCollection, IsPositive);  // 正数だけを抽出
        ForEach(selectedCollection, Show<double>); // 抽出されたそれぞれの double を表示
    }

private:
    static bool IsPositive(double number)
    { return number > 0; }

    template <class T>
    static void Show(T item)
    { wcout << item << endl; }
};

int main()
{
    Program().Run();
    return 0;
}

 今度の実行結果は、次の通りです。こちらもうまくいくようです。

実行結果
0.1
0.001
9e-005

 ここまでは、汎用的なアルゴリズムとして利用できているように見えます。


  • LINEで送る
  • このエントリーをはてなブックマークに追加

修正履歴

  • 2014/10/14 16:20 姉妹記事「ラムダ式でステップアップ! C#のプログラムから汎用的なアルゴリズムを切り出すことで、LINQについての理解を深めよう」の紹介を第一節に挿入しました。

  • 2014/09/18 16:20 第8章のソースコード中の「// Sample.cpp」という不要な行を削除。

著者プロフィール

  • 小島 富治雄(フジヲ)

    Microsoft MVP for C# (2005.07~)。 - 注目の MVP - Blog: プログラミング C# - 翔ソフトウェア (Sho's) - Web Site: 翔ソフトウェア (Sho's) - Twitter: Fujiwo...

あなたにオススメ

All contents copyright © 2005-2021 Shoeisha Co., Ltd. All rights reserved. ver.1.5