stdのアナログ::純粋なC89上のC ++ 11からのベクトル、および私が書いたとおり

画像

住宅団地。 いいえ、本当に。







Xiの愛好家と彼の信者の間のホリバー ろくでなし C ++の人の息子は生まれる前から始まっていて、これらの言語と私の両方が同時に死んだ後を除いて止まります。







Kernighan-Ritchieの偉大な創造の支持者たちは、Cの永遠性とその信じられないほどの柔軟性に関する公理を、ストラウストルプの手先に、勤務日の最後の2秒間まで証明する準備ができています。

それに応じて、彼らは独自の方法で、彼らが彼らが彼らが最後であることが判明しようとしているので、彼らが労働日をよりよく楽しむように助言します。

炎症を起こしたCの支持者は、「なぜCはC ++よりも優れているか」という何百万もの論文を引用します。

被告人は気分を害したままではない...







でもちょっと待って







私はCが大好きで、C ++を尊重し、ホリバーに耐えられません(正直に)。 同時に、Cには本当に多くのことが欠けていることを実感します。これの鮮明な例は、データを使った便利な作業の欠如です。 C ++では、この問題の大部分はSTLと言語自体のプロパティによって解決されます。 私の学生の意見では、おなじみのstd::vector



はここでは特に異なります。 C89を使用してそのアナログをどのように実装したかがおもしろくなった場合は、猫をお願いします。







背景



一般に、少し高いレベルの言語(私の場合はFreeBASICとFree Pascal)からCに切り替えるすべての人は、必ず上記の問題に直面します。 Redim



SetLength()



が存在しないという問題は、長い間愛されてきましたが、最初にrealloc()



を使用して「ハンマーで額」を解決します。 その後、知識は経験とともに受け入れられ、代わりに単純な自己記述型の動的配列がすでに使用されています。







ただし、そのたびに個々のデータ型のコードを複製する必要があります。 別のオプションは、ポインターを使用することです。これには、逆参照と型キャストが必要です。 そして、その人はC ++(またはそのアナログ)の手に落ち、その人はSTL(またはそのアナログ)を見ます。 どのタブロイド小説でも読むことができます。







それにもかかわらず、彼らは体に恋をするが、魂を愛する。 長い間Xと幸せな関係にある人、プロジェクトがすでにXに登場している人、お互いの利益のために、愛の対象をより良くしたい人は当然です。 そして、完璧な人は常に何かに集中します。







要するに、これは、Cの愛がどのようにして悪名高いstd::vector



をもたらしたのかについての物語です(彼?)-私はC ++で好きでしたが、これはかつて興味がありました(どれ?)







私たちの前でさえ洪水



任意の型に対するCの組み込み動的配列の欠如の問題は新しいものではなく、さまざまな方法で何度も解決されていることがすでに指摘されています。

Googleでわずか5分で見つけたベクターを実装するためのオプションは次のとおりです。







https://github.com/rxi/vec

https://github.com/eteran/c-vector

https://github.com/jibsen/scv

https://github.com/graphitemaster/cvec

https://github.com/robertkety/dataStructures(Ctrl + F "dynamicArray")

http://troydhanson.github.io/uthash/utarray.html

https://github.com/dude719/Dynamic-Array-Kernel

https://developer.gnome.org/glib/stable/glib-Arrays.html

https://www.happybearsoftware.com/implementing-a-dynamic-array

https://github.com/nothings/stb/blob/master/stretchy_buffer.h(Xop tipにより追加)







