STL の関数オブジェクト | Programming Place Plus C++編【標準ライブラリ】 第25章

トップページC++編

C++編で扱っている C++ は 2003年に登場した C++03 という、とても古いバージョンのものです。C++ はその後、C++11 -> C++14 -> C++17 -> C++20 -> C++23 と更新されています。
なかでも C++11 での更新は非常に大きなものであり、これから C++ の学習を始めるのなら、C++11 よりも古いバージョンを対象にするべきではありません。特に事情がないなら、新しい C++ を学んでください。 当サイトでは、C++14 をベースにした新C++編を作成中です。

この章の概要 🔗

この章の概要です。


標準の関数オブジェクト 🔗

<functional> という標準ヘッダには、よく使うと思われるいくつかの関数オブジェクトが定義されています。

具体的には、以下のものがあります。param1、param2 はそれぞれ、operator() に渡される実引数を表しています。

名称 operator() の処理内容
plus param1 + param2
minus param1 - param2
multiplies param1 * param2
divides param1 / param2
modules param1 % param2
nagate -param1
equal_to param1 == param2
not_equal_to param1 != param2
less param1 < param2
greater param1 > param2
less_equal param1 <= param2
greater_equal param1 >= param2
logical_and param1 && param2
logical_or param1 || param2
logical_not !param1
bit_and param1 & param2
bit_or param1 | param2
bit_xor param1 ^ param2

たとえば、std::sort関数(第22章)は、デフォルトでは昇順にソートを行います。これは、要素の比較を <演算子を使って行っているいるためです。std::sort関数には、比較関数を指定できるので、ここで std::greater を渡せば、要素の比較を >演算子を使って行うようになるので、降順にソートできます。

#include <algorithm>
#include <iostream>
#include <vector>
#include <functional>

namespace {
    void Println(int elem)
    {
        std::cout << elem << std::endl;
    }
}

int main()
{
    std::vector<int> v;
    v.push_back(3);
    v.push_back(2);
    v.push_back(4);
    v.push_back(0);
    v.push_back(1);

    std::sort(v.begin(), v.end(), std::greater<int>());
    std::for_each(v.begin(), v.end(), Println);
}

実行結果:

4
3
2
1
0


関数アダプタ 🔗

C++11 になって大きく変化しているため、C++11 における関数アダプタの項を別途設けています。

関数オブジェクトは、他の関数オブジェクトや関数、値などと組み合わせて使用することが可能です。この考え方は、関数合成と呼ばれ、このテクニックを記述するために、関数アダプタという機能を使用します。関数アダプタ自体は、<functional> という標準ヘッダに定義された関数オブジェクトです。

関数アダプタには以下のものがあります。

名称 処理内容
bind1st bind1st(op, elem) op(elem, param1)
bind2nd bind2nd(op, elem) op(param1, elem)
not1 not1(op) !op(param1)
not2 not2(op) !op(param1, param2)
ptr_fun ptr_fun(op) 通常の関数として op() を呼び出す
mem_fun_ref mem_fun_ref(op) メンバ関数として op() を呼び出す
mem_fun mem_fun(op) オブジェクトのポインタが指すメンバ関数として op() を呼び出す

これだけではよく分からないので、この後1つずつ取り上げていきますが、その前に、関数アダプタを使用する場合に必要な前提について説明しておきます。

関数アダプタは、その実装の都合上、いくつかの型定義を必要としています。つまるところ、関数オブジェクトにするクラスが「公開」された typedef を、標準規格で決められた名称で定義していれば良いのですが、これを簡潔に行うために、2つの構造体が用意されています。この構造体を公開継承(【言語解説】第26章)するだけで準備が整うように設計されています。

具体的には、std::unary_function構造体std::binary_function構造体の2つです。これらは、関数オブジェクトの operator() の引数が1つなのか、2つなのかで使い分けます。

C++11 では、std::unary_function、std::binary_function の使用は非推奨となっています。

unary_function、binary_function は、以下のように定義されています。

namespace std {
    template <typename Arg, typename Result>
    struct unary_function {
        typedef Arg argument_type;
        typedef Result result_type;
    };

    template <typename Arg1, typename Arg2, typename Result>
    struct binary_function {
        typedef Arg1 first_argument_type;
        typedef Arg2 second_argument_type;
        typedef Result result_type;
    };
}

いずれも、テンプレート構造体になっています。テンプレート仮引数には、引数と戻り値の型を指定します。その結果、テンプレート仮引数に当てはめた型に応じた typedef名が定義されます。

これを実際に使うと、次のようになります。

class EvenAndGreaterThanN : public std::unary_function<int, bool> {
public:
    EvenAndGreaterThanN(argument_type N) : mN(N)
    {}

    result_type operator()(argument_type elem) const
    {
        return (elem >= mN) && (elem % 2 == 0);
    }

private:
    const argument_type mN;
};

class WithinRange : public std::binary_function<int, int, bool> {
public:
    WithinRange(first_argument_type min, second_argument_type max) : mMin(min), mMax(max)
    {}

