関数ポインタとラムダ式 | Programming Place Plus 新C++編

トップページ新C++編

先頭へ戻る

このページの概要

このページでは、関数に関数を渡しておくことによって、適切なタイミングで、関数内から関数を呼び返す(コールバック)方法を取り上げます。その実現のために、オブジェクトではなく関数を指し示す、関数ポインタという機能を紹介します。また、もう1つの手段として使えるラムダ式について、これまでのページよりも詳しい解説を行います。

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



コールバック関数

スコープと名前空間」のページの練習問題では、コマンドライン引数の解析をおこなうコードを、バイナリエディタ以外のプログラムでも使えるようにする目的で、いったんソースコードの分離だけ行いました。次にしなければならないのは、コマンドライン引数の中に、有効なオプション指定を発見したとき、それに応じた処理を実行できるようにすることです。しかし、その処理をコマンドライン引数を解析する関数の中に書くと、ほかのプログラムで再利用できないことになります。このページでは、この問題を解決する方法を取り上げます。

やりたいことはこうです。

// コマンドライン引数を解析する
void analysis_cmdline_args(const std::vector<std::string>& cmdline_args, ????)
{
    // cmdline_args から、有効なオプション指定を探す。
    // 見つかったら、それに応じた処理を実行する。
}

すでにバイナリエディタには –lowerオプションが実装されていますが、これはバイナリエディタでは意味があっても、ほかのプログラムでは意味がないかもしれません。反対に、ほかのプログラムでは必要なオプションが、バイナリエディタのプログラムにはないかもしれません。あったとしても、おそらく実行したい処理は違うものでしょう。そのため、「どんなオプションが存在するのか」という情報と、「各オプションで実行すべき処理」は、呼び出し側に書かなければなりません。

そこで、オプション名とそれに対応する関数のリストを用意して渡せるようにします。

struct Option {
    std::string  name;  // オプション名
    ????         func;  // 発見時に実行する関数
};
const std::vector<Option> options { /* オプション名と、対応する関数のリスト */};

// コマンドライン引数を解析する
void analysis_cmdline_args(const std::vector<std::string>& cmdline_args, const std::vector<Option> options)
{
    // cmdline_args に options の name に一致する指定があるか探す。
    // 見つかったら、それに応じた func を実行する。
}

analysis_cmdline_args(cmdline_args, options);

func を関数にしたいわけですが、その型をどう書けばいいのかが問題です。これができれば、オプションの指定を発見したときに、その関数を呼び出してやればいいことになります。「適切なタイミングが来たとき、事前に渡しておいた関数を呼び出してもらう」ということです。このような処理はコールバック (callback) といい、呼び出される関数をコールバック関数 (callback function) といいます。ここでは func がコールバック関数です。

実際のところ、C++ では関数型 (function type) の変数やデータメンバ、仮引数を作ることはできません。しかし、代わりにとれる手段はあります。このページではその手段をいくつか紹介します。

関数ポインタ

まずは、関数ポインタ (function pointer)(あるいは、関数へのポインタ (pointer to function))です。これまでのページで使ってきたポインタは、メモリ上のオブジェクトを指し示すものでしたが、関数ポインタはメモリ上の関数を指し示します。

「メモリ上の関数」というのが不思議な感じがしたかもしれません。実は、プログラムの実行を開始するとき、関数の本体(のコードをコンパイルしたもの)がメモリ上に展開されています。そのため、関数には関数ごとに固有のメモリアドレスがあります。関数ポインタはこれを記憶することで、任意の関数を指し示します。

通常、単に「ポインタ」といったときは、オブジェクトを指し示すポインタを意味しますが、明確にするため、オブジェクトポインタ (object pointer) と呼ぶ場合があります。void* で表されるポインタもオブジェクトポインタに含みます1

関数ポインタの変数は、次のように宣言します。

戻り値の型 (*変数名)(仮引数の並び) 初期化子;
戻り値の型 (*変数名)(仮引数の並び);

