Standard C++

Pointer to Member Functions

“pointer-to-member-function” は “pointer-to-function” と型が違いますか?

Yes.

次の関数について考えてみましょう:

int f(char a, float b);

この関数の型は、それが普通の関数であるか、あるクラスの非staticメンバー関数であるかによって異なります:

注: それがclass Fredstaticメンバー関数である場合、その型はそれが普通の関数である場合と同じです:「int (*)(char,float)」です。

シグナル ハンドラ、X イベント コールバック、スレッド/タスクを開始するシステム コールなどに、メンバー関数へのポインタをどのように渡せばよいのでしょうか。

しないでください。

メンバ関数はそれを呼び出すオブジェクトがなければ意味がないので、直接これを行うことはできません (X WindowSystem が C++ で書き直された場合、おそらく関数へのポインタだけではなく、オブジェクトへの参照を渡しているでしょう; 当然、オブジェクトは必要な関数とおそらくそれ以上のものを体現しているはずです)。

既存のソフトウェアへのパッチとして、他の手法で取得したオブジェクトを受け取るラッパーとして、トップレベルの (非メンバーの) 関数を使用します。 呼び出すルーチンによって、この「他のテクニック」は些細なものかもしれませんし、あなたの側で少し作業が必要かもしれません。 例えば、スレッドを開始するシステム・コールは、void*と一緒に関数ポインタを渡すことを要求するかもしれないので、void*の中でオブジェクト・ポインタを渡すことができます。 多くのリアルタイムオペレーティングシステムでは、新しいタスクを開始する関数に対して同じようなことをします。 最悪の場合、オブジェクトポインタをグローバル変数に格納することもできます。これはUnixシグナルハンドラでは必要かもしれません(しかし、グローバルは一般的には望ましくありません)。

最悪の場合 (グローバルを使用した場合) の例を挙げます。

注意: static メンバー関数は呼び出される実際のオブジェクトを必要としないので、staticメンバー関数へのポインタは通常通常の関数へのポインタと型互換性があります。 しかし、おそらくほとんどのコンパイラで動作しますが、実際にはextern "C"非メンバー関数でなければ正しくありません。なぜなら、「C連携」は名前のマングリングだけでなく、CとC++で異なるかもしれない呼び出し規則もカバーするからです。

メンバ関数を割り込みサービスルーチンとして使用しようとすると、なぜコンパイルエラー (型の不一致) が発生し続けるのでしょうか。

これは前の 2 つの質問の特殊なケースですので、前の 2 つの回答を最初にお読みください。 thisポインタは、オブジェクトのインスタンスデータを指します。 システム内の割り込みハードウェア/ファームウェアはthisポインタの引数を提供することができません。

1 つの可能な解決策は、割り込みサービスルーチンとして static メンバを使用し、その関数が割り込み時に呼び出すべきインスタンス/メンバのペアを見つけるためにどこかを探すようにすることです。

Why am I trouble taking the address of a C++ function?

Short answer: If you’re trying to store it into (or pass it as a pointer-to-function), that’s the problem – this is a corollary to the previous FAQ.

Long answer: C++ では、メンバ関数はオブジェクト (メンバ関数内の this ポインタ) を指し示す暗黙のパラメータを持っています。 通常の C 関数はメンバ関数とは異なる呼び出し規則を持っていると考えることができるので、そのポインタの型 (pointer-to-member-function vs pointer-to-function) は異なり、互換性がありません。 C++ では、pointer-to-member と呼ばれる新しいタイプのポインターを導入しており、オブジェクトを提供することによってのみ呼び出すことができます。

NOTE: pointer-to-member-function を pointer-to-function に「キャスト」しようとしないでください。 例えば、pointer-to-member-function は、適切な関数のマシン アドレスを含む必要はありません。

How can I avoid syntax errors when creating pointers to members?

Use a typedef.

Yeah, right, I know: you are different.Why can I avoid syntax errors when you create pointers to members? あなたは賢い。 typedef を使わなくても、このようなことができるのです。 ため息が出ます。 私は、あなたと同じように、この FAQ のシンプルなアドバイスを拒否した人たちから多くのメールを受け取りました。 10秒のtypedefを使えば生活が楽になるのに、何時間も何時間も無駄にしたのです。 それに、あなたは自分だけが読めるコードを書いているわけではありません。疲れているときや、締め切りがあるとき、自分の課題があるときに、他の人も読めるようなコードを書きたいと思っているのです。 では、なぜ意図的に自分自身や他の人の生活を難しくするのでしょうか?

