web-dev-qa-db-ja.com

C ++でCスタイルを使用する必要がありますか?

私が働いている会社でソフトウェアをどのように開発するべきかについて自分の立場を発展させてきたので、私は確信が持てないという結論に達しました。

あなたがC++でプログラミングしている場合、それが助けられ、パフォーマンスの向上が絶対に必要ない場合は、Cスタイルを使用しないでください。このようにして、ポインタの計算や、RAIIを使用せずに新しいリソースを作成するなどの操作を回避できます。このアイデアが実施された場合、char *を見ることはおそらく過去のものになります。

これが他の人の結論だと思いますか?それとも私はこれについてあまりにも純粋主義的ですか?

6
c.hughes

これは基本的に、標準のC++が(委員会とコミュニティによって)意図され、推奨される方法で使用されます。

  • c ++言語のイディオムを使用します(主に [〜#〜] raii [〜#〜] に基づいています、スマートポインターのように)
  • 避けられなくなるまでC言語のイディオムを使用しないでください(Cインターフェースとのインターフェース時に、定期的に発生します)

これは、ほぼ10年前から "modern C++"と呼んでいるものです。しかし、ほとんどのC++開発者は、コードが生のポインター、新規/削除、およびその他のエラーが発生しやすい構成要素を必要としないように見えることを理解するために、今開始します。

現在、これらの構造体(Cかどうかにかかわらず)は、互換性を保つためと、ライブラリを記述して他の開発者がそれらを操作する必要がないようにするための両方でまだ残っています。また、Cは、Cスタイルのコードを必要とする低レベルの構成体の場合、Cライブラリとのインターフェースに使用する必要があります。それ以外の場合は、C++を使用できる場合はCイディオムの使用を回避できます。

明確化のために:Cスタイルを使用してもパフォーマンスは向上しません(C++構成とRAIIを理解していると仮定します)。実際、C++で記述されたアルゴリズムの多くはCで記述されたものよりも高速です。これは、C++がコンパイラーにより多くの情報を提供して、呼び出しコンテキストで最適化できるようにするためです(たとえば、テンプレートアルゴリズム/型について考えています)。

したがって、C++を作成するときにパフォーマンスがCイディオムを使用する正当な理由であるとは限りません。

22
Klaim

C++は、使用するかしないかを選択できる多くの機能を提供する言語です。あなたの決定はに基づいている必要があります

a)使用するフレームワーク

b)アプリケーションのドメイン

c)現在のコードベース

フレームワーク

一部のフレームワークは、特定のスタイルに適しています。たとえば、Qt/KDE/Wtを使用している場合、通常はそれらのメモリモデルを使用し、独自のスマートポインターはありません。例外をそれほど使用することはほとんどありません。 QtのMOCには、テンプレートを使用してできることを制限するテンプレートに対する特定の影響もあります。

同時に、ブーストを多用しているコードは、他の人が 'Modern C++'と記述しているもので重くなる傾向があります。

次に、本質的にクラスを備えたCである、Googleで様式化されたC++があります。 Google C++ Style Guide

アプリケーションドメイン

パフォーマンスが重要なソフトウェア(金融、組み込みなど)をコーディングしている場合、C++の多くの「便利さ」は、メモリフットプリントまたはCPUサイクルを制限することを優先して、ウィンドウの外に出る可能性があります。 Cがプログラム全体の一部ではなく、プログラムのごく一部に使用する「正しい」言語である状況があります。 C++ではこれを行うことができるので、クレイジーマトリックス演算を行うときにポインター演算を行うことができますが、それを使用するモジュールを組み合わせるときに高度な抽象化を行うことができます。

現在のコードベース

現実の世界では、C++を望まない、またはC++が好きではない10人のCプログラマーがいる店の場合、より「モダン」なものよりも、Googleスタイルガイドのようなものにこだわるほうがずっと良いでしょう。すべてのライブラリがCで記述されており、それらをC++で接着するだけの場合、char *は常にstd :: stringを行き来するよりも意味があるかもしれません。

結論

C++は、あらゆる学術的意味で理想的な言語ではありませんが、非常にうまく機能する実用的な妥協策です。したがって、それを使用するときは、問題に対する非常に実用的なアプローチも必要であり、「物事を行う正しい方法」に行き詰まりすぎないようにする必要があります。

あなたの質問に答えるために:あなたはあまりにも純粋主義的かもしれないしそうでないかもしれませんが、それは詳細に依存します。

5
MrFox

このアイデアが実施された場合、char *を見ることはおそらく過去のものになります。

私はCとC++を一緒にブレンドする傾向があり、そのような面白い "C/C++"開発者になっています。私が以下に挙げる理由の少なくともいくつかは合法的になると期待しています。多くの場合、C++実装とCインターフェース、またはCライクな実装とC++インターフェースを使用していますが、ほとんどの場合、C++実装とC++インターフェースを使用しています。

