コードの複製はCで必要な悪ですか?


16

私はCを初めて使用しますが、一般的なデータ構造とCの一般的な記述に関して、コードの重複が必要な悪なのかどうか疑問に思っています。

hash mapたとえば、一般的な実装を記述しようとすることもできますが、最終結果は常に乱雑になります。また、この特定のユースケースだけに特化した実装を記述し、コードを明確に保ち、​​読みやすくデバッグしやすくすることもできます。後者はもちろん、コードの重複につながります。

一般的な実装は標準ですか、それともユースケースごとに異なる実装を作成しますか?


11
あなたが説明している道徳的な不満は、Cに固有のものではありません。逸話的に、一般的な実装を作成するための基準は、聴衆がいない限り非常に高いことがわかります。適切な汎用実装を作成するために必要な労力は、ポイントソリューションであるIMEをはるかに超えています。
ロバートハーヴェイ


1
@kevincline:まあ、Javaにはジェネリックがあります。確かに、これは「カーテンの後ろの人に注意を払わない」タイプのジェネリック医薬品です。
ロバートハーベイ

1
そして、ジェネリックの前に、Objectを渡し、Float、Double、Integer、およびLongをそれぞれ渡すことができました。
ダニエルカプラン

3
Deja vuはコードの匂いチェックであり、DRYの設定は、IMOに注意を払う価値のあるすべてのベストプラクティスの根幹にあります。しかし、私は自分自身のために重複を避けるために一生懸命に努力しているOOP指向の言語に足を踏み入れました。無関係な懸念を不必要に結びつけたり、2つの類似した機能が1つになるために読みやすさを破壊したりしない場合は、ほとんどの言語で本能を発揮します。
エリックReppen

回答:


27

Cを使用すると、汎用コードを書くことが難しくなります。テンプレートと仮想関数を提供するC ++とは異なり、Cには汎用コードを記述するための3つのメカニズムしかありません。

  1. void* ポインタ
  2. プリプロセッサマクロ
  3. 関数ポインター

void* コンパイラーによって提供されるすべての型安全性を失うため、ポインターは理想からはほど遠いため、無効な型キャストに起因するデバッグが困難な未定義の動作を引き起こす可能性があります。

プリプロセッサマクロには顕著な欠点があります。プリプロセッサの拡張は、基本的にはコンパイル段階の前に発生する単なる検索/置換メカニズムであり、デバッグが困難なエラーが発生する可能性があります。典型的な例は次のようなものです:#define add(x) (x+x)、ここでxを呼び出すと 2回インクリメントできますadd(i++)。あなたはできる完全にC-マクロを使用してテンプレート・スタイルの一般的なコードを書いたが、結果は本当に恐ろしいし、維持するのが難しいです

関数ポインターは一般的なコードを書く良い方法を提供しますが、残念ながら型の一般性を提供しません-単にランタイムポリモーフィズムの可能性を提供するだけです(例えば、標準ライブラリはqsortまだvoid*ポインタ。)

汎用GObjectベースクラスを提供するGLibライブラリで行われているように、構造体を使用してCでクラス階層を実装することもできます。ただし、これはvoid*ポインターを使用する場合と同様の問題に悩まされます。これは、潜在的に危険な手動キャストをアップキャストおよびダウンキャストに依存する必要があるためです。

そのため、Cを使用すると、一般的で安全で保守が容易なコードを記述しにくくなり、残念ながらコードが重複する可能性があります。大規模なCプロジェクトでは、ビルドプロセス中にスクリプト言語を使用して繰り返しコードを生成することがよくあります。


1
一つは、また、外部のテンプレート言語/ツールやシェルスクリプトを使用するか、またはantを使用したり...の繰り返しCコードを生成することがありました
ジョブ

あなたがあなたの答えに入れたこのGoogle難読化されたロシアのURLは何ですか?クリックすると、ブラウザに「開く」プロンプトが表示されます。これは安全ですか?
ロバートハーベイ

2
@RobertHarvey私のウイルス対策スキャンとセキュリティスキャンでは、問題ないと考えています。これは、拡張子が.hのC言語ヘッダーファイルです。合法的に見えます。
maple_shaft