これらのソリューションにはすべて、次のうち少なくとも1つがあります。 致命的 欠点:







  1. 特定の管理機能のマクロ実装。

    インライン関数としてマクロを使用するのは悪い考えです。 これは何度も言われていますが、繰り返しにうんざりしていませんか?

    まず 、関数マクロを使用する場合、不正な引数タイプが原因で発生するエラーを追跡およびデバッグするのが難しくなります。

    第二に 、関数マクロは、コンマ演算子または変数名の下の別の引数で結果を保存するために倒錯が考慮されない限り、何も返すことができません。

    第三に 、インラインにあまり似ていない関数マクロからの一定のコード置換により、翻訳単位のサイズが大きくなります。 これにより、出力実行可能ファイルのサイズが増加し、他の楽しみが生まれます。

    4番目に、マクロ関数へのポインターを取得できません。







  2. 任意のベクトルに共通する関数の複製。

    たとえば、 int



    ベクトルとchar



    ベクトルの異なるリリース関数。 内部では、これらはfree()



    関数の呼び出しになります。この関数は、破棄されるバッファーに格納されているものと、そのポインターのタイプとはまったく無関係です。

    これもまた、翻訳単位の量の増加、コードの重複、および名前空間の散在を引き起こします。







  3. 型なしポインタを介して値を操作します。

    これには、プリミティブ型の単純なベクトル(intなど)に追加するために、常に値へのポインターを取る必要があります。 また、型キャストと逆参照を忘れないでください。 まあ、そのようなベクトルでは異なるタイプの値を潜在的に置くことができ、これについて誰も警告しません。







  4. 構造体としてのベクトルのタイプの指定。

    最大の欠点は、あるものが存在する場合、他のものが完全に存在しなくても役に立たないことです。

    最初に 、ベクトルの要素へのアピールは、構造体フィールドを通じて発生します。 一次元ベクトルの場合、これはすでに不便です-多次元について話す価値はありますか。

    第二に、構造のすべてのフィールドは、技術的なものであっても、ユーザーが自由にアクセスできます。

    第三に 、異なるタイプのベクトル間のほぼ完全な非互換性。

    第4に 、ベクターを作成および削除するには、それぞれ2つの呼び出しmalloc()



    / free()



    必要ですmalloc()



    つは構造用、もう1つはベクターバッファー自体用です。 ご想像のとおり、次元ベクトルの場合 n 呼び出しは既にあります 2n

    5番目に 、そのようなベクトルは構造体へのポインターによってのみ関数に渡すことができるため、関数でアクセスするための構文はわずかに異なります( .



    代わりに->



    )。









したがって、任意のタイプのCデータに特化し、次の機能を備えたベクターを作成するタスクが浮かび上がります。







  1. 次元に関係なく、通常の配列の要素としてのベクトルの要素へのアクセス: vec[k]



    vec[i][j]



    など。
  2. マクロとは異なり、型付き引数と戻り値を持つ通常の関数を使用してベクトルを制御します。
  3. ユーザータイプの値を受け入れたり返したりする関数のみの特殊化による重複コードの欠如。
  4. ユーザーはベクターの技術情報に直接アクセスできません。
  5. 1つの割り当てのレベルでの異なるタイプのベクトル間の互換性。
  6. ベクトルを特殊化するときの、値の送信または戻りの方法を示すためのユーザーの能力:値または参照(ポインターを使用)。
  7. C ++ 11のstd::vector



    のベクトルインターフェイスとの最大の類似性。


c89に飛び込む



少なくともC99ではなくC89である理由を事前に質問に答えます。 まず、Visual Studioからのコンパイラサポートを提供します(私は気に入らないが)。 第二に、私自身はC99を本当に愛していますが、この場合、より厳しい条件でこの課題を解決できると感じました。 結局のところ、「異常なプログラミング」での出版は正当化されなければなりません。







私がCを学び始めたばかりのとき、 便利なベクターを書くことは私にはまったく不可能に思えました-頭の中のインデックス演算子は厳密に配列に関連付けられ、配列は厳密に型付きポインターに関連付けられ、ベクターは構造に技術情報を保存する必要性に関連付けられました ベクターの要素へのアクセスはこの構造体のフィールド自体によってのみ実現できるという考えから逃げることができず、ベクター実装の大部分がこのアプローチを使用することで、これに対する信頼が強化されました。







しかし、その後、 Redis用に作成されたSimple Dynamic Stringsと呼ばれるC用の動的文字列のライブラリに出会いました。 別のアプローチを使用します。ベクターに関する技術情報は、ポインターと一緒に構造に保存されるのではなく、メモリー内のベクターバッファー自体の直前にヘッダーの形式で保存されます。 これにより、タイプされたポインターを介してベクターを直接操作できますが、技術情報の配置は常に確実に認識されます。







画像







