CallVoidMethod
は可変、つまり JNI環境へのポインター、作成されるオブジェクトの型へのポインター、および呼び出されたメソッド(この場合はコンストラクター)の識別子に加えて、任意の数の他の引数を取ります。 論理的です、なぜなら これらの他の引数は、Java側の呼び出されたメソッドに渡されます。メソッドは、任意のタイプの異なる数の引数で異なることができます。
したがって、ラッパー変数も作成しました。
CallVoidMethod
任意の数の引数を
CallVoidMethod
使用し
va_list
。この場合は異なるためです。 はい、それは
va_list
送信したもの
CallVoidMethod
。 そして、JVMの平凡なセグメンテーションエラーを削除しました。
2時間で、8日から11日までいくつかのバージョンのJVMを試すことができました。これは、最初はこれがJVMの最初の経験であり、この問題では自分よりもStackOverflowを信頼し、次に誰かがStackOverflowでは、この場合、OpenJDKではなくOracleJDKを使用し、8ではなく10を使用するようにアドバイスしました。そして、変数
CallVoidMethod
に加えて、
CallVoidMethod
を介して任意の数の引数を取る
CallVoidMethod
があることに最後に気付き
va_list
。
この話で一番気に入らなかったのは、省略記号(ellipsis)と
va_list
違いにすぐに気付かなかったこと
va_list
。 そして気づいたので、根本的な違いが何であるかを自分で説明できませんでした。 そのため、省略記号、
va_list
、および(まだC ++について話しているため)変数テンプレートを扱う必要があります。
標準の省略記号とva_listについての説明
C ++標準では、その要件と標準Cの要件との違いのみを説明しています。違い自体については後で説明しますが、ここでは、標準Cの内容(C89以降)について簡単に説明します。
- 任意の数の引数を取る関数を宣言できます。 つまり 関数は、パラメーターよりも多くの引数を持つことができます。 これを行うには、パラメーターのリストが省略記号で終わる必要がありますが、少なくとも1つの固定パラメーター[C11 6.9.1 / 8]も存在する必要があります。
void foo(int parm1, int parm2, ...);
- 省略記号に対応する引数の数とタイプに関する情報は、関数自体には渡されません。 つまり 最後の名前付きパラメーター(上記の例では
parm2
) [C11 6.7.6.3/9]の後 。
- これらの引数にアクセスするには、
<stdarg.h>
ヘッダーで宣言されたva_list
型と4つのマクロ(C11標準の前に3つ)va_start
、va_arg
、va_end
およびva_copy
(C11で始まる) [C11 7.16]を使用する必要があります。
例えばint add(int count, ...) { int result = 0; va_list args; va_start(args, count); for (int i = 0; i < count; ++i) { result += va_arg(args, int); } va_end(args); return result; }
はい、関数は引数の数を知りません。 彼女はどういうわけかこの番号を渡す必要があります。 この場合、単一の名前付き引数を使用します(別の一般的なオプションは、execl
や0のように、最後の引数としてNULL
を渡すことです)。 - 最後の名前付き引数には、
register
ストレージクラスを含めることはできません。関数または配列にすることはできません。 それ以外の場合、未定義の動作[C11 7.16.1.4/4] 。 - さらに、最後に指定された引数とすべての名前のない引数に、「 デフォルト引数のプロモーション 」が適用されます ( デフォルト引数のプロモーション 。この概念のロシア語への適切な翻訳があれば、喜んで使用します)。 つまり、引数の型が
char
、short
(符号付きまたは符号なし)、またはfloat
場合、対応するパラメーターはint
、int
(符号付きまたは符号なし)、またはdouble
としてアクセスする必要があります。 それ以外の場合、未定義の動作[C11 7.16.1.1/2] 。 -
va_list
型については、<stdarg.h>
宣言されており、完全である(つまり、この型のオブジェクトのサイズがわかっている) [C11 7.16 / 3]とのみ言われています。
なんで? しかし、なぜなら!
Cには多くの型はありません。 標準で
va_list
宣言されている
va_list
、内部構造については何も述べられていないのはなぜですか?
関数への任意の数の引数を
va_list
を介して
va_list
ことができる場合、なぜ省略記号が必要なのですか? 今は「構文糖として」と言えますが、40年前には、糖の時間はなかったと確信しています。
フィリップジェームスプラウガーフィリップジェームスプラウガーは、 The Standard C library -1992-で、最初はCはPDP-11コンピューター専用に作成されたと述べています。 また、単純なポインター演算を使用して、関数のすべての引数をソートできました。 この問題は、Cの人気と他のアーキテクチャーへのコンパイラーの転送で発生しました。 ブライアン・カーニガンとデニス・リッチーによるCプログラミング言語の初版-1978-には次のように明記されています。
ちなみに、任意の数の引数の移植可能な関数を作成する許容可能な方法はありません。 呼び出された関数が、呼び出されたときに渡された引数の数を調べるための移植可能な方法はありません。 ...この本はprintf
、引数の任意の数の最も典型的なC言語関数、...は移植性がなく、各システムに実装する必要があります。
printf
について説明していますが、まだ
vprintf
がなく、タイプとマクロ
va_*
については言及していません。 これらはCプログラミング言語の第2版(1988年)に登場し、これは最初のC標準(C89、別名ANSI C)の開発委員会のメリットです。 委員会は、UNIX OSの移植性を高める目的でAndrew Koenigが作成した
<varargs.h>
基礎として、
<stdarg.h>
を標準に追加しました。 既存のコンパイラが新しい標準をサポートしやすくするために、
va_*
マクロをマクロとして残す
va_*
が決定されました。
現在、C89と
va_*
ファミリーの出現により、移植可能な変数関数を作成できるようになりました。 そして、このファミリーの内部構造はいまだに記述されておらず、要件もありませんが、その理由はすでに明らかです。
好奇心から、
<stdarg.h>
実装の例を見つけることができます。 たとえば、同じ「C標準ライブラリ」はBorland Turbo C ++の例を提供します 。
Borland Turbo C ++の<stdarg.h>
#ifndef _STADARG #define _STADARG #define _AUPBND 1 #define _ADNBND 1 typedef char* va_list #define va_arg(ap, T) \ (*(T*)(((ap) += _Bnd(T, _AUPBND)) - _Bnd(T, _ADNBND))) #define va_end(ap) \ (void)0 #define va_start(ap, A) \ (void)((ap) = (char*)&(A) + _Bnd(A, _AUPBND)) #define _Bnd(X, bnd) \ (sizeof(X) + (bnd) & ~(bnd)) #endif
AMD64用のはるかに新しいSystemV ABIは 、
va_list
このタイプを使用し
va_list
。
SystemV ABI AMD64のva_list
typedef struct { unsigned int gp_offset; unsigned int fp_offset; void *overflow_arg_area; void *reg_save_area; } va_list[1];
一般的に、型とマクロ
va_*
は、変数関数の引数をトラバースするための標準インターフェイスを提供し、歴史的な理由によるそれらの実装は、コンパイラ、ターゲットプラットフォーム、およびアーキテクチャに依存すると言うことができます。 さらに、Cでは、省略記号(つまり、一般的な変数関数)が
va_list
(つまり、ヘッダー
<stdarg.h>
)よりも前に登場しました。 また、
va_list
は省略記号を置き換えるために作成されたのではなく、開発者が移植可能な変数関数を作成できるようにするために作成されました。
C ++は主にCとの後方互換性を維持しているため、上記のすべてが適用されます。 しかし、機能もあります。
C ++の変数関数
WG21ワーキンググループは、C ++標準の開発に関与しています。 1989年に、新しく作成されたC89標準が基礎となり、C ++自体を記述するために徐々に変更されました。 1995年に、提案N0695が John Miccoから受け取られました。著者は、マクロ
va_*
制限を変更することを提案しました。
- なぜなら C ++では、Cとは異なり、変数の
register
アドレスを取得でき、変数関数の最後の名前付き引数にこのストレージクラスを含めることができます。
- なぜなら C ++で出現したリンクは、C変数関数の未記述の規則に違反します-パラメーターのサイズは宣言された型のサイズと一致する必要があります-最後の名前付き引数はリンクにできません。 それ以外の場合、あいまいな動作。
- なぜなら C ++では、「 デフォルトで引数の型を上げる 」という概念はありません。その後、フレーズ
パラメーター
に置き換える必要がありますparmN
が...で宣言されている場合、デフォルトの引数プロモーションの適用後に生じるタイプと互換性のないタイプの場合、動作は未定義ですパラメーター
parmN
が...で宣言されている場合、パラメーターがない引数を渡すときに生じる型と互換性のない型の場合、動作は未定義です
ただし、3つの変更すべてが採用されました[C ++ 98 18.7 / 3] 。 C ++に戻ると、少なくとも1つの名前付きパラメーター(この場合、他のパラメーターにはアクセスできませんが、後で詳しく説明します)を持つ変数関数の要件はなくなり、名前のない引数の有効なタイプのリストは、クラスメンバーおよびPODタイプへのポインターで補足されました。
C ++ 03標準では、変数関数に変更はありませんでした。 C ++ 11は、
std::nullptr_t
型の名前のない引数を
void*
に変換し始め、コンパイラーの裁量で、非自明なコンストラクターとデストラクターを持つ型をサポートできるようにしました[C ++ 11 5.2.2 / 7] 。 C ++ 14では、最後の名前付きパラメーターとして関数と配列を使用できるようになり[C ++ 14 18.10 / 3] 、C ++ 17では、ラムダによってキャプチャされる拡張パックと変数の使用が禁止されました[C ++ 17 21.10.1 / 1] 。
その結果、C ++は落とし穴にさまざまな機能を追加しました。 非自明なコンストラクタ/デストラクタで指定されていない型サポートのみが価値があります。 以下では、変数関数のすべての非自明な機能を1つのリストにまとめ、特定の例で補足しようとします。
変数関数を簡単かつ誤って使用する方法
- 昇格された型で最後の名前付き引数を宣言するのは間違っています。
char
、signed char
、unsigned char
、singed short
、unsigned short
またはfloat
標準に従った結果は、未定義の動作になります。
無効なコードvoid foo(float n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; va_end(va); }
手元にあるすべてのコンパイラ(gcc、clang、MSVC)のうち、 clangのみが警告を発行しました。
クラン警告./test.cpp:7:18: warning: passing an object that undergoes default argument promotion to 'va_start' has undefined behavior [-Wvarargs] va_start(va, n); ^
そして、すべての場合において、コンパイルされたコードは正しく動作しましたが、それを当てにするべきではありません。
正しいでしょうvoid foo(double n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; va_end(va); }
- 最後の名前付き引数を参照として宣言するのは正しくありません。 任意のリンク。 この場合の標準は、未定義の動作も保証します。
無効なコードvoid foo(int& n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; va_end(va); }
gcc 7.3.0は、単一のコメントなしでこのコードをコンパイルしました。 lang6.0.0は警告を発行しましたが、それでもコンパイルしました。
クラン警告./test.cpp:7:18: warning: passing an object of reference type to 'va_start' has undefined behavior [-Wvarargs] va_start(va, n); ^
どちらの場合も、プログラムは正しく機能しました(幸運なことに、あなたはそれに頼ることはできません)。 しかし、 MSVC 19.15.26730はそれ自体を際立たせています-コードのコンパイルを拒否しました。va_start
引数va_start
参照であってはなりva_start
ん。
MSVCからのエラーc:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.15.26726\include\vadefs.h(151): error C2338: va_start argument must not have reference type and must not be parenthesized
さて、正しいオプションは、たとえば次のようになりますvoid foo(int* n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; va_end(va); }
-
va_arg
type-char
、short
またはfloat
va_arg
上げるように要求するのは間違っています。
無効なコード#include <cstdarg> #include <iostream> void foo(int n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; std::cout << va_arg(va, float) << std::endl; std::cout << va_arg(va, int) << std::endl; va_end(va); } int main() { foo(0, 1, 2.0f, 3); return 0; }
ここでもっと面白いです。 コンパイル時のgccは、float
ではなくdouble
を使用する必要があるという警告を出します。このコードがまだ実行されている場合、プログラムはエラーで終了します。
GCC警告./test.cpp:9:15: warning: 'float' is promoted to 'double' when passed through '...' std::cout << va_arg(va, float) << std::endl; ^~~~~~ ./test.cpp:9:15: note: (so you should pass 'double' not 'float' to 'va_arg') ./test.cpp:9:15: note: if this code is reached, the program will abort
確かに、プログラムは無効な命令に関する苦情でクラッシュします。
ダンプ分析は、プログラムがSIGILLシグナルを受信したことを示しています。 また、va_list
の構造も示しています。 32ビットの場合、これは
va = 0xfffc6918 ""
つまりva_list
は単なるchar*
です。 64ビットの場合:
va = {{gp_offset = 16, fp_offset = 48, overflow_arg_area = 0x7ffef147e7e0, reg_save_area = 0x7ffef147e720}}
つまり SystemV ABI AMD64で説明されているとおりです。
コンパイル時のclangは、未定義の動作を警告し、float
をdouble
置き換えることを提案します。
クラン警告./test.cpp:9:26: warning: second argument to 'va_arg' is of promotable type 'float'; this va_arg has undefined behavior because arguments will be promoted to 'double' [-Wvarargs] std::cout << va_arg(va, float) << std::endl; ^~~~~
しかし、プログラムがクラッシュすることはなくなり、32ビットバージョンでは以下が生成されます。
1 0 1073741824
64ビット:
1 0 3
MSVCは、/Wall
あっても、警告なしでのみまったく同じ結果を生成します。
ここで、32ビットと64ビットの違いは、最初のケースではABIがすべての引数をスタック経由で呼び出された関数に渡し、2番目では最初の4つ(Windows)または6つ(Linux)の引数がプロセッサレジスタを通過し、残りがスタック[ wiki ]。 ただし、4つの引数ではなく19でfoo
を呼び出して同じ方法で出力すると、結果は同じになります。32ビットバージョンでは完全に混乱し、64ビットバージョンではすべてのfloat
がゼロになります。 つまり ポイントはもちろんABIですが、引数を渡すためにレジスタを使用することではありません。
まあ、もちろん、そうするためにvoid foo(int n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; std::cout << va_arg(va, double) << std::endl; std::cout << va_arg(va, int) << std::endl; va_end(va); }
- 名前のない引数として重要なコンストラクタまたはデストラクタを持つクラスのインスタンスを渡すことは正しくありません。 もちろん、このコードの運命が「今ここでコンパイルして実行する」以上にあなたを興奮させない限りは。
無効なコード#include <cstdarg> #include <iostream> struct Bar { Bar() { std::cout << "Bar default ctor" << std::endl; } Bar(const Bar&) { std::cout << "Bar copy ctor" << std::endl; } ~Bar() { std::cout << "Bar dtor" << std::endl; } }; struct Cafe { Cafe() { std::cout << "Cafe default ctor" << std::endl; } Cafe(const Cafe&) { std::cout << "Cafe copy ctor" << std::endl; } ~Cafe() { std::cout << "Cafe dtor" << std::endl; } }; void foo(int n, ...) { va_list va; va_start(va, n); std::cout << "Before va_arg" << std::endl; const auto b = va_arg(va, Bar); va_end(va); } int main() { Bar b; Cafe c; foo(1, b, c); return 0; }
再びClangがより厳格になりました。 彼va_arg
、va_arg
の2番目の引数がPODタイプでva_arg
ないため、このコードのコンパイルを拒否し、プログラムが起動時にva_arg
することを警告します。
クラン警告./test.cpp:23:31: error: second argument to 'va_arg' is of non-POD type 'Bar' [-Wnon-pod-varargs] const auto b = va_arg(va, Bar); ^~~ ./test.cpp:31:12: error: cannot pass object of non-trivial type 'Bar' through variadic function; call will abort at runtime [-Wnon-pod-varargs] foo(1, b, c); ^
まだ-Wno-non-pod-varargs
フラグを使用してコンパイルすると、そうなります。
MSVCは、この場合に重要なコンストラクターを使用することは移植性がないと警告します。
MSVCからの警告d:\my documents\visual studio 2017\projects\test\test\main.cpp(31): warning C4840: "Bar"
ただし、コードは正しくコンパイルおよび実行されます。 コンソールで以下が取得されます。
打ち上げ結果Bar default ctor Cafe default ctor Before va_arg Bar copy ctor Bar dtor Cafe dtor Bar dtor
つまり コピーはva_arg
を呼び出したときにのみ作成され、引数は参照によって渡されます。 どういうわけか自明ではありませんが、標準では許可されています。
gcc 6.3.0は 、単一のコメントなしでコンパイルします。 出力は同じです:
打ち上げ結果Bar default ctor Cafe default ctor Before va_arg Bar copy ctor Bar dtor Cafe dtor Bar dtor
gcc 7.3.0も何についても警告しませんが、動作は変化しています:
打ち上げ結果Bar default ctor Cafe default ctor Cafe copy ctor Bar copy ctor Before va_arg Bar copy ctor Bar dtor Bar dtor Cafe dtor Cafe dtor Bar dtor
つまり このバージョンのコンパイラは引数を値で渡し、呼び出されると、va_arg
は別のコピーを作成します。 コンストラクタ/デストラクタに副作用がある場合、gccの6番目から7番目のバージョンに切り替えるときにこの違いを探すのは楽しいでしょう。
ところで、クラスへの参照を明示的に渡し、要求する場合:
別の間違ったコードvoid foo(int n, ...) { va_list va; va_start(va, n); std::cout << "Before va_arg" << std::endl; const auto& b = va_arg(va, Bar&); va_end(va); } int main() { Bar b; Cafe c; foo(1, std::ref(b), c); return 0; }
すべてのコンパイラがエラーをスローします。 標準で要求されているとおり。
一般的に、本当にしたい場合は、引数をポインタで渡すほうが良いでしょう。
このようにvoid foo(int n, ...) { va_list va; va_start(va, n); std::cout << "Before va_arg" << std::endl; const auto* b = va_arg(va, Bar*); va_end(va); } int main() { Bar b; Cafe c; foo(1, &b, &c); return 0; }
オーバーロード解決と変数関数
一方では、すべてが単純です。標準またはユーザー定義の型変換の場合でも、省略記号との一致は、通常の名前付き引数との一致よりも劣ります。
過負荷の例
#include <iostream> void foo(...) { std::cout << "C variadic function" << std::endl; } void foo(int) { std::cout << "Ordinary function" << std::endl; } int main() { foo(1); foo(1ul); foo(); return 0; }
打ち上げ結果
$ ./test Ordinary function Ordinary function C variadic function
ただし、これは、引数なしの
foo
の呼び出しを個別に考慮しない限り機能します。
引数なしでfooを呼び出す
#include <iostream> void foo(...) { std::cout << "C variadic function" << std::endl; } void foo() { std::cout << "Ordinary function without arguments" << std::endl; } int main() { foo(1); foo(); return 0; }
コンパイラー出力
./test.cpp:16:9: error: call of overloaded 'foo()' is ambiguous foo(); ^ ./test.cpp:3:6: note: candidate: void foo(...) void foo(...) ^~~ ./test.cpp:8:6: note: candidate: void foo() void foo() ^~~
すべては標準に準拠しています。引数はありません-省略記号との比較はありません。また、オーバーロードが解決されると、変分関数は通常のものより悪くなりません。
それにもかかわらず、変数関数を使用する価値があるのはいつですか
さて、変量関数は時々非常に明確に振る舞わず、C ++のコンテキストでは移植性が低いことが容易に判明します。 「変数C関数を作成または使用しないでください」など、インターネットには多くのヒントがありますが、C ++標準からのサポートは削除されません。 これらの機能にはいくつかの利点がありますか? よくそこに。
- 最も一般的で明白なケースは、下位互換性です。 ここでは、サードパーティのCライブラリの使用(JNIの場合)とC ++実装へのC APIの提供の両方を含めます。
- SFINAE C ++では、変数関数に名前付き引数が必要ないこと、およびオーバーロードされた関数を解決するときに変数関数が最後と見なされること(少なくとも1つの引数がある場合)が非常に便利です。 また、他の関数と同様に、変数関数は宣言することしかできませんが、呼び出すことはできません。
例template <class T> struct HasFoo { private: template <class U, class = decltype(std::declval<U>().foo())> static void detect(const U&); static int detect(...); public: static constexpr bool value = std::is_same<void, decltype(detect(std::declval<T>()))>::value; };
C ++ 14では、少し異なる方法で実行できます。
別の例template <class T> struct HasFoo { private: template <class U, class = decltype(std::declval<U>().foo())> static constexpr bool detect(const U*) { return true; } template <class U> static constexpr bool detect(...) { return false; } public: static constexpr bool value = detect<T>(nullptr); };
そして、この場合、既にdetect(...)
れた引数detect(...)
を呼び出すことができるものを監視する必要があります。 いくつかの行を変更し、変数関数のすべての短所を欠いた最新の代替関数を使用したいと思います。
バリアントテンプレートまたは現代のC ++で任意の数の引数から関数を作成する方法
可変テンプレートのアイデアは、2004年にDouglas Gregor、JakkoJärvi、Gary Powellによって提案されました。 これらの変数テンプレートが公式にサポートされたC ++ 11標準が採用される7年前。 この規格には、提案の第3版であるN2080が含まれています。
プログラマーが任意の数の引数からタイプセーフ(およびポータブル!)関数を作成できるように、最初から変数テンプレートが作成されました。別の目標は、可変数のパラメーターを持つクラステンプレートのサポートを簡素化することですが、現在は可変機能についてのみ説明しています。
変数テンプレートは、C ++ [C ++ 17 17.5.3]に 3つの新しい概念をもたらしました。
- テンプレートパラメータパッケージ(テンプレートパラメータパックは) -任意のテンプレート引数の数(0を含む)を転送することが可能である代わりにそのうち、パラメータ・テンプレートです。
- 関数パラメーターのパッケージ(function parameter pack)-したがって、これは(0を含む)任意の数の関数引数を取る関数パラメーターです。
- また、パッケージの展開(pack expansion)のみが、パラメーターパッケージで実行できます。
例
— ,
— ,
— .
template <class ... Args> void foo(const std::string& format, Args ... args) { printf(format.c_str(), args...); }
class ... Args
— ,
Args ... args
— ,
args...
— .
パラメータパッケージを展開できる場所と方法の完全なリストは、標準自体[C ++ 17 17.5.3 / 4]に記載されています。そして、変数関数の議論の文脈では、それを言うだけで十分です:
- 関数パラメーターパッケージは、別の関数の引数リストに展開できます
template <class ... Args> void bar(const std::string& format, Args ... args) { foo<Args...>(format.c_str(), args...); }
- または初期化リストに
template <class ... Args> void foo(const std::string& format, Args ... args) { const auto list = {args...}; }
- またはラムダキャプチャリストに
template <class ... Args> void foo(const std::string& format, Args ... args) { auto lambda = [&format, args...] () { printf(format.c_str(), args...); }; lambda(); }
- 関数パラメータの別のパッケージは、畳み込み式で展開できます
template <class ... Args> int foo(Args ... args) { return (0 + ... + args); }
畳み込みはC ++ 14で登場し、単項およびバイナリ、右および左になります。最も完全な説明は、いつものように、標準[C ++ 17 8.1.6]にあります。
- 両方のタイプのパラメーターパッケージは、sizeof ...演算子に展開できます。
template <class ... Args> void foo(Args ... args) { const auto size1 = sizeof...(Args); const auto size2 = sizeof...(args); }
明示的な省略記号パッケージを開示する際に、さまざまなテンプレート(サポートするために必要とされるパターン)開示をし、この曖昧さを避けるために。
例えば
— (
),
— , — (
).
template <class ... Args> void foo() { using OneTuple = std::tuple<std::tuple<Args>...>; using NestTuple = std::tuple<std::tuple<Args...>>; }
OneTuple
— (
std:tuple<std::tuple<int>>, std::tuple<double>>
),
NestTuple
— , — (
std::tuple<std::tuple<int, double>>
).
変数テンプレートを使用したprintfの実装例
すでに述べたように、変数テンプレートはCの変数関数の直接の代替としても作成されました。これらのテンプレートの作成者自身は、非常にシンプルでありながらタイプセーフなバージョンを提案し
printf
ました。
テンプレートのprintf
void printf(const char* s) { while (*s) { if (*s == '%' && *++s != '%') throw std::runtime_error("invalid format string: missing arguments"); std::cout << *s++; } } template <typename T, typename ... Args> void printf(const char* s, T value, Args ... args) { while (*s) { if (*s == '%' && *++s != '%') { std::cout << value; return printf(++s, args...); } std::cout << *s++; } throw std::runtime_error("extra arguments provided to printf"); }
疑わしいのは、変数引数の列挙のこのパターンが現れた-オーバーロードされた関数の再帰呼び出しを通して。しかし、私はまだ再帰のないオプションを好みます。
テンプレート上のprintfと再帰なし
template <typename ... Args> void printf(const std::string& fmt, const Args& ... args) { size_t fmtIndex = 0; size_t placeHolders = 0; auto printFmt = [&fmt, &fmtIndex, &placeHolders]() { for (; fmtIndex < fmt.size(); ++fmtIndex) { if (fmt[fmtIndex] != '%') std::cout << fmt[fmtIndex]; else if (++fmtIndex < fmt.size()) { if (fmt[fmtIndex] == '%') std::cout << '%'; else { ++fmtIndex; ++placeHolders; break; } } } }; ((printFmt(), std::cout << args), ..., (printFmt())); if (placeHolders < sizeof...(args)) throw std::runtime_error("extra arguments provided to printf"); if (placeHolders > sizeof...(args)) throw std::runtime_error("invalid format string: missing arguments"); }
オーバーロード解決および変数テンプレート関数
解決するとき、これらの変化関数は、他の後に、テンプレートとして、最も専門性の低いものと見なされます。ただし、引数なしの呼び出しの場合は問題ありません。
過負荷の例
#include <iostream> void foo(int) { std::cout << "Ordinary function" << std::endl; } void foo() { std::cout << "Ordinary function without arguments" << std::endl; } template <class T> void foo(T) { std::cout << "Template function" << std::endl; } template <class ... Args> void foo(Args ...) { std::cout << "Template variadic function" << std::endl; } int main() { foo(1); foo(); foo(2.0); foo(1, 2); return 0; }
打ち上げ結果
$ ./test Ordinary function Ordinary function without arguments Template function Template variadic function
過負荷が解決されると、変数テンプレート関数は変数C関数のみをバイパスできます(なぜそれらを混在させるのですか?)。例外-もちろん!-引数なしで呼び出します。
引数なしで呼び出す
#include <iostream> void foo(...) { std::cout << "C variadic function" << std::endl; } template <class ... Args> void foo(Args ...) { std::cout << "Template variadic function" << std::endl; } int main() { foo(1); foo(); return 0; }
打ち上げ結果
$ ./test Template variadic function C variadic function
省略記号との比較があります-対応する関数は失われ、省略記号との比較はありません-テンプレート関数は非テンプレート関数よりも劣っています。
可変テンプレート関数の速度に関する簡単なメモ
2008年、ロイックジョリーは提案N2772をC ++標準化委員会に提出しました。そこで、変数テンプレート関数は、引数が初期化リスト(
std::initializer_list
)である同様の関数よりも動作が遅いことを実際に示しました。これは作者自身の理論的実証に反したもののと、ジョリーは、実装することを提案し
std::min
、
std::max
そして
std::minmax
それは、初期化リストの代わりの変形パターンを介して行われます。
しかし、すでに2009年に反論がありました。ジョリのテストで、「重大な間違い」が発見されました(自分自身にさえ思えます)。新しいテスト(こちらとこちらをご覧ください)は、変数テンプレート関数がさらに高速で、場合によってはかなり高速であることを示しました。驚くことではありません 初期化リストはその要素のコピーを作成します。変数テンプレートの場合、コンパイル段階で多くをカウントできます。
それにもかかわらず、C ++ 11以降の標準
std::min
では
std::max
、
std::minmax
は通常のテンプレート関数であり、初期化リストを介して渡される任意の数の引数です。
簡単な要約と結論
したがって、Cスタイルの変数関数:
- 彼らは、引数の数もタイプも知りません。開発者は、残りの情報を渡すために、関数の引数の一部を使用する必要があります。
- 名前のない引数(および最後の名前の引数)の型を暗黙的に上げます。それを忘れると、あいまいな振る舞いをします。
- これらは純粋なCとの後方互換性を維持しているため、参照による引数の受け渡しをサポートしていません。
- C ++ 11より前は、POD型ではない引数はサポートされていませんでした。C++ 11以降の非自明な型のサポートは、コンパイラの裁量に任されていました。つまり コードの動作は、コンパイラとそのバージョンに依存します。
変数関数の唯一の使用は、C ++コードでC APIと対話することです。SFINAEを含む他のすべてについて、以下の変数テンプレート関数があります。
- すべての引数の数とタイプを知っています。
- 型セーフで、引数の型を変更しないでください。
- 値、ポインタ、参照、ユニバーサルリンクなど、あらゆる形式で引数を渡すことができます。
- 他のC ++関数と同様に、引数のタイプに制限はありません。
- ( C ), .
変数テンプレート関数は、Cスタイルの対応する関数と比較してより冗長である場合があり、時には独自のオーバーロードされた非テンプレートバージョン(再帰的な引数トラバーサル)を必要とすることさえあります。読み取りと書き込みが困難です。しかし、これはすべて、リストされている欠点がなく、リストされている利点が存在することで、対価が支払われます。
結論は簡単です。Cスタイルのバリエーション関数は、下位互換性のためだけにC ++のままであり、脚を撃つための幅広いオプションを提供します。現代のC ++では、新しいものを記述しないこと、できれば既存の変数C関数を使用しないことを強くお勧めします。可変テンプレート関数は、最新のC ++の世界に属し、はるかに安全です。それらを使用します。
文献と情報源
- PJプラウジャー、標準Cライブラリ
- ブライアンW.カーニハンとデニスM.リッチー、Cプログラミング言語、第1版
- ブライアンW.カーニハンおよびデニスM.リッチー、Cプログラミング言語、第2版
- 標準C11、ドラフトN1570
- C ++ 98標準
- C ++ 03標準
- C ++ 11標準、ドラフトN3337
- 標準C ++ 14、ドラフトN4296
- C ++ 17標準、ドラフトN4659
PS
上記の書籍の電子版をネット上で簡単に見つけてダウンロードできます。しかし、それが合法かどうかはわかりませんので、リンクは提供しません。