C ++ 98/03のis_function メタ関数の別の実装

これは、C ++ 98/03のis_functionメタ関数をどのように記述したかについての小さなレポートです。そのため、異なる数の引数に対して多くの特殊化を作成する必要はありません。

2016年にそのようなことをするのはなぜですか? 答えます。 これは挑戦です。 とりわけ、これは、「可能かどうか」というカテゴリーからの純粋に理論的な最初の研究であり、現代のコンパイラーの問題を明らかにしました。 この気分をぜひご覧ください。



ステージング

理論のビット

実装番号0

実施番号1

実装番号2

結論の代わりに



ステージング



is_function<T>



は、タイプT



が「関数」のタイプでtrue



場合はtrue



返し、 true



ない場合はtrue



返しtrue



。 そのため、調査中の関数型のさまざまな数の引数型に対して多数の特殊化を使用せずにメタ関数を記述する必要があります。 boostに特化した実装例を見ることができます。 現時点では、最大25個の引数があり、このすべてに多くのスペースが必要です。

なぜ専門化するのですか? C ++ 11では、すばらしいツールが登場しました- 可変長テンプレート 。 それは熟していたために現れた、と言われるかもしれませんが、ペント。 このツールを使用すると、テンプレート引数のこれらのシーケンスを処理できます

  some_template<A1, A2, A3 /*, etc. */>
      
      



パラメータの単一の「パッケージ」、パラメータパックとして。 パラメータパックは、引数の数が不明な場合に、一般化された特殊化、オーバーロード、および置換を行うのに役立ちます。 is_functionがC ++ 11で実装されるのはこのツールです。 C ++ 98/03では、そのようなツールはありませんでした。 つまり、一般的なケースでは、状況に応じて異なる数の引数を提供する必要がある場合、「すべての場合」にオーバーロードと特殊化を行う必要がありました。 boostのvariantmplなどのライブラリの実装を見る場合、そのようなコードが豊富であることを確認してください(プリプロセッサによって生成されることもあります)。 タイプT



R(A1, A2)



関数でT



かどうかを判断する必要がある場合、最も簡単で明白な解決策は、対応する特殊化を作成することでした。

  template <typename F> struct is_function { /* .... */ }; template <typename R, typename A1, typename A2> struct is_function<R(A1, A2)> { /* .... */ };
      
      



多くの場合、異なる方法でそれを行うことは単に不可能でした。 ブーストでの実装に不満があるとは思わないでください-それらのソリューションは最も移植性が高く、したがって、特にそのようなライブラリのコンテキストで最も正確です。 しかし、手近な仕事でこれなしでやることは私にとって興味深いことでした。

一般的に、私はサービスのおかげで不幸な人の一人です(これは見た目ですが)が、私はまだC ++ 03で作業しています。 したがって、古いコードとの互換性に関する私の懸念は、驚くことではありません。 誰かが言うかもしれません:「はい、このジャンクはあなたに与えられました、2016年の庭で!」。 これに同意することはできますが、純粋に主観的な競争感覚に加えて、これからいくつかの利点を引き出すことが判明しました。 そして、C ++ 03の精神は、上記の理由により、まだ風化することができませんでした。 したがって、楽しみのためだけに。

説明に移る前に、読者がSFINAEとは何か、コンパイル時チェックを記述する基本原則を理解する必要があることを警告したいと思います。標準からの表現を正確に翻訳しません。 興味のある読者は、望むなら、自分でこれに対処できると思います。



理論のビット



特殊化を伴う古典的なアプローチが私たちに合わない場合、それでは何ですか? 少し違った考え方をしてみましょう。 他にはない関数型のプロパティは何ですか? 標準(C ++ 03)を見てみましょう。

4.3 / 1

関数型Tの左辺値は、型「Tへのポインター」の右辺値に変換できます 。結果は、関数へのポインターです。
そのため、関数は暗黙的に関数へのポインターに変換できます。 配列には同じプロパティがあります。配列は暗黙的にポインタに変換されます。 他に何があるか見てみましょう:

8.3.5 / 3

各パラメーターの型を決定した後、「Tの配列」または「Tを返す関数型のパラメーターは それぞれ「Tへのポインター 」または「Tを返す関数へのポインター 」に調整されます
これは、「関数」タイプが別の関数のパラメーターとして指定された場合、暗黙的にポインターのプロパティを取得することを意味します。 これに基づいて、パラグラフ13.1では、そのような宣言は

  void foo(int ());
      
      



これに対応します(つまり、まったく同じです):

  void foo(int (*)());
      
      



