⚙️

<functional>でコードのレベルアップ! 【C++のテクニック】

2022/04/08に公開約5,500字

<functional>は、関数に+αするようなライブラリです。
他にもstd::reference_wrapperや、std::hashなども、<functional>で実装されます。

<functional>の機能は、便利で、知っておいて損はありません。

C++20時点での仕様を解説をします。

<functional>はC++11で追加されましたが、バージョンアップにてそこそこ大幅な仕様変更があったので、C++バージョンによってそこそこ違いがあります。

関数(関数ポインタ)・関数オブジェクト・ラムダ式を同等に扱う std::function

C言語で関数を引数に受け取ったり、には、関数ポインタを使用していました。
ただ、関数ポインタだとキャプチャをするラムダ式関数オブジェクトを受け取ることが出来ないので、そんな時はtemplate autoを使用します。
ただ、template autoでは型が決まっていないため、ヘッダとソースに分けられなかったり、エラーが見づらかったりしていました。

そこで、std::functionを使います。
std::function関数(関数ポインタ)関数オブジェクトラムダ式同じ型のオブジェクトとして扱うことができます。

std::functionのtemplate引数には戻り値型(第1引数の型, 第2引数の型, ...)の様に指定します。

関数・ラムダ式・関数オブジェクト
void func() { }

struct cls {
    void operator() { }
};

std::function<void()> f0 = func; // 関数
std::function<void()> f1 = []() { }; // ラムダ式
std::function<void()> f2 = cls{}; // 関数オブジェクト
キャプチャするラムダ式
int a = 10, b = 20;

std::function<int()> getVal = [&a, &b]() // キャプチャ
{
    return a + b;
};

std::functionも、ラムダ式や関数のように、関数呼び出し演算子()で呼び出しできます。

呼び出し
sayHello();

const int val = getVal();

a = 5; b = 40;
const int val2 = getVal();

少し特別な呼び出し std::invoke

関数呼び出しをする関数です。
これは普段の関数呼び出しにプラスで、INVOKE要件という要件に従った呼び出しが可能です。

cpprefjp - std::invoke

std::invoke(sayHello);

std::string str = "Hell";
std::invoke(&std::string::push_back, str, 'o');// str.push_back('o')のようになる

std::pair<int, char> p = { 12345, 123 };
const char p_sec = std::invoke(&std::pair::second, p);// p.secondのようになる

これは、普段の関数呼び出し()で十分な場合が多かったり、関数がオーバーロードされている場合型指定が必要で少し面倒だったりするので、std::invoke自体は、あまり使用しないかもしれません。

この関数はこの引数で呼び出し可能? std::is_invocable

std::is_invocableという、その呼び出し可能オブジェクトがある引数で呼び出し可能かどうかを調べる関数もあります。これは、std::invoke同様、INVOKE要件に従って呼び出し可能かを調べます。

templateの第1引数には、呼び出し可能なオブジェクトのを指定し、それ以降に引数のオブジェクトのを順に指定します。

// std::plus<int>は、引数(int, int)で呼び出し可能かどうか
const bool b = std::is_invocable_v<std::plus<int>, int, int>;

// C言語の可変長引数関数の関数ポインタ
const bool b2 = std::is_invocable_v<int(*)(const char*, ...), const char*, int, char>;

コンストラクタは別にstd::is_constructibleというものが<type_traits>に定義されています。

引数を事前にセットしておく

先頭からセット std::bind_front

引数の先頭から順番に束縛します。
std::bind_frontの戻り値は関数オブジェクトなので、std::functionにセットできます。

int f(int a, int b, int c) {
    return a + b + c;
}

const auto a = std::bind_front(f, 10, 20);
const std::function<int(int, int)> b = std::bind_front(f, 10);
const std::function<int()> c = std::bind_front(f, 10, 20, 30);

呼び出すときは束縛した引数を省略し、普通の関数呼び出しのように呼び出します。

呼び出し
a(30);
b(20, 30);
c();

好きな引数をセット std::bind