指し示したい関数の仮引数と戻り値に一致するように宣言しなければなりません。「仮引数の並び」は型名だけ書けばよく、引数名は省略しても構いません。

void f(int x, int y)
{
}

void (*pf1)(int, int) {f};  // 関数f を指し示す
void (*pf2)(int) {};        // ヌルポインタ
void (*pf3)(int);           // 自動ストレージ期間を持つ場合は未初期化
                            // 静的ストレージ期間を持つ場合はヌルポインタ

いつものように、{} で初期化した場合は値初期化(「構造体」のページを参照)、初期化子がない場合はストレージ期間に応じて結果が異なります。

関数ポインタにもヌルポインタという状態があり、関数を指し示していない状態です。ヌルポインタを表現する方法はオブジェクトポインタと同様なので(「構造体とポインタ」のページを参照)、明示的に書くなら nullptr を使うのがいいです。

上のコード例で、f という関数名を渡せていますが、f 自体は関数そのものです。関数ポインタに渡すべきものは関数そのものではありませんが、関数型は暗黙的に関数ポインタに変換できるため問題ありません。&演算子を使って書くこともできますが、意味はまったく同じになります。

void (*pf1)(int, int) {&f};  // f も &f も同じこと

関数ポインタ変数を const にするときは、* と関数ポインタ名のあいだに割り込ませます。先頭に置くと、戻り値型に const が付いていることになってしまいます。

void (* const pf)(int, int) {f};  // pf は書き換え不可

オブジェクトポインタと違って、関数ポインタには constポインタのようなものはありません。

型名の記述は、型推論に任せることもできます。

double f(int x)
{
    // ...
}

auto pf = f;  // double (*pf)(int)

関数ポインタ型に別名を付けるには次のようにします。

using 別名 = 戻り値の型 (*)(仮引数の並び);
typedef 戻り値の型 (*別名)(仮引数の並び);

どちらの方法を使っても結果は同じです。具体的なコードで書くとこうなります。

int double_to_int(double x)
{
    // ...
}

using converter_t = int (*)(double);
// あるいは、typedef int (*converter_t)(double);

converter_t converter {double_to_int}; 

関数ポインタが指し示している関数は、次のようにして呼び出せます。もちろん、戻り値があれば返されてきます。

関数ポインタ(実引数の並び);
(*関数ポインタ)(実引数の並び);

2つの書き方はまったく同じ意味なので、簡潔な1つ目の方法を使えばいいです。「関数ポインタ」が有効な関数を指し示していなければ、未定義の動作です

関数ポインタが指し示す関数を変えることで、実行時に処理を切り替えられます。次のプログラムでは、op(10, 1) という記述が、2つの異なる処理を実行していることが確認できます。

#include <iostream>

int plus(int x, int y)
{
    return x + y;
}
int minus(int x, int y)
{
    return x - y;
}

int main()
{
    int (*op)(int, int) {plus};
    std::cout << op(10, 1) << "\n";

    op = minus;
    std::cout << op(10, 1) << "\n";
}

実行結果:

11
9

引数に関数ポインタを渡すことで、コールバック関数を実現できます。

#include <cassert>
#include <iostream>
#include <vector>

using converter_t = int (*)(int);

void print_convert_values(const std::vector<int>& values, converter_t converter)
{
    assert(converter);

    for (std::vector<int>::size_type i {0}; i < values.size(); ++i) {
        std::cout << converter(values.at(i));  // コールバック
        if (i < values.size() - 1) {
            std::cout << ", ";
        }
    }
    std::cout << "\n";
}

int change_sign(int v)
{
    return -v;
}

int double_number(int v)
{
    return v * 2;
}

int main()
{
    std::vector<int> values {7, -5, 10, -1, 2};

    print_convert_values(values, change_sign);
    print_convert_values(values, double_number);
}

実行結果:

-7, 5, -10, 1, -2
14, -10, 20, -2, 4

渡されてきた関数ポインタがヌルポインタである可能性に注意してください。このサンプルでは assert でチェックしています。関数ポインタは bool型に暗黙的に変換できるので、assert(converter != nullptr) のように書く必要はありません。