ここにサンプルクラスがあります:

class Fred {public: int f(char x, float y); int g(char x, float y); int h(char x, float y); int i(char x, float y); // ...};

型定義は単純です:

typedef int (Fred::*FredMemFn)(char x, float y); // Please do this!

これだけです! FredMemFn は型名で、その型のポインタは Fredf, g, h, i のような (char,float) を取る任意のメンバを指します。

そして、メンバー関数ポインタを宣言することは些細なことです:

int main(){ FredMemFn p = &Fred::f; // ...}

そして、メンバー関数ポインタを受け取る関数を宣言することも些細なことです:

void userCode(FredMemFn p){ /*...*/ }

そして、メンバー関数ポインタを返す関数を宣言することも些細なことです:

FredMemFn userCode(){ /*...*/ }

だから頼む、typedefを使用してくれ。 または、メンバー関数ポインターの問題について私にメールを送らないでください!

Pointer-to-Member-function を使用してメンバー関数を呼び出す際に構文エラーを回避するにはどうすればよいですか?

C++17 標準の適切な部分を実装しているコンパイラーと標準ライブラリにアクセスできる場合は、std::invoke を使用します。 そうでなければ、#define マクロを使用してください。

お願いします。

このアドバイスを拒否して混乱した人々から、あまりにも多くの電子メールを受け取りました。 とても簡単なことなのです。 std::invoke もマクロも必要ないし、相談した専門家はそのどちらもなくてもできるのでしょうが、お金という大切なものの邪魔をするようなエゴを持たないでください。 他のプログラマはあなたのコードを読む/保守する必要があります。 そうです、あなたは他の誰よりも賢い。 そして、あなたは素晴らしいです。 しかし、あなたのコードに不必要な複雑さを加えないでください。

std::invoke を使用することは些細なことです。 注: FredMemFn は pointer-to-membertype の typedef です。

もし std::invoke が使えないなら、逆説的ですが、この特別なケースでは #define マクロを使って保守コストを減らしましょう。

(通常、私は #define マクロを嫌いますが、ポインタトメンバで使うべきです。なぜなら、その種のコードの可読性と書き込み性が向上するからです。)

マクロはつまらないものです:

#define CALL_MEMBER_FN(object,ptrToMember) ((object).*(ptrToMember))

マクロを使うこともつまらないものです。 注: FredMemFn はメンバー型へのポインタに対する typedef です:

std::invoke またはこのマクロが良いアイデアである理由は、メンバー関数呼び出しはしばしば、今挙げた単純な例よりもずっと複雑であるためです。 comp.lang.c++ は、構文を正しく理解できずに混乱したプログラマたちからの何百もの投稿に耐えなければなりませんでした。 5473>

Note: #define マクロは 4 つの異なる方法で邪悪です: evil#1, evil#2, evil#3, and evil#4. しかし、それらはまだ時々有用です。

How do I create and use an array of pointer-to-member-function?

typedefstd::invoke または前述の #define マクロを両方使えば、90%は完了します。

Step 1: typedef:

Step 2: std::invoke:

#define CALL_MEMBER_FN(object,ptrToMember) ((object).*(ptrToMember))

これで pointers-to-member-function の配列は簡単になりました。

FredMemFn a = { &Fred::f, &Fred::g, &Fred::h, &Fred::i };

そして、メンバー関数へのポインタの使い方は簡単です:

void userCode(Fred& fred, int memFnNum){ // Assume memFnNum is between 0 and 3 inclusive: std::invoke(a, fred, 'x', 3.14);}

あるいは std::invoke がなければ

void userCode(Fred& fred, int memFnNum){ // Assume memFnNum is between 0 and 3 inclusive: CALL_MEMBER_FN(fred, a) ('x', 3.14);}

Note: #define マクロは 4 つの異なる方法で邪道です: evil#1、 evil#2、 evil#3 そして evil#4 です。 しかし、それでも時には有用です。

How do I declare a pointer-to-member-function that pointing to a const member function?

Short answer: typedef を使って member-function-pointertype を宣言したら、 ) の右側に const を追加してください。

たとえば、Fred::f, Fred::g または Fred::h を指すメンバー関数へのポインタが必要だとします:

class Fred {public: int f(int i) const; int g(int i) const; int h(int j) const; // ...};

次に、typedef を使用してメンバー関数-ポインタ型を宣言すると、このようになります:

以上です!

そして、通常のようにメンバー関数ポインターを宣言/通過/返却できます。

.* と ->* 演算子の違いは何ですか。

std::invoke またはメンバー関数ポインター呼び出しのマクロを使っていれば、これを理解する必要はないでしょう。 Ohyeaさん、この場合はstd::invokeかマクロを使用してください。 そして、この場合は std::invoke かマクロを使うべきだと言いましたか?!!

たとえば:

BUT please consider a std::invoke or macro instead:

void sample(Fred x, Fred& y, Fred* z, FredMemFn func){ std::invoke(func, x, 42, 3.14); std::invoke(func, y, 42, 3.14); std::invoke(func, *z, 42, 3.14);}

or

先に述べたように、実際の呼び出しはしばしばここで言うシンプルネスよりもはるかに複雑なので、通常 std::invoke かマクロを使ってコードの書きやすさや可読性を改善することになるのです。

Can I convert a pointer-to-member-function to a void*?

No!

技術的詳細:メンバー関数へのポインタとデータへのポインタは必ずしも同じように表現されるわけではありません。 メンバ関数へのポインタは、単一のポインタではなく、データ構造である可能性があります。 仮想関数を指している場合、実際には静的に解決可能なコードの山を指していないかもしれないので、通常のアドレスでない可能性もあります – ある種の異なるデータ構造であるかもしれません。 私は気にしません。 これは違法です、以上。

Can I convert a pointer-to-function to a void*?

No!

Technical details.It’s illegal, period: void* ポインタはデータへのポインタであり、関数ポインタは関数を指します。 この言語は、関数とデータが同じアドレス空間にあることを必要としないため、例として、また限定するものではありませんが、異なるアドレス空間にそれらを持つアーキテクチャでは、2 つの異なるポインター タイプを比較することはできません。 私は気にしません。 それは違法です、以上。

関数ポインタのようなものが必要ですが、より柔軟でスレッドセーフなものが必要です。 ファンクションロイドは関数よりも厳密に強力で、その余分なパワーは、関数ポインタを使用するときに一般的に直面する課題のいくつか (すべてではありません) を解決します。

では、従来の関数ポインタの使用例を示し、その例をファンクションロイドに翻訳してみましょう。

int funct1( /*...params...*/ ) { /*...code...*/ }int funct2( /*...params...*/ ) { /*...code...*/ }int funct3( /*...params...*/ ) { /*...code...*/ }

そして、関数ポインタでそれらにアクセスします。

typedef int(*FunctPtr)( /*...params...*/ );void myCode(FunctPtr f){ // ... f( /*...args-go-here...*/ ); // ...}

時には、これらの関数ポインタの配列を作成することもあります。

array( /*...args-go-here...*/ );

functionoids では、まず純粋仮想メソッドを持つ基底クラスを作成します。

それから 3 つの関数の代わりに、3 つの派生クラスを作成します。 単に FunctPtr という typedef を作成して、残りのコードを昔ながらのアプローチと同じようにします:

typedef Funct* FunctPtr;void myCode(FunctPtr f){ // ... f->doit( /*...args-go-here...*/ ); // ...}

ほぼ同じ方法で配列を作成することができます。

このことから、ファンクションロイドがファンクションポインタよりも厳密に強力である最初のヒントが得られます。ファンクションポインタバージョンがそうでないのに対し、ファンクションロイドアプローチには ctor に渡すことができる引数があるという事実(上記では ..ctor-args… として示されています)です。 functionoidオブジェクトは、フリーズドライされた関数呼び出し(呼び出しという言葉を強調してください)と考えてみてください。 関数へのポインタとは異なり、ファンクションロイドは部分的に呼び出された関数へのポインタです(概念的には)。 関数に引数を渡し、その(部分的に完成した)呼び出しをフリーズドライすることができる技術を想像してみてください。 その技術によって、そのフリーズドライされた部分的に完了した関数呼び出しへの魔法のポインタのようなものが返されるとします。 そして、そのポインタを使って残りの引数を渡すと、システムは魔法のように(フリーズドライされた)元の引数を受け取り、それをフリーズドライされる前に関数が計算したローカル変数と組み合わせ、そのすべてを新しく渡された引数と組み合わせ、フリーズドライされたときに中断したところから関数の実行を継続するのです。 SFのように聞こえるかもしれませんが、概念的にはファンクションロイドで可能なことなのです。 しかも、そのフリーズドライされた関数を、さまざまな「残りのパラメータ」で、何度でも「完成」させることができるのです。

地に足をつけて、いくつかの例を使って、これらの難解な事柄が実際に何を意味しているのかを説明しましょう。

int funct1(int x, float y){ /*...code...*/ }int funct2(int x, const std::string& y, int z){ /*...code...*/ }int funct3(int x, const std::vector<double>& y){ /*...code...*/ }

パラメータが異なる場合、呼び出し側はどのパラメータを渡すべきかわからないため、昔ながらの関数ポインタのアプローチは困難です (呼び出し側は単に関数へのポインタを持っているだけで、関数名やパラメータが異なる場合、そのパラメータの数やタイプはわかりません) (このことについて私にメールを書かないでください; できるでしょうが、あなたは頭の上で立って面倒なことをしなければなりません; そのことについて私に書かないで – 代わりに関数オブジェクトを使ってください)。

ファンクションロイドを使えば、少なくとも時には状況はずっとよくなります。 functionoid は無風状態の関数呼び出しと考えることができるので、私が yz と呼んだような、一般的でない引数を取って、対応する ctor に引数として与えるだけでいいのです。 共通引数(この場合はxというint)をctorに渡すこともできますが、その必要はなく、代わりに純粋仮想のdoit()メソッドに渡すオプションがあります。 ここでは、xdoit()に、yzをctorに渡すと仮定します。

したがって、ユーザーがこれらのファンクションロイドの 1 つで doit() を呼び出すとき、ユーザーは「残りの」引数を提供し、呼び出しは概念的に ctor に渡された元の引数と doit() メソッドに渡された引数を結合することになるのです。

array->doit(12);

すでに述べたように、ファンクションロイドの利点の 1 つは、たとえば Funct1 のインスタンスを配列内に複数持つことができ、それらのインスタンスに異なるパラメータをフリーズドライすることができることです。 例えば、arrayarray はどちらも Funct1 型ですが、array->doit(12) の動作は doit() に渡された 12 と ctors に渡された引数の両方に依存するので異なります。

Functionoids の別の利点は、例を Functionoids の配列からローカル Functionoid に変更すると明らかになります。 舞台を整えるために、昔ながらの関数ポインタのアプローチに戻り、sort() または binarySearch() ルーチンに比較関数を渡そうとするところを想像してみましょう。 sort() または binarySearch() ルーチンは childRoutine() と呼ばれ、比較関数ポインタのタイプは FunctPtr:

void childRoutine(FunctPtr f){ // ... f( /*...args...*/ ); // ...}

で、呼び出し側によって、最適だと思うものに応じて異なる関数ポインタを渡すことになります。

void myCaller(){ // ... childRoutine(funct1); // ...}void yourCaller(){ // ... childRoutine(funct3); // ...}

この例を functionoids を使ったものに簡単に変換できます。

この例を背景にして、functionoids が function-pointers よりも優れている点が 2 つあります。 上記の「CTOR の引数」の利点と、ファンクションロイドはスレッドセーフな方法で呼び出し間の状態を維持できるという事実です。 しかし、静的データは本質的にスレッドセーフではなく、静的データはすべてのスレッド間で共有されます。 functionoidのアプローチは、コードがスレッドローカルデータで終わるので、本質的にスレッドセーフである何かを提供します。 実装は簡単で、昔ながらの静的データをファンクショノイドの this オブジェクト内のインスタンスデータメンバーに変更します。そして、パッと見て、データはスレッドローカルであるだけでなく、再帰呼び出しでも安全です。 スレッドグローバルデータが必要な場合、ファンクションロイドは若さを与えることもできます。ファンクションロイドのthisオブジェクト内のインスタンスデータメンバーからファンクションロイドのクラス内の静的データメンバー、またはローカルスコープの静的データに変更するだけです。 5473>

Functionoid のアプローチでは、昔ながらのアプローチでは利用できない 3 つ目のオプションがあります:Functionoid は、呼び出し側がスレッドローカルまたはスレッドグローバルデータを必要とするかどうかを決定することができます。 スレッドグローバルデータを必要とする場合には、ロックを使用する責任がありますが、少なくとも選択することができるのです。 簡単です。

Functionoid は柔軟なソフトウェアを作るときに遭遇するすべての問題を解決するわけではありませんが、関数ポインタよりも厳密により強力で、少なくとも評価する価値があります。 実際、ファンクションロイドがファンクションポインタより強力でないことは簡単に証明できます。なぜなら、ファンクションポインタの昔ながらのアプローチは、グローバルな (!) ファンクションロイドオブジェクトを持つことと等しいと想像できるためです。 グローバルなファンクションロイドオブジェクトを作ることはいつでもできるので、何の損失もありません。 QED.

ファンクションロイドを通常の関数呼び出しより速く作ることはできますか。

はい。

小さなファンクションロイドがある場合、現実世界ではよくありますが、関数呼び出しのコストはファンクションロイドが行う仕事のコストと比較して高くなることがあります。 以前のFAQでは、ファンクションロイドは仮想関数を用いて実装されており、通常、関数呼び出しのコストがかかります。 別のアプローチでは、テンプレートを使用します。

次の例は、以前の FAQ にあるものと精神的に似ています。 呼び出し側のコードの読みやすさを改善し、誰かが通常の関数ポインタを渡すことができるように、doit()operator()() にリネームしました。

このアプローチと以前の FAQ のものの違いは、fuctionoid が実行時ではなく、コンパイル時に呼び出し側に「束縛」されるということです。 もし、最終的に渡したいファンクションロイドの種類がコンパイル時にわかっていれば、上記のテクニックを使うことができ、少なくとも典型的なケースでは、コンパイラが呼び出し側でファンクションロイドコードをインライン展開することにより、スピードの恩恵を受けることができます。 以下はその例です。

template <typename FunctObj>void myCode(FunctObj f){ // ... f( /*...args-go-here...*/ ); // ...}

コンパイラーが上記をコンパイルするとき、呼び出しがインライン展開され、パフォーマンスが向上する可能性があります。

上記を呼び出す1つの方法です:

void blah(){ // ... Funct2 x("functionoids are powerful", 42); myCode(x); // ...}

余談: 上記の最初の段落で示唆されたように、通常の関数名を渡すこともできます (呼び出し側がこれらを使用すると、関数呼び出しのコストが発生するかもしれませんが):

void myNormalFunction(int x);void blah(){ // ... myCode(myNormalFunction); // ...}

functionoidとファンクタの違いは何ですか:

functionoidとは一つの主要メソッドを持ったオブジェクトのことです。 基本的には、printf() のような C ライクな関数を OO 拡張したものです。 関数が複数のエントリポイント (すなわち、複数の「メソッド」) を持ち、かつ、スレッドセーフな方法で呼び出し間の状態を維持する必要がある場合、ファンクショノイドを使用します (呼び出し間の状態を維持する C スタイルのアプローチは、関数にローカル「静的」変数を加えることですが、それはマルチスレッドの環境では恐ろしく安全とは言えないのです)。 それは関数呼び出し演算子をオーバーロードするので、コードは関数呼び出しと同じ構文を使用して、その主要なメソッドを呼び出すことができます。 例えば、”foo “がファンクタであれば、”foo “オブジェクトの “operator()() “メソッドを呼び出すには、”foo() “と言えばいい。 この利点はテンプレートにあります。テンプレートは関数として使用されるテンプレート・パラメータを持つことができ、このパラメータは関数名またはファンクタ・オブジェクトのいずれかにすることができるからです。 operator()()」メソッドがインライン化できるので、ファンクター・オブジェクトであることによるパフォーマンス上の利点があります (一方、関数のアドレスを渡す場合、それは必然的に非インライン化でなければなりません)。 C では、比較関数は常にポインターで渡されます (例, へのシグネチャを参照)、C++ では、パラメーターは関数へのアポインターまたはファンクター オブジェクトの名前のいずれかとして渡すことができ、その結果、C++ のソートされたコンテナーは、場合によっては、C の同等品よりもずっと速く(そして決して遅く)なることがあります。 通常は大したことではありませんが、C++ では、非常に高いパフォーマンスのコードを可能にしたいのです。 つまり、C++ は「使用する場合にのみ代金を支払う」という哲学を持っており、言語は物理的なマシンが実行できる以上のオーバーヘッドを恣意的に課してはなりません (もちろん、プログラマはオプションとして、一般的に柔軟性やその他の「能力」と引き換えに何らかのオーバーヘッドを課すことになる動的バインディングなどの技術を使用できますが、このような構造の利点 (とコスト) を必要とするかどうかは、設計者とプログラマ次第です。)

Leave a Reply