    result_type operator()(first_argument_type elem1, second_argument_type elem2) const
    {
        return mMin <= elem1 && elem2 <= mMax;
    }

private:
    const first_argument_type  mMin;
    const second_argument_type mMax;
};

このようにクラスを定義しておけば、ここから作った関数オブジェクトを関数アダプタに渡すことができます。

bind1st、bind2nd 🔗

bind1st関数bind2nd関数 は、関数オブジェクトに渡す引数の個数を2つから1つへ減らすことが目的の関数アダプタです。

たとえば、std::replace_if関数(第20章)に渡す関数オブジェクトは、引数が1つだけの単項叙述関数です。ここで、置換対象の条件を「要素の値が 0以下」としたいとき、冒頭で紹介した標準の関数オブジェクトの中から「equal_less」を使いたいところですが、equal_less の処理内容は「param1 <= param2」であり、引数が2つあります。そのため、以下のように使おうとすると、コンパイルエラーになります。

std::replace_if(v.begin(), v.end(), std::less_equal<int>(), 1);

コンパイルエラーになることを別としても、そもそも、「0以下」の「0」の部分を記述する場所がないことが分かります。ここで、bind2nd を使用すると、問題を解決できます。

#include <algorithm>
#include <iostream>
#include <vector>
#include <functional>

namespace {
    void Println(int elem)
    {
        std::cout << elem << std::endl;
    }
}

int main()
{
    std::vector<int> v;
    v.push_back(-2);
    v.push_back(-1);
    v.push_back(0);
    v.push_back(1);
    v.push_back(2);

    std::replace_if(v.begin(), v.end(), std::bind2nd(std::less_equal<int>(), 0), 1);
    std::for_each(v.begin(), v.end(), Println);
}

実行結果:

1
1
1
1
2

bind2nd(bind1st も)の第1引数には、引数が2個の関数オブジェクトを指定します。第2引数には、bind2nd の場合は、関数オブジェクトの2個目の引数として渡す値を指定します。(bind1st の場合は、1個目の引数として渡す値を指定します)。

つまり、本来なら引数が1つの関数オブジェクトでなければ受け取ってもらえない箇所(今回の例では replace_if関数に渡す関数オブジェクト)に、引数が2つの関数オブジェクトを渡せるようにする仕組みという訳です。

not1、not2 🔗

not1関数not2関数 は、関数オブジェクトが返す結果を反転させます。違いは、関数オブジェクトに渡す引数の個数で、not1 は1つ、not2 は2つの場合に対応しています。

たとえば以下のサンプルプログラムでは、「4以上の偶数」という条件を not1 で反転させて使用しています。

#include <algorithm>
#include <iostream>
#include <vector>
#include <functional>

namespace {
    class EvenAndGreaterThanN : public std::unary_function<int, bool> {
    public:
        EvenAndGreaterThanN(argument_type N) : mN(N)
        {}

        result_type operator()(argument_type elem) const
        {
            return (elem >= mN) && (elem % 2 == 0);
        }

    private:
        const argument_type mN;
    };
}

int main()
{
    std::vector<int> v;
    v.push_back(2);
    v.push_back(3);
    v.push_back(4);
    v.push_back(5);
    v.push_back(6);

    std::cout << std::count_if(v.begin(), v.end(), std::not1(EvenAndGreaterThanN(4))) << std::endl;
}

実行結果:

3

ptr_fun 🔗

ptr_fun関数は、通常の関数を、関数アダプタから使用できるようにするための関数アダプタです。通常の関数は、関数オブジェクトとは違って、std::unary_function や std::binary_function から派生させられないので、その代わりの手段として用意されたものです。

たとえば、次のようなコードはコンパイルエラーになってしまいます。

namespace {
    bool IsEven(int elem)
    {
        return elem % 2 == 0;
    }
}

std::count_if(v.begin(), v.end(), std::not1(IsEven));

これは、std::not1関数がその実装の都合上、typedef された型名を必要としているためです。当然、単なる関数である IsEven は、その要求に対応できません。ptr_fun関数を経由させれば、問題を解決できます。

#include <algorithm>
#include <iostream>
#include <vector>
#include <functional>

namespace {
    bool IsEven(int elem)
    {
        return elem % 2 == 0;
    }
}

int main()
{
    std::vector<int> v;
    v.push_back(2);
    v.push_back(3);
    v.push_back(4);
    v.push_back(5);
    v.push_back(6);

    std::cout << std::count_if(v.begin(), v.end(), std::not1(std::ptr_fun(IsEven))) << std::endl;
}

実行結果:

2

mem_fun_ref、mem_fun 🔗

mem_fun_ref関数mem_fun関数は、オブジェクトのメンバ関数や、オブジェクトを指すポインタからメンバ関数を呼び出すための関数アダプタです。

#include <algorithm>
#include <iostream>
#include <vector>
#include <string>
#include <functional>

namespace {
    class Name {
    public:
        Name(const char* s) : mStr(s)
        {}

        void Print() const
        {
            std::cout << mStr << std::endl;
        }

    private:
        std::string  mStr;
    };
}

