質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.50%
C++11

C++11は2011年に容認されたC++のISO標準です。以前のC++03に代わるもので、中枢の言語の変更・修正、標準ライブラリの拡張・改善を加えたものです。

C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

Q&A

解決済

3回答

6084閲覧

[C++]コールバックの実装方法について

gari

総合スコア21

C++11

C++11は2011年に容認されたC++のISO標準です。以前のC++03に代わるもので、中枢の言語の変更・修正、標準ライブラリの拡張・改善を加えたものです。

C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

0グッド

0クリップ

投稿2020/09/15 15:51

組み込みのソフト開発でコールバック関数の実装をしようとしております。
C++でコールバックを実装する場合、主に以下の2パターンの認識です。
(もしほかの方法(関数ポインタ以外)があればご教示いただきたいです。)

それぞれ、メリットとデメリットを教えていただけないでしょうか。

個人的には、純粋仮想関数を使う方法がベターだと考えているのですが、
C++の経験が浅く、知らないことも多いため、気になったため質問させていただきます。
メモリやCPUリソースに余裕が無いため、なるべくコストが少ない方法を選びたいです。

◆1. 仮想関数(純粋仮想関数)で定義された関数を呼ぶ。

C++

1#include <stdio.h> 2 3//下位層 4class Callback{ 5 public: 6 //投げるコールバック一覧を宣言 7 virtual void cb_func(void) = 0; 8}; 9class Task{ 10 public: 11 //上位層のコールバック先クラスのインスタンスを保存 12 Task(Callback* func) : func_(func){}; 13 void Start(void){ 14 //コールバック 15 func_->cb_func(); 16 } 17 private: 18 Callback* func_; 19}; 20 21//上位層 22class CbImpl : public Callback { 23 //コールバックを受けるクラスの定義 24 virtual void cb_func() override { 25 printf("call func."); 26 } 27}; 28int main(void){ 29 Callback* cb = new CbImpl(); 30 Task* tsk = new Task(cb); 31 //実行 32 tsk->Start(); 33 return 0; 34}

◆2. 関数ポインタをstd::functionで受け取ってそれを呼び出す。

C++

1#include <stdio.h> 2#include <functional> 3 4//下位層 5class Task{ 6 public: 7 //上位層の関数ポインタを保存 8 Task(std::function<void(void)> func): func_(func){}; 9 void Start(void){ 10 //コールバック 11 func_(); 12 } 13 private: 14 std::function<void(void)> func_; 15}; 16 17//上位層 18//コールバックを受ける関数の定義 19void abc(void){ 20 printf("call func."); 21} 22int main(void){ 23 Task* tsk = new Task(abc); 24 //実行 25 tsk->Start(); 26 return 0; 27}

下記はそれぞれ、私の知っている(聞いたことのある)範囲のメリット・デメリットです。
1のパターンだと、
・VTableができて、メモリ使用量が少し増加する。(デメリット)
・VTableの分、呼び出しに少しオーバーヘッドがある。(デメリット)
・コールバック関数の実装漏れが無い(メリット)

2のパターンだと、
・std::functionを使うとコンパイラの最適化がされない。(デメリット)
・std::functionを使うとメモリ使用量が増加する。(デメリット)
・呼び出し時にオーバーヘッドがある。(デメリット)

よろしくおねがいします。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答3

0

単なる関数ポインタを定義して、それで呼び出せばどうでしょう

投稿2020/09/15 23:03

y_waiwai

総合スコア87719

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

gari

2020/09/16 15:09

せっかくC++を使えるので、できるなら安全な言語機能を使いたいです。
fana

2020/09/17 01:41 編集

これ,そもそも背景事情的に,Taskは(std::functionを使うことで得られるような)汎用性が必要なのだろうか? そのTaskのようなものを,その組込みソフト内の何箇所でどれだけ複雑に使っていく想定なのかわからないけども… 実際のところ,Cなcallbackの書き方で十分だったりしないのだろうか?という. 呼び出しのオーバヘッドだとか使用メモリの話とかを気にするならば,結局,簡素なものが最も良いという話も有り得るのでは,的な. (…とか思ったので,高評価なう)
guest

0

私はラムダ式が好きなのでstd::function推しですね。

仮想関数を使う場合は

  • CbImplインスタンスをこの関数のコールバック以外に使う必要がある場合(なにか変数を保持したり)
  • 複数のコールバック関数を同時に渡したい場合(OnSuccess,OnFail,OnProcess等)

となります。


呼び出しのオーバーヘッドについては
最適化されている場合気になることはないと思います。