@maple_shaft:OK。リンクの周りのグーグル化を削除しました。
ロバートハーベイ

それは単なるプレーンテキスト.h(Cヘッダー)ファイルです
チャールズサルビア

15

私は他の人のために話すことはできませんが、私自身のCでの経験では、コードの重複はそれほど問題ではありませんでした。これはプロジェクトのサイズによるものか、魅力的なサンプルセットによるものかはわかりません。ただし、私が適用できると思う3つの経験則があります。順不同で、彼らは...

  1. 必要なものを書いてください。一般性は、必要に応じて後から来ることができます。
  2. 一般性が必要な場合、voidポインター、関数ポインター、および構造体のサイズは非常に貴重です。たとえば、qsort()ルーチンは、3つすべてを使用します。
  3. コードを明確にすることに重点を置きます。

5

コードの重複を減らすために完全に汎用的なソリューションを必要としない場合があることに注意してください。通常のリファクタリングと少しのメソッドの一般化で十分な場合があります。

一般ユーザー向けに一般化されたソリューションを作成するときは、次のことを考慮する必要があります。

  1. 消費者はあなたのコードをどのような新しい方法で使用しますか?
  2. どのエラーをキャッチする必要がありますか?どのエラーをキャッチしてはいけませんか?
  3. APIの堅牢性はどの程度必要ですか?いくつのオーバーロードを提供する必要がありますか?
  4. コードを悪意のある目的に使用できないようにするには、どのようなセキュリティ対策を講じる必要がありますか?

Eric Lippert 、.NETフレームワークコードでの単一の設計決定に関する記事全体を書きました。全体として、多くの場合、リファクタリングする方が簡単です。


3

私は通常、glibのような一般的な実装を使用します。キャストが面倒になる場合は、そのための小さなタイプ固有のラッパーを作成します。ただし、void*汎用型としてのaの使用と同様に、Cでは多くのキャストが予想されるため、別の言語で「乱雑」と見なされるものは典型的なCにすぎません。言語でより多くの経験を得る。


2
「別の言語で「面倒」と見なされるのは典型的なCだけです。恐ろしいことに、その言語でより多くの経験を積むと、はるかに自然に見えます。」ハハハ!Cは常に私のお気に入りの言語の1つですが、それはまだかなり面白いです!
グレンペターソン

0

再利用性の問題はCに固有のものではありません。しかし、いくつかの優れた非常に再利用可能なHashMap実装があり、それらをベースにすることができます。1、2年前から存在していたものはJavaにあります:http : //docs.oracle.com/javase/7/docs/api/java/util/HashMap.html

HashMapを含むすべてのJavaコレクションは、イテレーターの概念を取り除くためにJava 8用に書き直されており、代わりに内部で反復できるようにコレクションに関数を渡す必要があります。これは、並行コードを記述する上で大きなメリットです。コレクションは並行処理を内部的に管理できるため、クライアントはその必要がありません。ただし、ほとんどのプログラマーは関数ポインターを渡すことに慣れていません。これは、HashMapがどこに向かっているのかを説明する良い仕事をします: ます http //cr.openjdk.java.net/~briangoetz/lambda/collections-overview.html

Scalaの構文により、このようなものをCに変換するのが難しくなる可能性がありますが、Java 8コレクションが既に動作する方法のコレクションの例を次に示します-デフォルトでは不変で、さまざまな変換メソッドを提供し、関数ポインターやその他部分適用のようなクールなもの: http //www.scala-lang.org/api/current/index.html#scala.collection.Map

HashMapsやその他のより高度なデータ構造で多くのことを行っている場合は、それらをサポートする言語を調べてください。

一般的なコード複製の考え方に戻ると、切り取りと貼り付けを行うことで非常に高速にコーディングできますが、ひどいコードが生成されます。一度限りのレポートのような使い捨てのコードでは受け入れられると思います。他に何も依存しない画面を構築するUIコードのように、下流にコードがない場合でも問題ない場合がありますが、実際には非常に灰色の領域です。

コードを複製していることに気づいたときはいつでも、関数を作成する必要があると言っても過言ではありません。コードの1つのコピーで修正し、他のコードでは修正しないバグだけで、関数を作成する理由は十分です。