int main()
{
    std::vector<Name> v1;
    v1.push_back("xx");
    v1.push_back("xxxxx");
    v1.push_back("xxx");

    std::vector<Name*> v2;
    v2.push_back(&v1[0]);
    v2.push_back(&v1[1]);
    v2.push_back(&v1[2]);

    std::for_each(v1.begin(), v1.end(), std::mem_fun_ref(&Name::Print));
    std::for_each(v2.begin(), v2.end(), std::mem_fun(&Name::Print));
}

実行結果:

xx
xxxxx
xxx
xx
xxxxx
xxx

C++11 (関数アダプタ) 🔗

C++11 (bind) 🔗

C++11 では、std::bind1st、std::bind2nd が非推奨となり、代わる手段として、std::bind が追加されました。

#include <algorithm>
#include <iostream>
#include <vector>
#include <functional>

using namespace std::placeholders;

int main()
{
    std::vector<int> v = { -2, -1, 0, 1, 2 };

    std::replace_if(std::begin(v), std::end(v), std::bind(std::less_equal<int>(), _1, 0), 1);
    std::for_each(std::begin(v), std::end(v), [](int elem) {std::cout << elem << std::endl; });
}

実行結果:

1
1
1
1
2

std::bind は、第1引数に関数や関数オブジェクトを指定し、第2引数以降に引数を指定します。可変個引数になっているので、引数は任意の個数を連続して記述できます。

特定の値にバインドしない引数については、std::placeholders名前空間で定義されている _1、_2、_3・・・といった値を指定します。完全名だと「std::placeholders::_1」のようになりますが、さすがに長いので usingディレクティブを用いています。これらの特別な値は、プレースホルダ(置き場)と呼ばれており、std::bind が返した関数オブジェクトに渡す引数を表しています。

初見ではかなり分かりづらいと思うので、簡単な例を挙げます。

#include <algorithm>
#include <iostream>
#include <functional>

using namespace std::placeholders;

namespace {
    void func(int a, int b)
    {
        std::cout << a << ", " << b << std::endl;
    }
}

int main()
{
    auto f1 = std::bind(func, _1, _2);
    f1(3, 5);

    auto f2 = std::bind(func, _2, _1);
    f2(3, 5);

    auto f3 = std::bind(func, _1, 9);
    f3(3);
}

実行結果:

3, 5
5, 3
3, 9

std::bind が返すものは平たく言えば関数オブジェクトですが、実際には std::function というクラステンプレートから作られたクラスオブジェクトです。std::function については後の項で紹介しますが、何にせよ、変数で受け取るならば auto を使います。

std::bind が返した関数オブジェクトを呼び出すときに渡した第1引数が _1 のところに、第2引数が _2 のところに来ます。プレースホルダを指定せず、直接「9」という値を指定した f3 の例では、第2引数を省略していますが、ちゃんと 9 が出力されていることが分かると思います。

C++11 (mem_fn) 🔗

C++11 では、std::mem_fun_ref、std::mem_fun が非推奨となり、代わる手段として、std::mem_fn が追加されました。

#include <algorithm>
#include <iostream>
#include <vector>
#include <string>
#include <functional>

namespace {
    class Name {
    public:
        Name(const char* s) : mStr(s)
        {}

        void Print() const
        {
            std::cout << mStr << std::endl;
        }

    private:
        std::string  mStr;
    };
}

int main()
{
    std::vector<Name> v1 = { "xx", "xxxxx", "xxx" };
    std::vector<Name*> v2 = { &v1[0], &v1[1], &v1[2] };

    std::for_each(std::begin(v1), std::end(v1), std::mem_fn(&Name::Print));
    std::for_each(std::begin(v2), std::end(v2), std::mem_fn(&Name::Print));
}

実行結果:

xx
xxxxx
xxx
xx
xxxxx
xxx

このように、std::mem_fun_ref と std::mem_fun の使い分けは不要になりました。ただ、実際のところ、ラムダ式(【言語解説】第34章)を使って実現できることが多いと思われます。たとえば以下のように書き換えても同じ結果を得られます。

int main()
{
    std::vector<Name> v1 = { "xx", "xxxxx", "xxx" };
    std::vector<Name*> v2 = { &v1[0], &v1[1], &v1[2] };

    std::for_each(std::begin(v1), std::end(v1), [](const Name& n) {n.Print(); });
    std::for_each(std::begin(v2), std::end(v2), [](const Name* n) {n->Print(); });
}



練習問題 🔗

問題① 引数が偶数かどうか判定する以下のような IsEven関数があるとして、これを使って、奇数の要素だけを remove_if関数を使って取り除く処理を作成してください。

namespace {
    bool IsEven(int elem)
    {
        return elem % 2 == 0;
    }
}


解答ページはこちら

参考リンク 🔗


更新履歴 🔗

 コンパイラの対応状況について、対応している場合は明記しない方針にした。

 clang 3.7 (Xcode 7.3) を、Xcode 8.3.3 に置き換え。

 VisualC++ 2017 に対応。

 新規作成。



前の章へ (第24章 数値演算の STLアルゴリズム)

次の章へ (第26章 逆イテレータと挿入イテレータ)

C++編のトップページへ

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る