はじめに
C++標準ライブラリでは「関数のように呼び出し可能なエンティティ」を統一的に取り扱える std::function<R(ArgTypes...)>
クラステンプレートが提供されます。
std::function
の導入によって便利になった一方で、クラス仕様に関する問題点が複数指摘されており、C++23および次期C++26標準ライブラリでは std::function
の代替となる3種類のクラステンプレートが追加されました。
-
std::move_only_function
[C++23] -
std::copyable_function
[C++26] -
std::function_ref
[C++26]
本記事ではこれらクラステンプレート群について、それぞれの特徴や使いどころを簡単に紹介します。
目的別に機能特化した3種類クラステンプレートの登場により std::function
は “いらない子” になってしまうのですが、C++26時点で std::function
を非推奨化(deprecated)する提案 はさすがに却下されました。C++26準拠環境が広く普及するまでは時間を要するでしょうから、当面の間は std::function
を継続利用しても問題ないと思います。
std::function[C++11]
std::function
クラステンプレートは、下記のエンティティを格納できます。項番1~4は同一の関数シグニチャint(int)
1となり、利用側では実際に何が格納されているかを知らなくても関数のように呼び出せます。項番5のメンバ関数では関数シグニチャint(MyClass&, int)
のようにクラス名が追加されますが、std::bind
やstd::bind_front
と組み合わせて第1引数のオブジェクトを固定(bind)できます。
#include <functional>
using namespace std;
int func(int);
struct Callable {
int operator()(int);
};
struct MyClass {
int member;
int mem_func(int);
static int static_mem_func(int);
};
int main()
{
// 通常の関数
function<int(int)> fn1 = &func;
fn1(42);
// ラムダ式
function<int(int)> fn2 = [](int n) -> int { return n; };
fn2(42);
// operator()を実装したクラス
Callable callable;
function<int(int)> fn3 = callable;
fn3(42);
// クラスの静的メンバ関数
function<int(int)> fn4 = &MyClass::static_mem_func;
fn4(42);
// クラスのメンバ関数
MyClass obj;
function<int(MyClass&, int)> fn5a = &MyClass::mem_func;
function<int(int)> fn5b
= bind(&MyClass::mem_func, obj, std::placeholders::_1);
// C++20以降なら bind_front(&MyClass::mem_func, obj) と書ける
fn5a(obj, 42);
fn5b(42);
// クラスのメンバ変数
function<int&(MyClass&)> fn6 = &MyClass::member;
fn6(obj) = 42;
}
std::move_only_function[C++23]
C++23標準ライブラリに追加されたstd::move_only_function
クラステンプレートは、std::function
が抱える下記の問題点を解決します。
- コピー不可/ムーブのみ可能なエンティティを扱えない
- 呼び出し時に毎回nullチェックが行われる
- const性が正しく伝搬されない
- noexcept性が正しく伝搬されない
- 右辺値/左辺値性が正しく伝搬されない
- 実行時型情報(RTTI)サポートを要求する5
項番1は名前に "move_only" と含まれる通り、move_only_function
ではコピー不可/ムーブのみ可能なエンティティを取り扱えます。当然ながら、move_only_function
自身もコピー不可/ムーブのみ可能なオブジェクトとなります。
項番2は動作性能の違いをもたらします。std::function
は呼び出しのたびにエンティティ保持有無をチェックします。move_only_function
では事前確認しておくことで呼び出し処理のオーバーヘッドを削減します。
項番3~5のうち「const性の伝搬」についてのみ例示コードで説明します。これ以外のユースケースは cpprefjpの例 を参照ください。関数シグニチャstring()
とstring() const
に対して、move_only_function
オブジェクト自身のconst性が適用されていることに着目してください。
-
mof1
: (非const)オブジェクト経由で(非const)メンバ関数を呼び出す -
mof2
: constオブジェクト経由で(非const)メンバ関数は呼び出せない(コンパイルエラー) -
mof3
: (非const)オブジェクト経由でconstメンバ関数を呼び出す -
mof4
: constオブジェクト経由でconstメンバ関数を呼び出す
一方でstd::function
ではオブジェクト自身のconst性によらず、常に(非const)メンバ関数が呼び出されます。
#include <functional>
#include <print>
using namespace std;
struct Functor {
// (非const)メンバ関数
string operator()()
{ return "non-const"; }
// constメンバ関数
string operator()() const
{ return "const"; }
};
int main()
{
move_only_function<string()> mof1 = Functor{};
const move_only_function<string()> mof2 = Functor{}; // (利用時にコンパイルエラー)
move_only_function<string() const> mof3 = Functor{};
const move_only_function<string() const> mof4 = Functor{};
println("mof1: {}", mof1()); // "non-const"
//println("mof2: {}", mof2()); // ❌コンパイルエラー
println("mof3: {}", mof3()); // "const"
println("mof4: {}", mof4()); // "const"
function<string()> fn1 = Functor{};
const function<string()> fn2 = Functor{};
// function<string() const> fn3 = Functor{}; // ❌コンパイルエラー
//const function<string() const> fn4 = Functor{}; // ❌コンパイルエラー
println("fn1: {}", fn1()); // "non-const"
println("fn2: {}", fn2()); // "non-const"
}
std::copyable_function[C++26]
C++26標準ライブラリに追加されるstd::copyable_function
クラステンプレートは、その名前の通り std::move_only_function
の「コピー対応バージョン」です。
std::function
が抱える下記の問題点を解決します。
- 呼び出し時に毎回nullチェックが行われる
- const性が正しく伝搬されない
- noexcept性が正しく伝搬されない
- 右辺値/左辺値性が正しく伝搬されない
- 実行時型情報(RTTI)サポートを要求する
std::move_only_function
およびstd::copyable_function
が解決する「const性の伝搬」は、std::function
の仕様バグとして2014年頃から指摘されていました。C++26以降はstd::function
の機能改善された代替品としてstd::copyable_function
を利用できます。
std::function_ref[C++26]
C++26標準ライブラリに追加されるstd::function_ref
クラステンプレートは、対象エンティティを所有管理しない軽量ラッパーです。関数引数などのAPI仕様において、シグニチャ型を固定したいユースケースが想定されます。
高階関数の引数として関数オブジェクトや関数ポインタを汎用的に受けとる場合、C++23現在は関数テンプレートとして実装するか(hof1
)、オーバーヘッドの大きいstd::function<R(Ts...)>
引数型を利用する(hof2
)しか選択肢がありませんでした。C++26以降ではstd::function_ref<R(Ts...)>
を引数型に利用することで、これらの問題が解決します(hof3
)。
#include <functional>
using namespace std;
// 関数テンプレートとして高階関数を実装
// ✅利点: 実行時オーバーヘッドが小さい
// ❌欠点: 関数シグニチャが型検査されない(ドキュメント必須)
// ❌欠点: 大量にインスタンス化されると生成コードが肥大化
template <class F>
void hof1(F f);
// 引数型にstd::functionを利用
// ✅利点: 関数シグニチャが型検査される
// ❌欠点: 実行時オーバーヘッドが大きい
void hof2(function<int(int)> f);
// 引数型にstd::function_refを利用
// ✅利点: 関数シグニチャが型検査される
// ✅利点: 実行時オーバーヘッドが小さい
void hof3(function_ref<int(int)> f);
目的特化されたstd::function_ref
は、以下の特徴をもちます。
- オブジェクトサイズが小さい6
- 対象エンティティの設定が必須(呼び出し時nullチェックなし)
- メンバ関数・メンバ変数を直接サポートする(
bind
等を用いない) - const性・noexcept性を伝搬する7
- 実行時型情報(RTTI)サポート不要
function_ref
におけるメンバ関数の直接サポートは、従来方式とは異なるアプローチで実現されます。コンストラクタの第1引数に非型タグnontype<F>
でメンバ関数ポインタを、第2引数には対象オブジェクトを指定することで、メンバ関数にthisポインタを束縛した関数シグニチャint(int)
として取り扱えます。
#include <functional>
using namespace std;
int f(int);
struct MyClass {
int mem_func(int);
};
int main()
{
// 通常の関数
function_ref<int(int)> fn1 = f;
fn1(42);
// クラスのメンバ関数
MyClass obj;
function_ref<int(MyClass&, int)> fn2a = nontype<&MyClass::mem_func>;
fn2a(obj, 42);
function_ref<int(int)> fn2b{nontype<&MyClass::mem_func>, obj};
fn2b(42);
}
まとめ
最後にC++標準ライブラリ<functional>
が提供するstd::function
ファミリの特徴を表形式で整理します。C++26が待ち遠しいですね(毎年同じような結び...)
特徴 | function | move_only _function |
copyable _function |
function_ref |
---|---|---|---|---|
ムーブのみ型 サポート |
❌ | ✅ | ❌ (設計判断) |
✅ |
所有権管理 | ✅ | ✅ | ✅ | - |
const伝搬 noexcept伝搬 |
❌ | ✅ | ✅ | ✅ |
右/左辺値伝搬 | ❌ | ✅ | ✅ | ❌ (設計判断) |
メンバ関数 サポート |
✔️ (要bind) |
✔️ (要bind) |
✔️ (要bind) |
✅ |
呼び出し時 nullチェック |
❌(有) | ✅(無) | ✅(無) | ✅(無) |
RTTI要求 | ❌(有) | ✅(無) | ✅(無) | ✅(無) |
推論補助 |
✅ (一部留保) |
❌ (留保) |
❌ (留保) |
✅ |
C++標準 | C++11 | C++23 | C++26 | C++26 |
-
関数シグニチャ(signature)に馴染みがなければ、「通常の関数宣言から関数名を削除したもの」と解釈すれば分かりやすいと思います。例えば関数
int func(int)
は関数型int(int)
を持ちます。 ↩ -
C++の ラムダ式(lambda expression) は「
operator()
を実装した匿名のクラス(closure type)」のインスタンスを生成します。これは、本質的には項番3の関数オブジェクトと同義です。 ↩ -
静的メンバ関数(static member function)の関数シグニチャは通常の関数シグニチャと同じですから、本質的には項番1の関数ポインタと同義です。 ↩
-
おそらくほとんど知られていないマイナー機能と思われますが、C++標準ライブラリの
std::function
は メンバ変数へのポインタ の取り扱いもサポートします。この奇妙な仕様はINVOKE
仮想操作 を介して実現されています。 ↩ -
実行時型情報(RTTI)サポートの要求は、字義通りのため本文中での説明は割愛します。
std::function
のtarget_type()
やtarget()
なんて利用してる人いないでしょ? ↩ -
std::function_ref
オブジェクトは、ポインタ2個分のサイズで実装可能とされています。高階関数の引数型として利用する場合、このオブジェクトサイズだとスタック渡しではなくレジスタ渡しによる軽量な取り扱いが可能となります。技術詳細は提案文書P0792R14を参照ください。 ↩ -
std::function_ref
はstd::move_only_function
やstd::copyable_function
と異なり右辺値/左辺値性を伝搬せず、対象エンティティが左辺値として呼び出し可能であること(Lvalue-callable)要求します。この動作に関してはstd::function
と同じです。 ↩