1
-「コードを複製していることに気づいたら、関数を作成する必要があると言うのはあまりにも単純すぎるとは思わない」-Cでは、これは単純すぎる。各データ型のプッシュ関数とポップ関数を複製せずに、整数、倍精度、および文字列のCでスタックを作成してみてください。
mike30

@mike voidポインターを使用することはできませんか?
サキスク

@マイク-良い点。しかし、カウンターポイントは、データ型ごとにStackを実装でき、Stackを再度実装する必要がないということです。したがって、目標はnumberOfImplementations = 1ではなく、5のようになります。sのint実装を機能させることができるかもしれませんchar... Cを使用してから非常に長い間、思い出せません。
グレンペターソン

1
@faif。はい。ただし、voidポインターを使用する必要があるため、Cのポイントになります。ポインターのキャストは、C ++の代替よりも遅く、エレガントではありません。コードの複製は、Cでコーディングするときに可能なオプションのリストでやや高くなります
。– mike30

@ mike30「Cでのポインターのキャスティングは、C ++で見つかった代替よりも遅い」という証拠/参照がありますか?
sakisk

0

一般的なデータ構造と一般的なCの記述に関して、コードの複製が必要な悪であるかどうか疑問に思っていますか?

Cでは、私にとって絶対に、CとC ++の間を行き来する人として。私は間違いなく、C ++よりもCで日常的に些細なことを複製しますが、意図的に、少なくともいくつかの実用的な利点があるため、必ずしも「悪」とは思いません-すべてを考慮するのは間違いだと思います厳密に「善」または「悪」として-ほぼすべてがトレードオフの問題です。これらのトレードオフを明確に理解することは、後知恵で後悔する決定を避けないための鍵であり、単に「良い」または「悪」とラベル付けするだけで、こうした微妙な点はすべて無視されます。

他の人が指摘したように、この問題はCに固有のものではありませんが、マクロやジェネリックのvoidポインターよりも洗練されたものが不足していること、非自明なOOPの扱いにくいこと、 C標準ライブラリにはコンテナが付属していません。C ++では、独自のリンクリストを実装する人は、学生でない限り、標準ライブラリを使用しない理由を要求する人々の怒りの群衆を得る可能性があります。Cでは、Cプログラマーが少なくともこれらのタイプのことを少なくとも毎日行うことが期待されることが多いため、エレガントなリンクリストの実装をスリープ状態で自信を持って展開できない場合、怒った暴徒を招待します。それ' Linus Torvaldsは、言語を理解し、「良い味」を持っているプログラマーを評価するための基準として二重間接を使用するSLL検索および削除の実装を使用した、リンクリストに対する奇妙な強迫観念によるものではありません。これは、Cプログラマーがそのようなロジックを自分のキャリアで何千回も実装する必要があるためです。このCの場合、新しい料理人のスキルを評価するシェフのように、卵を準備して、常に必要な基本的なことを少なくとも習得できるかどうかを確認します。

たとえば、この基本的な「インデックス付きフリーリスト」データ構造を、この割り当て戦略を使用する各サイトに対してローカルでCで数十回実装しました(リンク構造のほとんどすべてが、一度に1つのノードを割り当ててメモリを半分にする64ビットのリンクのコスト):

ここに画像の説明を入力してください

しかし、Cではrealloc、成長可能な配列に非常に少量のコードを取り込み、これを使用する新しいデータ構造を実装するときに、フリーリストへのインデックス付きアプローチを使用して、そこからメモリをプールします。

これで、同じものがC ++で実装され、クラステンプレートとして一度だけ実装されました。しかし、これはC ++側のはるかに複雑な実装であり、数百行のコードと、数百行のコードにまたがる外部依存関係があります。そして、それがはるかに複雑な主な理由は、考えTられるデータ型の可能性があるという考えに反してコーディングしなければならないからです。いつでもスローする可能性があります(破棄する場合を除き、標準ライブラリコンテナと同様に明示的に行う必要があります)、適切なアライメントを考えてメモリを割り当てる必要がありましたT (幸いなことにこれはC ++ 11以降で簡単になります)、それは非自明に構築可能/破壊可能である可能性があります(新しい配置と手動のdtor呼び出しを配置する必要があります)、私はすべてが必要ではないがいくつかのことが必要なメソッドを追加する必要がありますそして、イテレータ、可変および読み取り専用(定数)イテレータなどを追加する必要があります。