当初の目的に戻って、関数ポインタを使ってオプションの一覧表を実現してみます。

// --- 汎用的なコード ---

using option_callback_t = void (*)();

struct Option {
    std::string        name;
    option_callback_t  func;
};


// --- バイナリエディタ固有のコード ---

void on_option_lower()
{
    // --lower発見時の処理
}

const std::vector<Option> options {
    { "--lower", on_option_lower },
};

これで、“–lower” のような具体的なオプション名や、そうしたオプションを発見したときに実行する処理を、汎用的なコードの側から取り除くことができます。

ラムダ式

コールバック関数を実現する方法として、ラムダ式を使うこともできます。

ラムダ式はすでに何度も使っていますが、じつはそのほとんどがコールバック関数そのものといえる使い方をしています。たとえば、条件に合う要素の個数を調べる std::count_if関数は、第3引数にラムダ式を指定できます(「要素を探索する」のページを参照)。

std::vector<int> v {8, 11, -10, 0, -5, 13};
auto count = std::count_if(std::cbegin(v), std::cend(v), [](int e){ return e < 0; });

std::count_if関数内では v の要素を1つ1つ順番に辿りながら、そのつど第3引数に指定したラムダ式(の本体のコード)をコールバックしています。

このプログラムは、次のように関数オブジェクト(「シャッフルと乱数」のページを参照)を使って書き換えられます。

struct IsNegative {
    bool operator()(int value)
    {
        return value < 0;
    }
};

std::vector<int> v {8, 11, -10, 0, -5, 13};
auto count = std::count_if(std::cbegin(v), std::cend(v), IsNegative());

ラムダ式が導入された C++11 よりも前の時代には、実際にこのようなコードを書いていました(普通の関数でもいいですが)。これは少々面倒ですし、実際に使いたい箇所から離れたところにコードを書くことになるのもデメリットでした。ラムダ式は、必要なことを必要になるその場所に、埋め込むような感じで記述できることが1つの利点であるといえます。

実際のところ、ラムダ式は関数オブジェクトを簡単に記述する構文です。ラムダ式が行っていることは、関数オブジェクト(これはクラスなので型)と、そのオブジェクトの生成です。作られるクラスをクロージャ型 (closure type) と呼び、その型のオブジェクトをクロージャオブジェクト (closure object) と呼びます。

ラムダ式の基本的な構文は次のようになっています。

[](仮引数の並び) -> 戻り値の型 {本体}

「-> 戻り値の型」は省略できます。その場合の戻り値の型は、「本体」のコードから型推論によって決定します。あえて auto を記述できるほか、auto& と書けば、参照型に型推論させられます(「関数から値を返す」のページを参照)。

クロージャ型は次のようなクラスです。「戻り値の型」「仮引数の並び」「本体」にはそれぞれ、ラムダ式に記述したものが入ります。クラス名はコンパイラが決めるため、ここでは ???? としています。

class ???? {
public:
    戻り値の型 operator()(仮引数の並び) const
    {
        本体
    }
};

operator() の末尾に const がありますが、メンバ関数の宣言の末尾に const があると、そのメンバ関数内からデータメンバの値を書き換えられなくなります。このようなメンバ関数は、constメンバ関数 (const member function) と呼ばれます。

あとで取り上げる mutable を使うと、この const を解除できます。

【上級】ラムダ式の仮引数の型を auto にすることでジェネリックラムダになります2。この場合、生成される operator() が関数テンプレートなり、任意の型の引数を受け付けるようになります。


ラムダ式を評価した結果はクロージャオブジェクトです。変数に受け取れますが、クロージャ型の名前はコンパイラが決めるため、型名をソースコードに記述できません。方法の1つとして auto で型推論させることで、型名の記述を避ける手があります。

auto is_negative = [](int e){ return e < 0; };
// is_negative の型名は処理系定義

しかし、仮引数や、構造体のデータメンバなど、auto が使えない場面もあります。そうした場面では、あとで取り上げる std::function を使う方法があります。