任意の引数を束縛します。
std::bindの戻り値も関数オブジェクトなので、std::functionにセットできます。

束縛しない引数にはプレースホルダーを使用します。
プレースホルダーは、呼び出しの時の引数の番号を指定します。

int f(int a, int b, int c) {
    return a + b + c;
}

// プレースホルダー[_1, _2, ...]が宣言されている名前空間
using namespace std::placeholders;

const auto a = std::bind(f, 10, _1, 30);
const std::function<int(int, int)> b = std::bind(f, _1, _2, 30);
const std::function<int()> c = std::bind(f, 10, 20, 30);

事前にセットした引数を省略して呼び出します。

呼び出し
a(20);
b(10, 30);
c();

std::mem_fn

std::mem_fnは、メゾッド単体から呼び出し可能オブジェクトを生成します。
INVOKE要件的には、std::bind_frontで代替ができます。

struct cls {
    void mthd() { }
    int mthd2(int a, int b) { return a + b; }
};

const auto a = std::mem_fn(&cls::mthd);
const std::function<int(int, int)> b = std::mem_fn(&cls::mthd2);
const std::function<int(int, int)> c = std::bind_front(&cls::mthd2);// bind_frontでもOK

オブジェクトを第一引数にセットします。

呼び出し
cls obj;

a(obj);
b(obj, 10, 20);
c(obj, 10, 20);

std::mem_fnの便利な使い方 メゾッドを関数の様に扱う

std::mem_fnの戻り値を更にstd::bind_frontすることで、メゾッドを関数と同等に扱うことが出来ます。革命的!!

void callFunc(const std::function<void()>& f) {
  f();
}

struct cls {
    void mthd() { std::cout << __FUNCTION__ "\n"; }
};

cls obj;

const auto func = std::bind_front(std::mem_fn(&cls::mthd), obj);
callFunc(func);

その他の機能

参照オブジェクト std::reference_wrapper

std::reference_wrapperは、普通の&の参照、そしてポインタに変わる、参照オブジェクトです。
std::reference_wrapperは、普通の参照&では出来なかった、参照先の変更が出来ます。

注意点として、普通に参照先の値に代入をしたい場合はget()などを使用して参照先にアクセスする必要があります。

int a, b;
std::reference_wrapper<int> rw = a;

rw = b;// bを参照するように切り替える
// rw = 100; これは出来ない
rw.get() = 100;

参照をコンテナに入れたい時にも使用します。

参照をコンテナに
std::vector<std::reference_wrapper<int>> refs;
int a, b, c;
refs.push_back(a);
refs.push_back(b);
refs.push_back(c);

std::ref std::cref

std::ref std::crefを使用してstd::reference_wrapperオブジェクトを生成します。
template引数に参照を渡すときなどに使用します。
template引数が1つの場合<>を使用して参照を指定すればいいですが、可変引数templateのように、いっぱいtemplate引数がある場合に優位に使えます。

template<class... Args>
void f(Args&&... args) {  }

int a;
f<int, int, int&>(10, 20, a);// これだと引数を変更するときに<>内も変更しないといけない
f(10, 20, std::ref(a));

ハッシュ値を求める std::hash

std::unordered_mapなどで使用されるハッシュ値を求める関数オブジェクトです。
戻り値はsize_t型です。

const auto i_h = std::hash<int>()(100);
const auto s_h = std::hash<std::string>()("abc");

演算子の関数オブジェクト

std::plus std::minus std::multiplies std::dividesなど、各演算子の関数オブジェクトが定義されています。

cpprefjp - <functional>

まとめ

std::functionを使用して関数(関数ポインタ)・関数オブジェクト・ラムダ式を同等のオブジェクトとして扱うことができます。

std::bind std::bind_frontを使用して、任意の引数を事前にセットをすることができます。

std::mem_fnを使用してメゾッドを普通の関数同等に扱うことが出来ます。

その他、<functional>には便利な関数オブジェクトがあります。

今後の開発の参考になれば幸いです。
ありがとうございました。

Discussion

ログインするとコメントできます