成長可能な配列はロケット科学ではない

ではC ++の人が同じように聞こえる作るstd::vector死に最適化されたロケット科学者の仕事は、ですが、それは、任意のより良いだけで使用する特定のデータ型に対してコード化された動的なC配列よりも実行されませんreallocAでプッシュバック上のアレイの容量を増やすために数十行のコード。違いは、成長可能なランダムアクセスシーケンスを標準に完全に準拠させるには非常に複雑な実装が必要であり、挿入されていない要素でアクタを呼び出さないようにし、例外に対して安全で、constおよびnon-constの両方のランダムアクセスイテレータを提供し、タイプを使用することです特定の整数型の範囲アクタからフィルアクタを明確にする特性T型特性などを使用して異なるPODを処理する可能性があります。その時点で、実際には、成長可能な動的配列を作成するために非常に複雑な実装が必要になります。プラス面では、PODと非自明なUDTの両方を保存する必要があり、準拠するデータ構造で動作する一般的なイテレーターベースのアルゴリズムを使用する必要がある場合、余分な労力から非常に多くの燃費を得ることができます。例外処理とRAIIの恩恵を受けます。少なくともstd::allocator、独自のカスタムアロケーターなどでオーバーライドすることもあります。どれだけの利益があるかを考えると、標準ライブラリで確実に成果が得られます。std::vector それを使用した人々の世界全体にありましたが、それは世界全体のニーズを対象とするように設計された標準ライブラリに実装されたもののためです。

非常に具体的なユースケースを処理する簡単な実装

「インデックス付きフリーリスト」で非常に具体的なユースケースを処理した結果、このフリーリストをC側で数十回実装し、結果としていくつかの些細なコードを複製したにもかかわらず、私はおそらくより少ないコードを書いたC ++で1回実装するよりも数十回実装するためにCで合計し、その1つのC ++実装を維持するよりも、これら12個のC実装を維持する時間を短縮しました。C側が非常に単純である主な理由の1つは、この手法を使用するときは常にCでPODを操作し、通常inserteraseこれをローカルに実装する特定のサイトで。基本的に、C ++バージョンが提供する機能の最も小さなサブセットを実装することができます。これは、非常に特定の用途に実装する場合、自分が何をすべきかについて多くの仮定を自由に立てることができるためです。場合。

現在、C ++バージョンは非常に優れており、タイプセーフに使用できますが、たとえば、1つの一般的な再利用可能な実装を考え出す方法で、例外セーフおよび双方向イテレータ準拠を実装および作成する主要なPITAでしたこの場合、実際に節約される時間よりも長くなります。そして、それを一般化された方法で実装するための多くのコストは、前払いだけでなく、エスカレートされたビルド時間のような形で繰り返し繰り返されます。

C ++への攻撃ではありません!

しかし、これはC ++が大好きなのでC ++に対する攻撃ではありませんが、データ構造に関して言えば、実装するために多くの余分な時間を前もって費やしたい本当に重要なデータ構造に対して主にC ++を好むようになりました非常に一般化された方法で、すべての可能なタイプに対して例外セーフをT作成し、標準に準拠し、反復可能にするなど、そのタイプの先行費用は実際に大量のマイレージの形で報われます。

しかし、それはまた非常に異なるデザインの考え方を促進します。C ++では、衝突検出のためにOctreeを作成したい場合、n番目の程度に一般化したい傾向があります。インデックス付きの三角形メッシュを保存するだけではありません。実行時にすべての抽象化ペナルティを排除する非常に強力なコード生成メカニズムを指先で持っているときに、それを使用できるデータ型を1つだけに制限する必要があるのはなぜですか?手続き型の球体、立方体、ボクセル、NURBSサーフェス、点群などを保存し、すべてに適したものにしようとしています。テンプレートを指先に置いたときにそのように設計したいのは魅力的だからです。衝突検出に限定したくないかもしれません。レイトレーシング、ピッキングなどはどうですか?C ++は、最初は「簡単に」見えるようにします データ構造をn次まで一般化する。そして、C ++でこのような空間インデックスを設計するために使用した方法です。私はそれらを世界の飢handleニーズ全体に対処するように設計しようとしましたが、その見返りに得られたのは通常、考えられるすべてのユースケースとバランスをとるための非常に複雑なコードを持つ「すべての取引のジャック」でした。