型付きポインターを使用すると、通常の配列のように、インデックス演算子を介してベクターの要素にアクセスできるようになることを思い出してください。 そして、ベクトル自体の直前にある技術情報の場所は、ユーザーからの直接アクセスを奪います-このため、ユーザーはポインターを操作する必要があります。 構造の場合、ベクトルへのポインタと技術情報の両方がそのフィールドであり、それらへのアクセスは同じです。







したがって、可能性(1)と(4)を実現しました。 続けましょう。







現在、ベクトルは型付きポインタにすぎないため、単に引数「解放するベクトルへのポインタ」をvoid*



指定するだけで、さまざまなタイプのベクトルのリリース関数などの関数を既に一般化できるように思われvoid*



。 他のポインターは暗黙的にvoid*



に変換でき、またその逆も可能であることはよく知られています。







ただし、他の機能に対してこれを行うことはできますか? 奇妙なことに、しかしはい。 格納された値自体を直接操作する関数はありません。元々は、ベクトルのタイプごとに個別に特化されるはずでした。 実際、値の保存場所でのみ動作し、値自体では動作しません。 したがって、ベクターの技術情報に保存し、対応する引数を渡すことでその作成の機能を埋めることができる1つの要素のサイズのみを知るだけで十分です。 このようなトリックを使用すると、さまざまなタイプのベクトルのすべての関数を一般化し、ユーザータイプの値を受け入れるおよび/または返す関数のみに基づいて特殊化することができます。







パラグラフ(2)および(3)が実装されます。 また、Cにはオブジェクトがなく、文字通りメモリをコピーすることで値を別の変数に再割り当てできるため、パラグラフ(5)も実装されています。 同じように続けます。







実際、すべての特殊な関数は、次の2つの方法のいずれかでユーザー型の値を操作します。









値は関数に渡されるか、値によって返されるか(値を取得できません)、または参照によって返されることが知られています。 プリミティブ型の場合は、最初のオプションが推奨されますが、複雑な構造の場合は2番目のオプションが推奨されます。

Cには確かにC ++リンクはありませんが、ポインターがそれらを私たちに置き換えます。







テキストにうんざりしていませんか? 修辞的な質問。

次に、明確にするために、値と参照によって変数をそれぞれ受け入れる/返す同じ関数のバリアントの定義を示します。







 gvec_error_e gvec_NAME_push( gvec_NAME_t* phandle, const TYPE value ) gvec_error_e gvec_NAME_push( gvec_NAME_t* phandle, const TYPE* value )
      
      





 TYPE gvec_NAME_front( gvec_NAME_t handle ) TYPE* gvec_NAME_front( gvec_NAME_t handle )
      
      





どちらの場合でも、違いは1つのシンボルのみであることがわかります。







既にC89で、代入演算子は、プリミティブ型だけでなく、 すべての型で使用できます。 これにより、参照によって、または特殊化された関数の値によって、マクロスペシャライザーの引数によって指定された転送および戻りが可能になります。 確かに、合理的な疑問が生じます。送信と返送を同時に行うために、これを1つの引数ですぐに示してはどうでしょうか。 しかし、非常に簡単です。プリミティブ型の場合、値による戻りはより便利で高速ですが、ベクトルに要求された要素がない場合、値が決定されない場合があります。 この場合に参照で返す場合、単にNULL



返すことができNULL



。 要するに、これはプログラマーの裁量に任されています。







その結果、パラグラフ(6)が実装されました。 条項(7)は、以前のものすべての集合に実装されていると考えることもできます。







おわりに



実際に使用できるC89ベクトルライブラリの最終的な実装は次のとおりです。







https://github.com/cher-nov/genvector MITライセンス 現在WTFPL)







最も簡単な使用例はReadMeです。







もちろん、この記事では実装の複雑ではないが面白みのある他の側面については説明していません。その説明については、簡潔さや雄弁さはありませんでした。 彼らはまた、成功しなかったことが判明した決定についての暴言と、彼らの再考を省略しました。 しかし、最初の答えはリポジトリのコードとReadMeから取得でき、2番目の答えはコミットの履歴から取得できると確信しています。







これはHabréに関する私の最初の記事ですので、できる限り厳しく判断するようお願いします。 舌を結ぶために-特に。







これがすべて誰かに役立つことを願っています。








All Articles