また、クロージャオブジェクトは、仮引数や戻り値がラムダ式の記述と一致する関数ポインタに変換できるため(ただし、あとで取り上げるキャプチャを使っている場合を除く)、関数ポインタ型で受け取る方法もあります。

bool (*is_negative)(int) = [](int e){ return e < 0; };

クロージャオブジェクトに () を使えば、ラムダ式の本体を呼び出せます。

bool result {is_negative(x)};

ラムダ式を記述した流れで呼び出すことも可能です。

bool result {[](int e){ return e < 0; }(x)};


ラムダ式から関数ポインタに変換できるので、関数ポインタのところで書いたオプションの一覧表はそのまま利用できます。

// --- 汎用的なコード ---

using option_callback_t = void (*)();

struct Option {
    std::string        name;
    option_callback_t  func;
};


// --- バイナリエディタ固有のコード ---

const std::vector<Option> options {
    { "--lower", [](){ /* --lower発見時の処理 */ } },
};

キャプチャ

ラムダ式の本体のコードから、本体の外側で宣言している変数を使いたいことがあります。たとえば、std::count_if関数の例で、あるローカル変数に格納されている値以上の要素をカウントしたいとします。次のように書ければよさそうですが、これはコンパイルできません。

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

int main()
{
    std::vector<int> v {8, 11, -10, 0, -5, 13};

    int min {};
    std::cin >> min;

    std::cout << std::count_if(std::cbegin(v), std::cend(v), [](int e){ return e >= min; }) << "\n";
}

ラムダ式の本体からは、ラムダ式の外側で宣言されているローカル変数(静的ローカル変数は除く)や仮引数を使うことはできません。そこで、キャプチャ (capture)(ラムダキャプチャ (lambda capture))という機能を使います。キャプチャは、ラムダ式を記述する位置からみえるローカル変数(静的ローカル変数は除く)や仮引数を、ラムダ式の本体のコードから使用できるようにする機能です。

キャプチャの書き方にはいくつか種類があります。このあと、それぞれ解説します。

なお、キャプチャを使っているラムダ式は、その情報を覚える場所が必要になるため、関数ポインタに変換できません。

コピーキャプチャ

一番基本となるキャプチャは、コピーキャプチャ(コピーによるキャプチャ) (capture is by copy) です。

[キャプチャ](仮引数の並び) -> 戻り値の型 {本体}

「キャプチャ」の部分には、本体で使用したい変数の名前を記述します。複数の変数をコピーキャプチャする場合は , で区切って記述します。あとで取りあげるデフォルトキャプチャを用いる方法もあります。

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

int main()
{
    std::vector<int> v {8, 11, -10, 0, -5, 13};

    int min {};
    std::cin >> min;

    std::cout << std::count_if(std::cbegin(v), std::cend(v), [min](int e){ return e >= min; }) << "\n";
}

実行結果:

5  <-- 入力された値
3

コピーキャプチャされた変数は、クロージャオブジェクトのデータメンバになり、その値はキャプチャされた変数からコピーされます。つまり、このラムダ式からは、次のようなクロージャ型が生成されています。

class ???? {
    int min;  // 値は、キャプチャされた変数からコピーされてくる

public:
    bool operator()(int e) const
    {
        return e >= min;
    }
};

コピーを取っているので、キャプチャされた変数の値を書き換えても、クロージャオブジェクトのデータメンバには影響しませんし、その逆も同様です。また、そもそも operator() はデフォルトで constメンバ関数なので、データメンバを書き換えることはできません。

std::count_if(std::cbegin(v), std::cend(v), [min](int e){
    min *= 2;  // エラー
    return e >= min;
});

mutableキーワード を使うと、operator() が constメンバ関数でなくなり書き換えられるようになります。

std::count_if(std::cbegin(v), std::cend(v), [min](int e) mutable {
    min *= 2;  // データメンバを書き換える。キャプチャされた変数には影響なし
    return e >= min;
});

データメンバを書き換えているので、次に本体が呼び出されたときには、書き換え後の値を使うことになります。