CのインターフェースまたはCの実装に到達する理由はいくつかあります(重要度の高い順)。

  1. API/ABI(インターフェース)
  2. Exception-Safety(実装:ハァッ?)
  3. 型安全性の欠如(実装:え?)
  4. パフォーマンス(実装:まれ)
  5. ビルド時間(実装)
  6. 美学(実装)

API/ABI

私が取り組んできたコードベースの多くは常にかなり大きく、サードパーティが拡張できるように設計されています。プラグイン開発者は、実際に私たちの製品のプラグインを書くことに生計を立てています。その結果、中央の抽象インターフェースは、製品を拡張するためのプラグインを作成するときに、内部開発者とサードパーティ開発者の両方が実際に使用するソフトウェア開発キットです。

サードパーティのプラグイン作成者は、幅広いコンパイラと標準ライブラリの実装を使用しています。

この要件により、関連するABIの問題により、コアAPIレベルでC++インターフェースを使用することができなくなります。

中央のAPIを介して_std::vector_を渡し始めた場合、あるベンダーの実装から別のベンダーの実装へのABIの非互換性に直面することになり、実際には独自のベクターのようなコンテナーをロールバックしましたが、完全に標準に準拠して、 _std::vector_と一致することを確認するためのパフォーマンステストを使用して、範囲のアクター、範囲の消去など。

抽象インターフェイスをモデル化するためにvtablesと仮想関数を使用すると、プラグインの作成者が使用するのと同じコンパイラーを使用する必要があり、単一の仮想関数を導入した瞬間にABIも中断します。その結果、中心的なコアAPIレベルで設計するインターフェイスはCインターフェイスであり、ほとんどが関数ポインターのテーブルですが、RAIIに準拠するクラスにこれらをラップするプラグインに静的にリンクされているC++ラッパーSDKを使用しています。ラッパーは内部でプラグインにリンクされているため、標準コンテナーを自由に使用することもできます。

モジュールの境界を越えて例外処理を使用すると、同じジレンマに直面するため、モジュールの境界レベルでCエラーコードを使用します(ただし、例外との間で変換されることが多い)。静的にリンクされたC++ラッパーSDKは、これらのエラーコードを受け取り、例外に変換し直しますが、プラグイン内からプラグイン自体に安全にスローします。

例外安全パート1

これは奇妙な問題ですが、スタックのアンワインド中に例外をスローすると_std::terminate_の動作が発生し、プロセスが強制終了されるため、何らかのリソースのリリースで外部エラーが発生すると、非常に危険です。もう1つは、キャッチハンドラから再帰的にスローすることです。そうすると、中止してはならない外部トランザクションが中止されます。

これらの場合、失敗する可能性のある何らかの理由でリソースを割り当てようとすることがあります(まれですが、必要な場合があります)。たとえば、一般的なデストラクタからのリソースの解放中、またはスコープガードのデストラクタ内からの副作用のロールバック中に、あいまいなケースでメモリを割り当てる必要があるかもしれません。

これらの場合、失敗は重大ではなく、無視できることがあります(理想的ではありませんが、完全な例外安全性はhard!)。たとえば、メモリの割り当てに失敗すると、一部の作業をスキップできる場合がありますが、これは絶対に重要なことではありません(メッセージのロギングなど)。これらのケースでは、リリース/ロールバック/キャッチコンテキストで頻繁にトリガーされる関数の再帰的なスローの可能性を回避するために、失敗に対してCスタイルのエラーコードを使用する方が簡単だと思います。

例外安全パート2

ABIとやり取りする場合、特に呼び出し元が呼び出し先とは異なるコンパイラ/ビルド設定を使用している場合は、別のモジュール内の呼び出し先内から呼び出し元に例外をスローするのは安全ではありません。

その結果、APIの境界を越えてリークする前に、例外を飲み込む必要があります。このため、C APIの比較的単純な実装がある場合は、Cをスローして、スローできない可能性があるため、スローする可能性について心配する必要がないようにします。 APIへのすべてのエントリポイントにtry/catchブロックを振りかける。

タイプの安全性の欠如

さて、これは奇妙なものですが、ハードウェアと低レベルの通信を行ったり、メモリを生のビットとバイトとして扱う場合、タイプセーフは、必然的に実際に大量のコードを必要とするコードと干渉する可能性があります。醜い明示的なキャスト。一例は、中央のC APIを通じて提供される固定アロケーターです。その場合、メモリアロケーターは型の安全性にあまり役立たないため、C++ではなくCで実装を行うことがよくあります。これらはビットおよびバイトレベルで機能します。

パフォーマンス(注意!)

これは、上記のタイプセーフの欠如に非常に関連しています。 Cスタイルの方が高速であるとは限りません。 CPUキャッシュの相互作用などについてどのように注意を払っているのかに関して非常に重要なことをする場合、あちこちに生のポインタなどを避けるのは難しいというだけです。

次のようなストライドインターフェースデザインを考えてみます。

_void process_points(int num_points, 
                    float* x, int stride,
                    float* y, int stride,
                    float* z, int stride);
