22
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C++Advent Calendar 2024

Day 1

std::functionと愉快な仲間たち ~move_only_function, copyable_function, function_ref~

Last updated at Posted at 2024-11-30

はじめに

C++標準ライブラリでは「関数のように呼び出し可能なエンティティ」を統一的に取り扱える std::function<R(ArgTypes...)> クラステンプレートが提供されます。

std::function の導入によって便利になった一方で、クラス仕様に関する問題点が複数指摘されており、C++23および次期C++26標準ライブラリでは std::function の代替となる3種類のクラステンプレートが追加されました。

本記事ではこれらクラステンプレート群について、それぞれの特徴や使いどころを簡単に紹介します。

目的別に機能特化した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::bindstd::bind_frontと組み合わせて第1引数のオブジェクトを固定(bind)できます。

  1. 通常の関数(関数ポインタ)
  2. ラムダ式2
  3. operator()を実装したクラス(関数オブジェクト)
  4. クラスの静的メンバ関数3
  5. クラスのメンバ関数
  6. クラスのメンバ変数4
(std::function利用例)
#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が抱える下記の問題点を解決します。

  1. コピー不可/ムーブのみ可能なエンティティを扱えない
  2. 呼び出し時に毎回nullチェックが行われる
  3. const性が正しく伝搬されない
  4. noexcept性が正しく伝搬されない
  5. 右辺値/左辺値性が正しく伝搬されない
  6. 実行時型情報(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)メンバ関数が呼び出されます。

(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が抱える下記の問題点を解決します。

  1. 呼び出し時に毎回nullチェックが行われる
  2. const性が正しく伝搬されない
  3. noexcept性が正しく伝搬されない
  4. 右辺値/左辺値性が正しく伝搬されない
  5. 実行時型情報(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は、以下の特徴をもちます。

  1. オブジェクトサイズが小さい6
  2. 対象エンティティの設定が必須(呼び出し時nullチェックなし)
  3. メンバ関数・メンバ変数を直接サポートする(bind等を用いない)
  4. const性・noexcept性を伝搬する7
  5. 実行時型情報(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
  1. 関数シグニチャ(signature)に馴染みがなければ、「通常の関数宣言から関数名を削除したもの」と解釈すれば分かりやすいと思います。例えば関数int func(int)は関数型int(int)を持ちます。

  2. C++の ラムダ式(lambda expression) は「operator()を実装した匿名のクラス(closure type)」のインスタンスを生成します。これは、本質的には項番3の関数オブジェクトと同義です。

  3. 静的メンバ関数(static member function)の関数シグニチャは通常の関数シグニチャと同じですから、本質的には項番1の関数ポインタと同義です。

  4. おそらくほとんど知られていないマイナー機能と思われますが、C++標準ライブラリのstd::functionメンバ変数へのポインタ の取り扱いもサポートします。この奇妙な仕様は INVOKE 仮想操作 を介して実現されています。

  5. 実行時型情報(RTTI)サポートの要求は、字義通りのため本文中での説明は割愛します。std::functiontarget_type()target()なんて利用してる人いないでしょ?

  6. std::function_refオブジェクトは、ポインタ2個分のサイズで実装可能とされています。高階関数の引数型として利用する場合、このオブジェクトサイズだとスタック渡しではなくレジスタ渡しによる軽量な取り扱いが可能となります。技術詳細は提案文書P0792R14を参照ください。

  7. std::function_refstd::move_only_functionstd::copyable_functionと異なり右辺値/左辺値性を伝搬せず、対象エンティティが左辺値として呼び出し可能であること(Lvalue-callable)要求します。この動作に関してはstd::functionと同じです。

22
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?