面白いことに、長年にわたってCで実装してきた空間インデックスをさらに再利用してきました。C++の欠点はありませんが、言語が私を誘惑するものだけです。Cでoctreeのようなものをコーディングするとき、ポイントで動作するようにし、それだけで満足する傾向があります。なぜなら、言語は、それをn次まで一般化することすら難しくするからです。しかし、これらの傾向のため、私は長年にわたって実際に効率的で信頼性が高く、特定のタスクに本当に適しているものを設計する傾向がありました。それらは、すべての取引のジャックではなく、1つの専門カテゴリのエースになります。繰り返しますが、これはC ++の欠点ではありませんが、Cとは対照的に私が使用しているときの人間の傾向にすぎません。

とにかく、私は両方の言語が大好きですが、異なる傾向があります。CIでは、十分に一般化しない傾向があります。C ++では、一般化しすぎる傾向があります。両方を使用することで、バランスを取ることができました。

一般的な実装は標準ですか、それともユースケースごとに異なる実装を作成しますか?

配列またはそれ自体を再割り当てする配列(std::vectorC ++のアナロジカルな同等物)からのノードを使用した片方向リンクの32ビットインデックス付きリストや、ポイントを保存してそれ以上何もしないことを目的とするoctreeなど任意のデータ型を格納するポイントまで一般化する必要はありません。特定のデータ型を格納するためにこれらを実装します(ただし、抽象的で関数ポインタを使用する場合もありますが、少なくとも静的なポリモーフィズムを使用したダックタイピングよりも具体的です)。

そして、それらを徹底的に単体テストすることを条件に、それらのケースでの冗長性が少しあれば完全に満足しています。ユニットテストを行わないと、冗長性がはるかに不快に感じ始めます。これは、重複する間違いを引き起こす可能性のある冗長なコードがある可能性があるためです。たとえば、書いているコードの種類が設計変更をまったく必要としない場合でも、壊れているため、まだ変更が必要な場合があります。理由として書いているCコードのより徹底的な単体テストを書く傾向があります。

自明ではないことについては、通常C ++に到達しますが、Cで実装する場合は、void*ポインタだけを使用することを検討し、各要素に割り当てるメモリ量を知るために型サイズを受け入れ、おそらくcopy/destroy関数ポインタを使用することを検討します簡単に構築/破壊できない場合は、データをディープコピーして破棄します。ほとんどの場合、私は気にせず、最も複雑なデータ構造とアルゴリズムを作成するためにそれほど多くのCを使用しません。

特定のデータ型で十分に頻繁に1つのデータ構造を使用する場合は、ビットとバイトおよび関数ポインターで動作するものに型保証バージョンをラップすることもできvoid*ます。たとえば、Cラッパーを介して型安全を再設定します。

たとえば、ハッシュマップの一般的な実装を記述しようとすることもできますが、最終結果は常に乱雑になります。また、この特定のユースケースだけに特化した実装を記述し、コードを明確に保ち、​​読みやすくデバッグしやすくすることもできます。後者はもちろん、コードの重複につながります。

ハッシュテーブルは、実装が簡単であるか、ハッシュ、リハッシュに関するニーズの複雑さに応じて本当に複雑になる可能性があるため、テーブルを暗黙的に自動的に拡張する必要がある場合、またはテーブルサイズを予測できる場合、ただし、留意すべきことの1つは、ハッシュテーブルを特定のサイトのニーズに完全に適合させた場合、実装がそれほど複雑ではなく、多くの場合勝つことです。それらのニーズに合わせて正確に調整されていれば、それほど冗長ではありません。少なくとも、ローカルに何かを実装する場合、それは言い訳です。そうでない場合は、上記で説明したメソッドvoid*と関数ポインタを使用して、物をコピー/破壊して一般化するだけです。