メモリ使用量についても
ポインタ1個か2個程度のものに落ち着くと思いますので、関数の深さにまで拘るような環境で無い限りは気にならないはずです。

投稿2020/09/16 05:14

asm

総合スコア15147

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

gari

2020/09/16 15:11

ありがとうございます。 >複数のコールバック関数を同時に渡したい場合(OnSuccess,OnFail,OnProcess等) というのは、コールバック関数を複数用意する場合、ということでしょうか?
guest

0

ベストアンサー

こんにちは。

基本的に「可能な時はメジャーな手法を用いる」のが良いです。マイナーな手法の場合、知らない人が多いので当該コードを理解するのに余計な時間がかかります。また、メジャーな手法は多くの実績があるので落とし穴が少ない(もしくは、落とし穴の情報が広く出回っている)のでバグを発生させにくいです。

「コールバック関数の実装漏れが無い(メリット)」が活かせるケース(あまり多くはないと思いますが)でない限り、私なら上記原則に従ってstd::functionを用います。(ご提示されているようなケースならstd::fuction一択です。)

ところで、「std::functionを使うとコンパイラの最適化がされない。(デメリット)」は聞いたことがないです。出典を教えて頂けるとありがたいです。(std::fuctionはテンプレートなので最適化されると理解しています。実際にコードを確認したことはないので私の理解が間違っているかも知れないです。)

投稿2020/09/16 03:43

Chironian

総合スコア23272

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

fana

2020/09/16 03:54 編集

どうでもいい話ですが, 仮想関数とstd::function,どちらをより「マイナー」と感じるか?と言ったら, 私は std::function の方をよりマイナーな存在だと感じてしまいます. (きっと,私の目の前に出現した順序に依存している)
Chironian

2020/09/16 06:26

fanaさん なるほどです。 メジャーな方法とマイナーな方法どちらも使える時はメジャーな方法を使った方が好ましいと思いますが、おっしゃる通りstd:functionをそもそも知らない人も少なくない筈です。(標準機能ですから出会ったタイミングで覚えるのも1つとは思いますが。) つまり、std::functionは仮想関数に比べるとメジャーではないですね。 でも、このケースではやっぱりstd::functionを使った方が良いと思います。 単純に「ラムダ式を渡せるからstd::functionの方が良い」でも良いかも知れません。(ああ、asmさんが既に書かれてました。) 他にも「汎用な機能と専用な機能の両方が使える時は、専用機能を使った方が好ましい」という考え方もあります。専用機能の方が機能が限定されている分、コードの読み手に優しいです。選択肢が少ないのでより短時間で書けますしバグも作り込みにくいです。 std::functionは仮想関数に比べると関数を渡すという機能により特化された専用機能です。
fana

2020/09/16 06:42 編集

> このケースではやっぱりstd::functionを使った方が良いと思います その点に関しては同意であります. (でも,Taskの使い方次第では,weak_ptr<インタフェースクラスの型> みたいなのも有り得るかな?)
gari

2020/09/16 12:54

Chironianさん、fanaさん、ありがとうございます。 「メジャーな実装かどうか」、 「機能が限定的か汎用的か」の観点は考えてませんでした。 ありがとうございます。 ラムダも確かに魅力的ですね。抜けておりました。。 >ところで、「std::functionを使うとコンパイラの最適化がされない。(デメリット)」 これは他のc++を使っている人から聞いただけで、出典は見つけられず…… その人は「型の自動変換に欠陥があって、使った瞬間に最適化がほとんど阻害される」とおっしゃっていました。 ありがとうございます。
yohhoy

2020/09/16 13:27 編集

一般論としては、std::functionは関数ポインタやラムダ式(クロージャオブジェクト)よりも「汎化」されているため、性能面では不利になりがちです。 std::function内部実装ではType Erasure(型消去)テクニックが用いられるため、C++コンパイラにとっては非常に最適化しづらいのは間違いないと思います。 究極的には利便性と動作性能のトレードオフ問題になりますから、どちらを重視するかで決めれば良いとは思います。 組み込み用途のとことですが、std::functionは内部で動的メモリ確保を伴う(可能性がある)ことには留意した方が良いかも知れません。
gari

2020/09/16 15:08

yohhoyさん、ありがとうございます! 最適化しづらい理由が分かって大変スッキリしました。 >組み込み用途のとことですが、std::functionは内部で動的メモリ確保を伴う(可能性がある)ことには留意した方が良いかも知れません。 ありがとうございます。 メモリ確保が走るしきい値まではまだ理解できてなかったりしますが、 気をつけます。
Chironian

2020/09/16 15:11