これはすでに使用できます。 これに基づいてチェックを記述し、目の前の機能を判断できます。 ただし、すべては見た目ほど単純ではありませんが、後でさらに詳しく説明します。 それまでの間、「関数」のタイプに基づいて、他に使用できるものを見てみましょう。

8.3.4 / 1

Dが次の形式である宣言TD

D1 [定数式opt ]

宣言T D1の識別子の型は「派生宣言子型リストT」であり、Dの識別子の型は配列型です。 Tは配列要素型と呼ばれます。 この型は、参照型、(おそらくcv修飾された)型void、関数型、または抽象クラス型であってはなりません。
ええ、それも面白いです。 つまり 「関数」型の要素の配列を取得することはできません。 これに加えて、リンクの配列、 void



配列、および抽象クラスの型の要素を持つ配列を取得することはできません。 残りのオプションを遮断するチェックを記述すると、目の前の機能を正確に判断できます。

まとめます。 関数には2つの特徴があります。 タイプ「機能」

これらのプロパティを計画の実装に使用します。



実装番号0、予備



前のセクションで特定した「関数」タイプの最初の機能から始めたいと思います。 約8.3.5 / 3です。

次のように要約できます。チェックされた型F



は、等式の場合に関数です。

void( F ) == void( F * )





それはすべて非常に簡単に聞こえます。 また、最初の実装も非常に簡単でした。 シンプルだが間違っている。 このため、全体を説明することはしませんが、その中で使用した1つのプロパティについて個別に話したいと思います。 このコードを検討してください。

  template <typename F> static void (* declfunc() )( F ); template <typename F> static void (* gen( void (F *) ) )( F ); template <typename F> static void (* gen( void (F ) ) )( F * );
      
      



将来を見据えて、このコードは誤った仮定に由来すると言うでしょう。 しかし、Clangコンパイラー(バージョン3.4まで)、GCCコンパイラー(バージョン4.9まで)、VSのコンパイラー(cl 19.x、おそらく以前)は、予想どおりにコンパイルしました。 どのように機能し、どのように使用する予定だったかを説明します。 まず、チェックに役立つ関数宣言を作成します。

  template <typename X> static char (& check_is_function( X ) ) [ is_same<void(*)( F ), X>::value + 1 ];
      
      



check_is_functionに渡される型がvoid(*)( F )



に一致する場合、関数は2つのchar



配列への参照を返します。一致しない場合は、1つのchar



から派生しvoid(*)( F )



sizeof



を使用して戻り値の型を分析できます)。 ここでは、 F



がタイプ「関数」に属することを調査中のタイプであることを受け入れます。 さて、これをシンプルなテンプレートに入れると、

  template <typename F> struct is_function { template <typename X> static char (& check_is_function( X ) ) [ is_same<void(*)( F ), X>::value + 1 ]; enum { value = sizeof( check_is_function( gen( declfunc<F>() ) ) ) - 1 }; };
      
      



上記のコンパイラで次の形式の式を確認できます。

  is_function<int()>::value; is_function<int>::value; typedef void fcv() const; is_function<fcv>::value;
      
      



結果1、0、および1をそれぞれ取得します(完全なコードはここにあり、 ここで実行できます )。 はい、これは完全に機能するソリューションではありません。ここでは、関数ポインタと関数を区別していません。リンク、 void



などに問題があります。 しかし、これはすべてバイパスされているので、これに注意を集中したくありません。 指定したコンパイラ(GCC> = 4.9、Clang> = 3.5、cl 19.x)よりも新しいコンパイラで同じ例を実行する場合は、出力が変更されていることを確認してください。 結果1、0、0をそれぞれ取得します。 これは、 cv-qualifier-seq (これは最後に同じconst



またはvolatile



)を持つ関数の型(別の関数の型で(ポインタープロパティを取得中に)置換される)、バリアントの有効な引数置換でなくなったために発生します。

  template <typename F> static void (* gen( void (F *) ) )( F );
      
      



なんで? (最終ドラフト)と明確に述べられている新しい標準の導入により、

8.3.1 / 4

関数型にcv修飾子またはref修飾子がある場合、関数型へのポインターの形成は不正です。
同様のコードに対するコンパイラのアプローチが変更されました。 このタイプにアスタリスクを追加するのは間違った置換であるため、コードは機能しなくなりました。 C ++ 03では、同様に明確なルールはありませんでした(変更については、 こちらを参照してください )。 もちろん、それはそこで許可されていたという意味ではありません。 ただし、標準の文言の曖昧さにより、この点をスキップする機会が残りました。これは以下のリンクに記載されているものです。