多くの場合、それは非常に一般化するデータ構造を倒すために多くの努力や多くのコードをとらない場合は、あなたの代わりにあなたの正確なユースケースに非常に狭く適用されます。例として、mallocすべてのノードで使用するパフォーマンスに勝るのは非常に簡単です(多くのノードで大量のメモリをプールするのとは対照的に)、非常に正確なユースケースで再訪する必要のないコードでの新しい実装がmalloc登場しても。それを打ち破り、コードを複雑にするのに一生かかるかもしれませんが、その一般性に合わせたい場合、人生の大部分を維持し、最新の状態に保つ必要があります。

別の例として、PixarまたはDreamworksが提供するVFXソリューションの10倍以上のソリューションを実装するのが非常に簡単であることがよくありました。睡眠中にできます。しかし、それは私の実装が優れているからではありません。彼らはほとんどの人にとって実に劣っています。これらは、私の非常に具体的なユースケースでのみ優れています。私のバージョンは、ピクサーやドリームワークよりもはるかに一般的に適用されません。彼らのソリューションは私の単純なソリューションと比較して絶対に素晴らしいので、それはばかげて不公平な比較ですが、それは一種のポイントです。比較は公平である必要はありません。必要なのがいくつかの非常に具体的なものだけである場合、データ構造に不要なものの無限のリストを処理させる必要はありません。

同種のビットとバイト

C に型安全性の本質的な欠如があるため、Cで活用する1つのことは、ビットとバイトの特性に基づいて物事を均一に格納するという考えです。メモリアロケーターとデータ構造の間の結果として、より多くのあいまいさがあります。

しかし、可変サイズのものや、ポリモーフィックやのような単に可変サイズにできるものを格納することは、効率的に行うのが困難です。ある要素から次の要素に到達するまでの歩幅が異なる可能性があるため、それらが可変サイズであり、単純なランダムアクセスコンテナに連続して格納できるという前提で行くことはできません。その結果、犬と猫の両方を含むリストを保存するには、3つの別個のデータ構造/アロケーターインスタンス(犬用、猫用、ベースポインターまたはスマートポインターのポリモーフィックリスト用、またはそれ以上)を使用する必要がある場合があります、各犬と猫を汎用アロケーターに割り当ててメモリ全体に分散します)、これは高価になり、キャッシュミスの倍増を招きます。DogCat

したがって、Cで使用する1つの戦略は、型の豊富さと安全性は低下しますが、ビットとバイトのレベルで一般化することです。あなたは、と仮定することができるかもしれないDogsCatsのビットと同じバイト数を必要とし、同じフィールド、関数ポインタのテーブルに同じポインタを持っています。しかし、その代わりに、より少ないデータ構造をコーディングできますが、同様に重要なこととして、これらすべてを効率的かつ連続的に保存します。その場合、あなたは犬と猫を類推的な労働組合のように扱っています(あるいは実際に労働組合を使うかもしれません)。

そして、それはタイプセーフティに多大なコストがかかります。Cで他の何よりも見逃していることが1つあるとしたら、それは型安全です。構造は、メモリの割り当て量と各データフィールドの配置方法を示すだけのアセンブリレベルに近づいています。しかし、それが実際にCを使用する一番の理由です。メモリレイアウトを制御しようとしている場合、すべてが割り当てられ、すべてが相互に関連して格納されている場合、多くの場合、ビットレベルで物事を考えるのに役立ちますバイト、および特定の問題を解決するために必要なビットとバイトの量。そこでは、Cの型システムの愚かさはハンディキャップよりも実際に有益になる可能性があります。通常、最終的には処理するデータ型がはるかに少なくなりますが、

錯覚/見かけの複製

今では、冗長ではないものに対して、ゆるい意味で「複製」を使用しています。「偶発的/見かけの」重複と「実際の重複」のような用語を区別する人を見てきました。私が見る方法は、多くの場合、そのような明確な区別がないということです。この区別は「潜在的な一意性」対「潜在的な重複」に似ており、どちらの方向にも進むことができます。多くの場合、設計と実装をどのように進化させたいか、特定のユースケースに合わせてどのように完全に調整するかによって異なります。しかし、コードの重複と思われるものが、何度か改善を繰り返した後、もはや冗長ではなくなっていることがよくわかりました。

