C ++:自発的な考古学のセッションと、Cのスタイルで変数関数を使用しない理由

いつものように、すべてはエラーから始まりました。 Java Native Interfaceを使用したのはこれが初めてで、C ++パートではJavaオブジェクトを作成する関数をラップしました。 この関数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以降)について簡単に説明します。





なんで? しかし、なぜなら!



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 ++ Standardの「 デフォルトの引数型のエスカレーション 」は[C ++ 17 8.2.2 / 9]のままです。 そして第二に、私はこのフレーズの意味について長い間困惑し、すべてが明確なスタンダードCと比較しました。 N0695を読んだ後にようやく理解できたのは、同じことです。



ただし、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つのリストにまとめ、特定の例で補足しようとします。



変数関数を簡単かつ誤って使用する方法



  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); }
          
          





  2. 最後の名前付き引数を参照として宣言するのは正しくありません。 任意のリンク。 この場合の標準は、未定義の動作も保証します。



    無効なコード
     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); }
          
          





  3. 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); }
          
          





  4. 名前のない引数として重要なコンストラクタまたはデストラクタを持つクラスのインスタンスを渡すことは正しくありません。 もちろん、このコードの運命が「今ここでコンパイルして実行する」以上にあなたを興奮させない限りは。



    無効なコード
     #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 ++で任意の数の引数から関数を作成する方法



可変テンプレートのアイデアは、2004年にDouglas Gregor、JakkoJärvi、Gary Powellによって提案されました。 これらの変数テンプレートが公式にサポートされたC ++ 11標準が採用される7年前。 この規格には、提案の第3版であるN2080が含まれています。



プログラマーが任意の数の引数からタイプセーフ(およびポータブル!)関数を作成できるように、最初から変数テンプレートが作成されました。別の目標は、可変数のパラメーターを持つクラステンプレートのサポートを簡素化することですが、現在は可変機能についてのみ説明しています。



変数テンプレートは、C ++ [C ++ 17 17.5.3]に 3つの新しい概念をもたらしました





 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 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 APIと対話することです。SFINAEを含む他のすべてについて、以下の変数テンプレート関数があります。





変数テンプレート関数は、Cスタイルの対応する関数と比較してより冗長である場合があり、時には独自のオーバーロードされた非テンプレートバージョン(再帰的な引数トラバーサル)を必要とすることさえあります。読み取りと書き込みが困難です。しかし、これはすべて、リストされている欠点がなく、リストされている利点が存在することで、対価が支払われます。



結論は簡単です。Cスタイルのバリエーション関数は、下位互換性のためだけにC ++のままであり、脚を撃つための幅広いオプションを提供します。現代のC ++では、新しいものを記述しないこと、できれば既存の変数C関数を使用しないことを強くお勧めします。可変テンプレート関数は、最新のC ++の世界に属し、はるかに安全です。それらを使用します。



文献と情報源





PS



上記の書籍の電子版をネット上で簡単に見つけてダウンロードできます。しかし、それが合法かどうかはわかりませんので、リンクは提供しません。



All Articles