cv-qualifiersまたはref-qualifiersを含む関数型へのポインターと参照が許可されていないため、テンプレート引数の置換中に作成された場合、推論に失敗することを既存の文言から十分に明らかにしていません
したがって、多くの最新のコンパイラーは、これをまだ考慮していません(たとえば、cl 18.xまたはicc 13.xおよび14.x)。 「 cv-qualifier-seqを使用した関数」の型にアスタリスクを明示的に追加することが許可されない場合、この型をパラメーターとして指定する際に暗黙的なものも使用できないはずだと、すでに注意深い読者はおそらく疑問に思っています。 はい、おそらくそうです。 ただし、現時点では、明示的に禁止する単一のコンパイラはありません。

C ++ 03標準には次のものがあります。

8.3.5 / 4

cv-qualifier-seqは、非静的メンバー関数の関数型、メンバーへのポインターが参照する関数型、または関数typedef宣言の最上位関数型の一部にのみなります。
これは、 cv-qualifier-seqで関数型を使用するかなり狭いコンテキストについて教えてくれます。 そして、私たちのケースはそこに収まらないようです。

したがって、関数型のポインターへの「変異」に基づいて構築されたコードはすべての場合に機能するわけではありませんが、現時点ではまだ機能するため、完全なソリューションを示します。 これは、世界の不完全さについての思考の糧になると思います。



実装番号1、作業中



この実装は、前のセクションで説明した制限を考慮して最終化されました。 ほとんどの場合(100%確信はありませんが、これは多くを示します)、この実装は標準に完全に準拠しておらず、それでも機能するという事実は、少なくとも3つの最新のコンパイラーをサポートするバグレポートを送信する理由として役立つはずです。

前の実装の主な問題は、 cv-qualifier-seqを使用した関数の場合、ポインターによる置換が機能しなくなったことです。 幸いなことに、それを解決する鍵はこの問題に隠されています。 渡された型へのポインターを代用できるかどうかを判断するSFINAEチェックを作成できます。 したがって、これが不可能な場合はオプションを切り捨てます。 チェックは次のようになります。

  template <typename F> struct may_add_ptr { template <typename P> static char (& may_add_ptr_check(P *) )[2]; template <typename P> static char (& may_add_ptr_check(...) )[1]; enum { value = sizeof( may_add_ptr_check<F>(0) ) - 1 }; };
      
      



置換P*



が正しくない場合、省略記号付きのオーバーロードが選択されます。 選択したオーバーロードに応じて、戻り値からsizeof



は1または2を返します。1を引くと、値0または1が得られます。タイプへのポインターを置き換えることができる場合は1が得られ、不可能な場合は0が得られます(これは後で使用します同じように)。 これで、このチェックに基づいて関数へのポインターの型を作成する機会が得られました。 私たちはさまざまな方法で行動することができます-過負荷または専門化に基づいて。 オーバーロードに基づいた方法を示します よりポータブルです。

  template <typename F> static typename enable_if< may_add_ptr<F>::value == 1, void (*)(typename remove_reference<F>::type *) >::type declfunc(); template <typename F> static typename enable_if< may_add_ptr<F>::value == 0, void (*)(typename remove_reference<F>::type ) >::type declfunc();
      
      



そのため、型type-パラメータが別の型である関数へのポインタを形成しました。 これは、タイプ「関数」に属するために調査しているタイプです。 だから

declfunc<F>() == void(*)( F )





次に、タイプF



は関数です。 リンクの削除( remove_reference



)が必要です。この場合、次の場合に不等式が自動的に取得されます。

F = R(&)(Args)



またはF = T &





なぜなら すべての置換の後、次のタイプが比較されます。

void(*)( R(*)(Args) )



およびvoid(*)( R(&)(Args) )





または

void(*)( T )



およびvoid(*)( T & )





それに応じて。 これらのタイプは明らかに一致しません。これが必要なものです。

F



R(Args)



形式の関数である場合、それらは比較されます

void(*)( R(*)(Args) )



およびvoid(*)( R(Args) )





上記の標準の規定( 8.3.5 / 3 )に基づいて、これらのタイプは同等です。

F



R(Args) const



形式の関数である場合、それらは比較されます

void(*)( R(Args) const )



およびvoid(*)( R(Args) const )





これらのタイプも等しく、必要なものです。

F = T



(関数ではない)の場合、それらは比較されます

void(*)( T * )