参照キャプチャ

キャプチャした変数の値を、ラムダ式の本体から書き換えたいのなら、参照キャプチャ(参照によるキャプチャ) (capture is by reference) を使います。

コピーキャプチャと参照キャプチャのどちらを使うかの判断は、関数にコピーで渡すか参照で渡すかの判断と同じです。ラムダ式の本体から値を書き換えたい場合や、オブジェクトが大きく、コピーに時間が掛かってしまう場合などに参照キャプチャを使います。

[] の内側に記述する変数名の頭に & を付けると参照キャプチャになります。複数の変数をキャプチャする場合は、, で区切って記述するか、あとで取り上げるデフォルトキャプチャを使います。

[&変数名](仮引数の並び) -> 戻り値の型 {本体}
std::count_if(std::cbegin(v), std::cend(v), [&min](int e) mutable {
    min *= 2;  // キャプチャされた min を書き換えている
    return e >= min;
});

コピーキャプチャと混在できます。参照キャプチャしたい変数は1つ1つ & を付けなければなりません。

[&a, b, &c](){};  // a と c は参照キャプチャ、b はコピーキャプチャ

参照キャプチャされた変数は、クロージャオブジェクトの参照型のデータメンバになります。そのため、このデータメンバにアクセスすることは、キャプチャされた変数にアクセスすることと同じ意味になります。

class ???? {
    int& min;  // キャプチャされた変数の参照

public:
    bool operator()(int e) const
    {
        min *= 2;
        return e >= min;
    }
};

参照なので、キャプチャされた変数の側で行った値の書き換えは、ラムダ式の本体からアクセスしたときの値に影響します。

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

int main()
{
    std::vector<int> v {8, 11, -10, 0, -5, 13};
    int min {0};

    auto greater_min = [&min](int e){ return e >= min; };  // この時点では min は 0
    min = 10;
    std::cout << std::count_if(std::cbegin(v), std::cend(v), greater_min) << "\n";  // 本体を実行するときには 10 になっている
}

実行結果:

2

また、参照キャプチャによって参照された変数の寿命が、クロージャオブジェクトよりも先に尽きるケースがあります。そのような場合のアクセスは当然、未定義動作となるので注意が必要です。

#include <iostream>

auto f()
{
    int x {2};
    return [&x](int a){ x *= a; return x; };  // x を参照キャプチャ
}  // x の寿命が終わる

int main()
{
    auto mul = f();
    std::cout << mul(10) << "\n";  // 寿命が終わっている x を使ってしまう(未定義動作)
}

デフォルトキャプチャ

キャプチャする変数の名前を書き並べる以外に、[=][&] と記述する方法もあります。[=] は見える範囲にあるすべての(静的ローカル変数を除く)ローカル変数と仮引数をまとめてコピーキャプチャし、[&] はまとめて参照キャプチャします。これらの方法はデフォルトキャプチャ (default capture) と呼ばれます。文法は次のとおりです。

[=](仮引数の並び) -> 戻り値の型 {本体}
[&](仮引数の並び) -> 戻り値の型 {本体}
#include <iostream>

int main()
{
    int a {10};
    int b {20};
    int c {30};

    [&](){
        std::cout << a + b + c << "\n";
        c = 0;
    }();

    [=](){ std::cout << a + b + c << "\n"; }();
}

実行結果:

60
30

また、一部の変数だけをコピーキャプチャして、残りは全部参照キャプチャにするとか、その逆に、一部の変数だけを参照キャプチャして、残りは全部コピーキャプチャにするといった場合、次のように記述できます。

[=, &変数名(, &変数名・・・)](仮引数の並び) -> 戻り値の型 {本体}
[&, 変数名(, 変数名・・・)](仮引数の並び) -> 戻り値の型 {本体}
#include <iostream>

int main()
{
    int a {10};
    int b {20};
    int c {30};

    [=, &c](){
        std::cout << a + b + c << "\n";
        c = 0;
    }();

    [&, c](){
        std::cout << a + b + c << "\n";
        a = 0;
        b = 0;
    }();

    std::cout << a << ", " << b << ", " << c << "\n";
}