_

それを非常に安全なデザインにラップするのは本当に難しいです。結局のところ、ストライドは、渡されたこれらの浮動小数点をまたがるストライドを使用する必要があるため、連続したインデックスアクセスさえ提供されない可能性があります(バイトをスキップしているだけかもしれません)。

これは残念ながらタイプセーフティに違反しますが、これらの設計は多くの場合、非常に重い負荷を処理し、線形複雑度の要件を満たしていない(すべてに触れなければならない)インターフェースには不可欠ですが、個々のフィールドにSoA SIMDアクセスのような潜在的な最適化を有効にしますsizeof(float)に一致するストライド。

私は以前にすべての種類の型安全なラッパーを使用してこれらの種類のインターフェイスを半安全にしようとしましたが、ビルド時間のインフレーションが好きではなかっただけでなく、(型の安全性は欠けていますが)美的感覚が好きになりましたカスタムストライド(OpenGL APIスタイル)で一括して渡されたプレーンな古いデータ。

これは、パーティクルシステムのように、コードベースの非常に選択された領域のみを対象としています。また、非常に危険であるため、私がよくテストを作成するデザインでもあります。

ビルド時間

これは少し迷信的かもしれませんが、同じ数のヘッダーなどを含めても、C++ではビルドがより高速になり、適切な場合はC++でpimplsなどを慎重に使用します。 Cのようなコードを書くときの傾向が原因かもしれませんが、結果として、ビルド時間を維持するためにC APIの背後でそれほど複雑ではないものを実装するときにCを使用する傾向がありますダウン。

ビルド時間は本当に嫌いなので、パフォーマンスが重要ではないもの(LuaJITでは、パフォーマンスがわずかに重要ないくつかのことを行うこともできます)の大量のコードには、可能な限りLuaを使用し、次にC++中程度の量のコード、そして引用されたこれらの理由のための少量のコードのためのC。

美学

これはおそらくそれらすべての中で最も偏ったセクションですが、時々私は軽量でミニマリストのCの美学が好きなだけです。型の安全性、豊富なインターフェースとデータ型を設計する能力、汎用性などの点でC++が大好きです。

だから私は時々Cの「気分」にいるだけです。単純な領域のいくつかのために、C APIインターフェースの1つをC実装で任意に実装することを選択します。それは恐ろしい理由だと思いますが、両方の言語が美しく、融合できると思うので、それを含めたかったのです。一方が他方に取って代わる必要はありません。

結論

とにかく、これらは主にC++を使用している場合でも、CまたはCのようなコーディングを時々使用する主な理由です。それらが大きな理由かどうかはわかりません。私はそのような独断的な迷信的なプログラマーにならないようにしていますが、Cをあちこちに到達することは有益であり、複数のコンパイラやdylib APIを使用する複数の言語を対象とする場合にABIに関しては不可欠であることがわかりました。

5
user204677

Cスタイルで書くべきケースが1つあります。これはハードウェア関連のプログラミングです。 「生のC」がC++言語で保持される主な理由は、下位互換性だけではありませんが、「生のC」によってC++を必要最小限のハードウェアアプリケーションで使用できるようになります。 OSが存在する場合、またはOS自体を作成している場合。

パフォーマンスに関する議論もあります。理想主義者は、生の配列を使用すべきではなく、std :: vectorのみを使用すべきであり、プレーン配列と同じくらい高速でなければならないことを教えてくれます-そうでなければ、コンパイラは悪いです。これはユートピアです。まだ見ていません。これまでのところ、STLとテンプレートは「ベアボーン」コードよりも低速のままです。ただし、テンプレートやSTLの抽象化などを使用するC++プログラマーが増えるほど、コンパイラーはより高速なコードを生成するようにプレッシャーをかけられます。

Cの機能がC++から削除された場合、現在の汎用言語ほど多くはなくなります。組み込みシステムやパフォーマンス重視のアプリケーションではそれほど使用されず、JavaまたはC#などのデスクトップ専用の言語になります。

2
user29079

答えは明らかにあなたの行動に依存するため、私がよく知っている特定のアプリケーションである、高性能パケット処理に焦点を当てます。

ネットワークパケットでは、ポインタ演算によってメモリの場所にアクセスする必要があります。このアクセスをC++クラスの背後に隠すことはできますが、Cスタイルのポインター演算を完全に取り除くことはできません。ほとんどのバイナリファイル形式についても同じことが言えます。生のメモリブロックにアクセスする必要があります。

また、多くのライブラリはCスタイルでのみ使用できます。実際には、C++スタイルのクラスでCシステムコールのほとんどを抽象化する優れたライブラリはおそらく見つかりません。したがって、必要な機能のサブセットに対して、上記のライブラリを実装する必要があります。

したがって、結論は、CスタイルのプログラミングはC++スタイルのクラスの背後に隠れている可能性があるということですが、実務ではCスタイルのプログラミングを完全に回避することはできません。下位互換性には理由があります。

1
juhist