およびvoid(*)( T )





これらのタイプは等しくありません。これが必要なものです。

次に、実際にタイプを比較する必要があります。 1つありますが、 is_same



では通常のis_same



チェックを使用できません。 is_function



引数も抽象型である場合があり、このコンテキストで使用するとコンパイルエラーが発生します。 したがって、 is_same



を次の意味のSFINAEチェックに置き換えます。

  template <typename F> static char (& is_function_check( void( F ) ) )[2]; template <typename F> static char (& is_function_check( ... ) )[1];
      
      



次のいずれかを使用します。

  value = sizeof( is_function_check<Tp>( declfunc<Tp>() ) ) - 1;
      
      



完全なコードは次のようになります。
  template <typename Tp> struct is_function { private: template <typename F> struct may_add_ptr { template <typename X> static char (& may_add_ptr_check(X *) )[2]; template <typename X> static char (& may_add_ptr_check(...) )[1]; enum { value = sizeof( may_add_ptr_check<F>(0) ) - 1 }; }; template <typename F> static typename enable_if< may_add_ptr<F>::value == 1, void (*)(typename remove_reference<F>::type *) >::type declfunc(); template <typename F> static typename enable_if< may_add_ptr<F>::value == 0, void (*)(typename remove_reference<F>::type ) >::type declfunc(); template <typename F> static char (& is_function_check( void( F ) ) )[2]; template <typename F> static char (& is_function_check( ... ) )[1]; public: enum { value = sizeof( is_function_check<Tp>( declfunc<Tp>() ) ) - 1 }; };
      
      





このテンプレートが実際に機能することを確認するために、テストマクロを作成しましょう。

  #define TEST_IS_FUNCTION(Type, R) \ std::cout << ((::is_function<Type>::value == R) ? "[SUCCESS]" : "[FAILED]") \ << " Test is_function<" #Type "> (should be [" #R "]):" \ << std::boolalpha \ << (bool)::is_function<Type>::value << std::endl
      
      



そして、次の実行
テストスイート。
  struct S { virtual void f() = 0; }; int main() { typedef void f1() const; typedef void f2() volatile; typedef void f3() const volatile; TEST_IS_FUNCTION(void(int), true); TEST_IS_FUNCTION(void(), true); TEST_IS_FUNCTION(f1, true); TEST_IS_FUNCTION(void(*)(int), false); TEST_IS_FUNCTION(void(&)(int), false); TEST_IS_FUNCTION(f2, true); TEST_IS_FUNCTION(f3, true); TEST_IS_FUNCTION(void(S::*)(), false); TEST_IS_FUNCTION(void(S::*)() const, false); TEST_IS_FUNCTION(S, false); TEST_IS_FUNCTION(int, false); TEST_IS_FUNCTION(int *, false); TEST_IS_FUNCTION(int [], false); TEST_IS_FUNCTION(int [2], false); TEST_IS_FUNCTION(int **, false); TEST_IS_FUNCTION(double, false); TEST_IS_FUNCTION(int *[], false); TEST_IS_FUNCTION(int &, false); TEST_IS_FUNCTION(int const &, false); TEST_IS_FUNCTION(void(...), true); TEST_IS_FUNCTION(int S::*, false); TEST_IS_FUNCTION(void, false); TEST_IS_FUNCTION(void const, false); }
      
      





ここで、サンプルとテストの完全なコードを確認し、 ここで実行できます 。 ところで、このコードはC ++ 11でも機能します。 GCC 4.4.x-6.0、Clang 3.0-3.9、VS 2013およびVS 2015でテストされました。cv -qualifier-seqで F



へのポインタを追加することを検討しているコンパイラがあります(icc 13.xなど)。 これらのコンパイラでは検証は機能しません。



規格に準拠した実装No.2



8.3.4 / 1を思い出してください。 関数は、配列を作成できない数少ないタイプの1つであると述べました。 前の方法はすべて明確ではないので、ここでもっと幸運が待っているのではないでしょうか? 作成できない型の配列をもう一度リストします。

  1. リンク
  2. void



  3. 抽象クラス
  4. 機能
したがって、タスクを2つの段階に分けることができます。 同様の動作を持つ他のタイプを除外し、指定されたタイプの配列を作成できるかどうかを決定するSFINAEチェックを記述します。 最初に、抽象クラスを除外します。 すべてのクラスを一度に取り除く最も簡単な方法ですが。 これを行うには、メタ機能が必要です。

  template <typename Tp> struct is_class;
      
      