実行結果:

60
30
0, 0, 0

デフォルトキャプチャは便利ですが、誤解しやすい機能でもあります。たとえば、静的ローカル変数はキャプチャとは関係がなく、そもそもラムダ式の本体から普通に使用できます。しかし、[=] を使っているとコピーキャプチャしているように錯覚するかもしれません。

#include <iostream>

int main()
{
    int a {10};
    int b {20};
    static int c {30};

    [=](){
        std::cout << a + b + c << "\n";
        c = 0;  // ???
    }();

    std::cout << a << ", " << b << ", " << c << "\n";
}

実行結果:

60
10, 20, 0

c がコピーキャプチャされたものであるなら、本体内で行った c = 0 は、ローカル変数c には影響を与えないはずです。しかし実行結果のとおり、c0 に書き換えられています。c = 0 はキャプチャされたのではなく、静的ローカル変数c を直接アクセスしているわけです。

デフォルトキャプチャの使用は避けて、本当に必要な変数だけを明示的に書き並べることを勧めるガイドラインも存在しています3

【上級】あるクラスC のメンバ関数内でデフォルトキャプチャを行うと this もキャプチャされます([=][&] いずれでも)。ラムダ式の本体から、C のデータメンバを使えるようになるため、C のデータメンバがキャプチャされているようにみえますが、実際には thisポインタを経由したアクセスです。キャプチャされたわけではないので、C のデータメンバを書き換えることもできてしまうので、[=] を使うとコピーキャプチャしているようにみえますが、実質的には参照によるアクセスと変わりません。そのため、this が無効になったあとにラムダ式の本体を呼び出すと、不正なアクセスになってしまうことに注意が必要です。
問題を避けるためには、C のデータメンバを一旦ローカル変数にコピーして、それをキャプチャすることが考えられます。これは、あとで取り上げる初期化キャプチャで実現することもできます。また、下のコラムで取り上げるとおり、C++17 からは新たな選択肢もあります。

【上級】【C++17】メンバ関数内のラムダ式で、[*this] というキャプチャが可能になりました4。このキャプチャでは、ラムダ式を評価した時点で this が指し示しているオブジェクトをキャプチャします。

【上級】【C++20】メンバ関数内のラムダ式で、[=, this] というキャプチャが可能になりました5。これは [=] とだけ書くことと同じですが、this がキャプチャされていることを明確に示せるという意味があります。

初期化キャプチャ

初期化キャプチャ (init capture) を使うと、式の結果をキャプチャできます。文法は次のとおりです。

[識別子 初期化子](仮引数の並び) -> 戻り値の型 {本体}
[&識別子 初期化子](仮引数の並び) -> 戻り値の型 {本体}

「識別子」には任意の名前を記述し、その値を作る式を「初期化子」に記述します。「本体」ではその値を「識別子」の名称で使用できます。

たとえば、[a = x + 1] のように記述しますが、[a {x + 1}] のような書き方も許されます。

「識別子」の頭に & を付けた場合は参照キャプチャ、付かなかった場合はコピーキャプチャです。「識別子」と「初期化子」のペアを , で区切って複数記述できるほか、初期化キャプチャではないキャプチャを混ぜることもできます。

「初期化子」に任意の式を記述できるので、たとえば [a = f()] とすれば、f関数の戻り値をキャプチャできます。[a = x, &r = x] のようにして、同じローカル変数のコピーと参照を作るといった応用も可能です。

【上級】ムーブによるキャプチャを実現するためにも使用します([a = std::move(x)])。

【上級】データメンバをキャプチャするためにも使用できます([data = this->data])。通常ならデータメンバはキャプチャできませんが、初期化キャプチャの「初期化子」に記述する式の中でならデータメンバを使用できます。式として書いた this->data の値を data という名前でキャプチャしていることになります。

#include <iostream>

int f()
{
    static int value {1};
    return value++;
}

int main()
{
    for (int i {0}; i < 5; ++i) {
        [v = f()](){ std::cout << v << "\n"; }();
    }
}