使用して、単純な可変長配列の実装を取りrealloc、の類推相当しますstd::vector<int>。最初は、たとえばstd::vector<int>C ++ で使用すると冗長になる場合があります。しかし、ヒープの割り当てを必要とせずに16個の32ビット整数を挿入できるように、事前に64バイトを事前に割り当てることが有益であることが、測定を通じてわかる場合があります。少なくともを使用して、もはや冗長ではなくなりましたstd::vector<int>。そして、あなたは言うかもしれません、「しかし、これを新しいものに一般化SmallVector<int, 16>することはできます。しかし、あなたはそれが非常に小さい、短命の配列であり、ヒープ割り当ての配列容量を1.5(ほぼその量vector実装は、アレイの容量が常に2のべき乗であるという仮定を回避しながら使用します)。今、あなたのコンテナは本当に異なっており、おそらくそのようなコンテナはありません。そして、あなたは事前割り当ての重さをカスタマイズしたり、再割り当ての動作をカスタマイズしたりするためにテンプレートパラメータを追加することでそのような動作を一般化しようとするかもしれませんが、その時点で何十行もの単純なCに比べて本当に使いにくいものを見つけるかもしれませんコード。

そして、256ビットのアライメントされたパディングされたメモリを割り当て、AVX 256命令のPODのみを保存し、128バイトを事前に割り当てて一般的な小さな入力サイズのヒープ割り当てを回避し、容量が2倍になるデータ構造が必要になるポイントに達するかもしれません完全で、配列サイズを超えるが配列容量を超えない後続要素の安全な上書きを許可します。その時点で、少量のCコードの重複を避けるために解決策を一般化しようとしている場合、プログラミングの神があなたの魂に慈悲を持っているかもしれません。

そのため、特定のユースケースに合わせてソリューションをより完全に、より完全に一意で冗長ではないものに合わせて調整すると、最初は冗長に見え始めたものが成長し始めます。しかし、それは特定のユースケースに完全に合わせる余裕がある場合のみです。私たちの目的のために一般化された「まともな」ものが必要な場合もありますが、非常に一般化されたデータ構造の恩恵を最も受けます。しかし、特定のユースケースのために完璧に作られた例外的なものについては、「一般的な目的」と「私の目的のために完全に作られた」という考えは相容れないものになり始めています。

PODとプリミティブ

現在Cでは、POD、特にプリミティブを可能な限りデータ構造に格納する言い訳をよく見つけます。これはアンチパターンのように思えるかもしれませんが、C ++で頻繁に行っていた種類のコードよりもコードの保守性を向上させるのに、うっかり役立つことがわかりました。

簡単な例は、短い文字列をインターンすることです(通常、検索キーに使用される文字列の場合と同様に、非常に短い傾向があります)。実行時にサイズが変化するこれらすべての可変長文字列を処理するのが面倒なのはなぜですか(単純にヒープの割り当てと解放が必要になる可能性があるため)。スレッドセーフトライや文字列インターン専用に設計されたハッシュテーブルのような中央のデータ構造にこれらのものを保存してから、それらの文字列を単純な古いint32_tまたはで参照してください:

struct IternedString 
{
    int32_t index;
};

...辞書式ソートが不要な場合、ハッシュテーブル、赤黒木、スキップリストなどで?これで、32ビット整数で動作するようにコーディングした他のデータ構造はすべて、事実上32ビットであるこれらのインターン化された文字列キーを格納できるようになりましたints。そして、少なくともユースケースで見つけました(レイトレーシング、メッシュ処理、画像処理、パーティクルシステム、スクリプト言語へのバインド、低レベルマルチスレッドGUIキットの実装などの分野で作業しているので、私のドメインにすぎないかもしれません- OSほど低レベルではありません)、偶然にもこのようなものへのインデックスを保存するだけでコードがより効率的かつ単純になることがあります。それでうまくいくので、私はしばしば仕事をしています。int32_tfloat32 私の非自明なデータ構造、または同じサイズ(ほとんど常に32ビット)のものを保存するだけです。