ここで、リンクとvoidを考慮から除外する必要があります。 これには、次の2つのテンプレートを使用します。

  template <typename Tp> struct is_lvalue_reference; template <typename Tp> struct is_void;
      
      



それはすべてのようですが、何かが欠けています。 実際、配列の要素になれない別の型があります-それは未知の境界の配列(未知のサイズT[]



配列)です。 また、それを取り除く必要があります。 原則として、一度にすべてのアレイを苦労してふるいにかけることはできません。

  template <typename Tp> struct is_array;
      
      



これらのメタ関数の実装は、 ここで見つけることができます。たとえば、boostから取得することもできます。

次に、メインテンプレートを作成します。

  template <typename Tp> struct is_function { private: template <typename F> static char (& check_is_function( ... ) )[2]; template <typename F> static char (& check_is_function( F (*)[1] ) )[1]; public: enum { value = !is_class<Tp>::value && !is_void<Tp>::value && !is_lvalue_reference<Tp>::value && !is_array<Tp>::value && (sizeof( check_is_function<Tp>(0) ) - 1) }; };
      
      



「配列へのポインタ」型の定義を介して、型が配列の要素になり得るかどうかを確認します。型の要素は、 check_is_function



関数のパラメーターとしてテストされます。 置換が失敗した場合、タイプF



は関数です。

テストとして、 実装1から以前のセットを取得します。 完全なコードはここで表示でき、 ここで実行できます 。 この実装は標準に完全に準拠しており、ほとんどのコンパイラで動作する可能性が最も高くなります。 このコードはC ++ 11でも機能します。右辺値リンクを追加で除外するだけです。



結論の代わりに



1) cv-qualifier-seqからポインターへの関数の不正なプロモーションに関する3つのバグレポートを送信しました。

Clangのサポート

GCCをサポートしています。

VSのサポート。

既に述べたように、100%確信はありませんが、これがこの問題に関する開発者の意見を得る唯一の方法です。



2)私のgithubにある完全なコードは、記事に記載されているコードとは多少異なります。 ここでは、いくつかの詳細とマナーが意図的に省略されました。



2.1)cv-qialifier-seqを使用して関数のタイプをより詳細に説明するように依頼されました。
この型を使用して空き関数を宣言できないことは明らかです。 cv-qialifier-seqはこれを指します(9.3.1 / 3を参照)。

それではどのような状況で機能しますか? 標準では、次の場合にこれを許可しています。

8.3.5 / 7

宣言子にcv-qualifier-seqが含まれる関数型のtypedefが使用されます。

非静的メンバー関数の関数型を宣言するだけで、

メンバーへのポインタが参照するか、別の関数typedef宣言のトップレベルの関数型を宣言します。

[例:
 typedef int FIC(int) const; FIC f; // ill-formed: does not declare a member function struct S { FIC f; // OK }; FIC S::*pm = &S::f; // OK
      
      



—終わりの例]
つまり cv-qualifier-seqで関数型のtypedefを使用することができます:
  • メンバー関数を宣言する
  • メンバー関数へのポインターを形成し、
  • 別の関数型のtypedef宣言で最上位型として使用します。
後者のケースはかなり霧がかかっています(別の機能の意味は?)。 比較のために、最後のドラフトから引用します。

8.3.5 / 6

cv-qualifier-seqまたはref-qualifier(typedef-name(7.1.3、14.1)で指定された型を含む)を持つ関数型は、次のようにのみ表示されます:

(6.1)-非静的メンバー関数の関数タイプ、

(6.2)-メンバーへのポインターが参照する関数型、

(6.3)-関数typedef宣言またはエイリアス宣言のトップレベル関数型、

(6.4)-型パラメーターのデフォルト引数の型ID(14.1)、または

(6.5)-型パラメーター(14.3.1)のテンプレート引数の型ID。

[例:
 typedef int FIC(int) const; FIC f; // ill-formed: does not declare a member function struct S { FIC f; // OK }; FIC S::*pm = &S::f; // OK
      
      



-終了例]
同意します。これははるかに理解しやすいです(6.3に注意してください。トップレベル関数のtypedef



cv-qualifier-seqの使用を合法化し、他の関数のみではありません)。 このタイプを関数パラメーターとして使用できるようになったという事実は、私が作成したバグレポートの主題です。 さらに、この場合、これらのタイプはポインターに進められ、これも禁止されます( 8.3.1 / 4 )。

ここで変更を比較します 。 また、誰かがこの資料に興味を持つ可能性もあります。



3)ご清聴ありがとうございました:)




All Articles