実行結果:

1
2
3
4
5

std::function

クロージャオブジェクトを受け取るために、auto や関数ポインタ型を使う方法を取り上げましたが、auto は仮引数やデータメンバには使えませんし、キャプチャを使っているラムダ式は関数ポインタに変換できません。

そこで、汎用的に使える方法として std::function6 があります。std::function を使うには、<functional> をインクルードする必要があります。

std::function は、呼び出し可能オブジェクト (callable object)を保持するための型です。呼び出し可能オブジェクトとは、() を使った呼び出しができる型(呼び出し可能型 (callable type))のオブジェクトのことです7。具体的には、関数ポインタや関数オブジェクトを保持できます。関数は関数ポインタに変換できますし、ラムダ式は関数オブジェクトを生成していますから、これらもそのまま渡せます。

std::function の型名の記述はやや特殊です。

std::function<戻り値の型(仮引数の並び)>

たとえば、void f(int a, double b); という関数を指し示す関数ポインタを保持するには、次のように記述します。

std::function<void(int, double)>;

std::function のデフォルトの初期値は、何も呼び出せるものを保持していない、いわば空の状態です。仮引数の型と順番、戻り値の型が一致すれば、どんな関数ポインタや関数オブジェクトでも保持できます。

void f(int a, double b);

std::function<void(int, double)> func {};

// 関数ポインタ
void (*pf)(int, double) {f};
func = pf;
func = f;  // 関数は関数ポインタに変換できるので、そのまま渡してもいい

// 関数オブジェクト(ラムダ式)
func = [](int a, double b){};

// キャプチャ付きのラムダ式
int x {};
func = [x](int a, double b){};

保持している呼び出し可能オブジェクトを呼び出すには () を使います。

func(100, 2.5);

空の状態で呼び出そうとしてはいけません。空の状態でないことは、次のようにして確認できます。

if (func) {  // 空の状態でなければ true
    func(100, 2.5);
}

【上級】空の状態で呼び出そうとすると、std::bad_function_call 例外が送出されます。

std::function は仮引数やデータメンバとしても使用できるので、オプションの一覧表を次のように書き換えられます。

// --- 汎用的なコード ---

using option_callback_t = std::function<void()>;

struct Option {
    std::string        name;
    option_callback_t  func;
};


// --- バイナリエディタ固有のコード ---

void on_option_lower()
{
    // --lower発見時の処理
}

const std::vector<Option> options {
    // 以下のどちらでも対応できる
    { "--lower", on_option_lower },
    { "--lower", [](){ /* --lower発見時の処理 */ } },
};

実際にバイナリエディタのプログラムを改造する作業は、練習問題で取り組むことにします。

まとめ


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク


練習問題

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (基本★★)

int型の配列から、指定の条件を満たす最初の要素を見つけて、その要素を指し示すポインタを返す関数を作成してください。見つからない場合はヌルポインタを返すようにしてください。

解答・解説

問題2 (基本★★)

int型の配列から、条件を満たす要素を順番に探し、そのつどコールバック関数に渡す関数を作成してください。

解答・解説

問題3 (基本★★)

問題2で作成した関数に与える条件として、「標準入力から入力された2つの整数min、max の範囲内の値であること」を指定したプログラムを作成してください。

解答・解説

問題4 (応用★★★)

コールバック関数を利用して、バイナリエディタのプログラムのコマンドライン引数を解析する部分を汎用的に使えるように改造してください。

最新のバイナリエディタのプログラムは、「スコープと名前空間」のページの練習問題にあります。

解答・解説

問題5 (応用★★★)

問題4のバイナリエディタのプログラムをさらに改造し、パラメータ付きオプションに対応できるようにしてください。たとえば、“–size 100” のように指定すると、100バイト分の情報だけを出力するというオプションを実装してください。

(現状、実際のファイルのサイズを越える指定がなされた場合を対処できないので、今のところ無視して構いません)。

解答・解説


解答・解説ページの先頭



更新履歴




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