そして当然、それがあなたのケースに当てはまるなら、そもそもごくわずかしか使っていないので、さまざまなデータ型の多くのデータ構造の実装を避けることができます。

テストと信頼性

私が最後に提供するものの1つは、すべての人に当てはまるわけではないかもしれませんが、それらのデータ構造のテストを作成することです。何かを本当に上手にします。信頼性が非常に高いことを確認してください。

重複したコードに連鎖的な変更を加える必要がある場合、コードの複製はメンテナンスの負担に過ぎないため、これらの場合、いくつかのマイナーなコードの複製ははるかに許されます。このような冗長なコードを変更する主な理由の1つを排除することで、コードの信頼性を高め、実行しようとしていることに本当に適しています。

私の美意識は長年にわたって変化しています。1つのライブラリがドット積または別のライブラリに既に実装されている単純なSLLロジックを実装しているのを見て、イライラすることはもうありません。物事が十分にテストされておらず、信頼できない場合にのみイライラします。重複したコードを通じてバグを複製するコードベースを真に扱っており、コピーアンドペーストコーディングの最悪のケースが、1つの中心的な場所に些細な変更を加えるべきであったものを、多くのエラーが発生しやすいカスケード変更に変えているのを見てきました。それでも多くの場合、それはテストが不十分だったためであり、コードがそもそも何をしているのかが信頼できず、上手くいかなかったためです。以前、バグのあるレガシーコードベースで作業していたとき、私の考えでは、すべての形式のコード複製は、バグを複製し、カスケード変更を必要とする可能性が非常に高いと関連付けていました。しかし、1つのことを非常にうまく確実に行うミニチュアライブラリでは、たとえ冗長なコードがあちこちにあるとしても、将来変更する理由はほとんどありません。品質の低さとテストの欠如よりも、複製が私を苛立たせたとき、私の優先事項は当時オフでした。これらの後者の事柄が最優先事項であるはずです。

ミニマリズムのためのコード複製?

これは私の頭の中に浮かんだ面白い考えですが、ほぼ同じことをするCおよびC ++ライブラリに遭遇する可能性がある場合を考えてみてください。そして最も重要なことは、両方とも有能に実装され、十分にテストされ、信頼できることです。残念ながら、完璧な並列比較に近いものを見つけたことがないので、ここで仮説的に話さなければなりません。しかし、この比較比較で私が見つけた最も近いものは、CライブラリがC ++の同等のもの(コードサイズの1/10の場合もあります)よりもはるかに小さいことがしばしばありました。

そして、その理由は、1つの正確なユースケースの代わりに最も広い範囲のユースケースを処理する一般的な方法で問題を解決するために、数百から数千行のコードが必要な場合がありますが、後者はダース。冗長性があるにも関わらず、C標準ライブラリは標準データ構造を提供することに関してはひどいものであるという事実にもかかわらず、同じ問題を解決するために人間の手でより少ないコードを生成することになることがよくあります。これら2つの言語間の人間の傾向の違いに。1つは非常に具体的なユースケースに対する解決を促進し、もう1つは最も広範なユースケースに対するより抽象的な一般的なソリューションを促進する傾向がありますが、これらの最終結果はそうではありません。

先日、GitHubで誰かのレイトレーサーを見ていましたが、C ++で実装されていたため、おもちゃのレイトレーサー用のコードが大量に必要でした。そして、コードを見るのにそれほど時間を費やしませんでしたが、レイトレーサーが必要とするよりもはるかに多くの方法で処理している汎用構造のボートがそこにありました。そして、このコーディングスタイルを認識しているのは、C ++をスーパーボトムアップのような方法で同じように使用していたためです。問題を手に入れ、次に実際の問題に取り組みます。しかし、これらの一般的な構造はあちこちで冗長性を排除し、新しいコンテキストで多くの再利用を楽しむことができますが、それと引き換えに、不必要なコード/機能のボート負荷と少しの冗長性を交換することにより、プロジェクトを非常に膨らませます。後者は前者よりも維持が必ずしも容易ではありません。それどころか、可能な限り幅広いニーズに対して設計決定のバランスをとる必要がある一般的なものの設計を維持するのが難しいため、維持するのが難しいことがよくあります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.