gariさん、yohhoyさん なるほどです。 ギチギチに高速化したい時は、std::functionは使わない方が良さそうですね。 ところで、「std::functionは内部で動的メモリ確保を伴う(可能性がある)」はびっくりです。内部実装はポインタを1つと若干の追加関数呼び出しコードと思ってました。なにか複雑なケースが存在するのですね。
yohhoy

2020/09/17 02:27

std::function内部実装については、下記記事が理解の助けになるかもしれません。 https://cpplover.blogspot.com/2010/03/stdfunction.html https://qiita.com/rita0222/items/286bc7f6c98aa160b41b 前者の"簡易版実装"では、TypeErasureを実現するholder_base継承先クラスがnewされます。 実際のC++標準ライブラリでは"十分小さな関数オブジェクト"や関数ポインタに対して動的メモリ確保を回避するSmall Object Optimizationが行われます(より複雑な内部実装になっています)
Chironian

2020/09/17 05:36

yohhoyさん おお、ありがとうです。 江添氏のコードで理解できました。std::functionは関数オブジェクトのポインタを保持しているとなんとなく思っていたのですが、そうではなく関数オブジェクトそのものを保持しているということですね。 確かに当該関数オブジェクトのスコープを抜けるケースを考えるとポインタではなくそのものをコピーなりムーブなりして保持した方がバグになりにくいです。(性能が落ちるので、性能重視のC++にしては珍しい判断ですが、ポインタでは使い勝手が悪すぎると判断したのでしょうね。) 関数オブジェクトのサイズはそのクラスによって異なる(戻り値と引数は同じだがキャプチャが異なるラムダ式とか)ので動的メモリ確保が発生することに納得です。 更にQiitaの記事によると、std::functionオブジェクトに固定領域を設けているので、そこに入るくらい小さい時は動的確保はしないで済むという実装になっているようですね。 いや~、なんとなく思っていたより遥かにstd::functionは奥が深いです。
yohhoy

2020/09/18 04:39 編集

FYI: 質問主題からは外れてしまう高度なトピックですが、std::functionの設計に関して https://quuxplusone.github.io/blog/2019/03/27/design-space-for-std-function/ (英語記事) で色々な考察がされています。 > 確かに当該関数オブジェクトのスコープを抜けるケースを考えるとポインタではなくそのものをコピーなりムーブなりして保持した方がバグになりにくいです。 std::function は「関数のように呼び出せる何か」を「普通のオブジェクトのように扱える」ことがメリットです。この目的のため、多少のオーバーヘッドは許容しています。 > 性能が落ちるので、性能重視のC++にしては珍しい判断ですが、ポインタでは使い勝手が悪すぎると判断したのでしょうね。 前掲記事でも触れていますが、将来(C++23とかそれ以降)にむけて std::function のような所有権管理を行わず性能重視型の std::function_ref が提案されています ;P http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0792r5.html https://github.com/SuperV1234/Experiments/blob/master/function_ref.cpp (サンプル実装) まさに本質問のような、コールバックインタフェースでの応用を目指しているようです。
gari

2020/09/18 16:18

Chironianさん、yohhoyさん ありがとうございます。 std::functionの内部実装までは全然理解が追いついて無いのですが、 お二方のコメントとURLを見てstd::functionの動的メモリ確保について ある程度ですが理解できました。 (内部実装に関してはURLまで出していただいたのにすみません。全然分からない。。。 もっとC++勉強しなくては) 仮想関数とstd::functionでどちらを使うかの判断材料が得られたので 本質問についてはこちらの回答をベストアンサーにさせていただきます。 今回、実際に実装しようとしているコールバック関数は、大きめのデータを引数に取るものと、 引数のサイズは大きくないが高頻度で呼ばれるものなどがあるため、純粋仮想関数で実装しようと思います。 (std::functionの理解も少し進んだので早くどこかで使いたい)
Chironian

2020/09/18 17:09

yohhoyさん なるほどです。コールバック関数は意外に有用なのでどんどん進歩しつつあるようですね! ついていかねば。 gariさん 江添氏のコードの内、超難解な部分は実はあまり使われない機能(メンバ関数やデータメンバ)を保持するコードです。(元のstd::functionの仕様に沿うためと思います。) member_holder、data_member_holder とその呼び出し部は削除しても関数ポインタと関数オブジェクトは保持できる筈です。(大丈夫とは思いますが、若干自信がないです。) この2つは関数テンプレートのオーバーロードが使われており理解することが非常に難解な部分なのです。正直、読めるようになるには半年~1年かかっても可笑しくないと思います。(かくいう私も雰囲気は掴めてますが完全には読めていません。)
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.50%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問