Linus Torvaldsによるこの有名な怒り を読んだ後、C++のプログラマーにとって実際にすべての落とし穴は何なのかと思いました。私は この質問とその回答 で扱われているタイプミスや不正なプログラムフローを明示的に言及していませんが、コンパイラによって検出されず、明らかなバグを引き起こさない、より高レベルのエラーを指します最初の実行、完全な設計エラー、Cではありそうもないことですが、コードの完全な意味を理解していない新参者によってC++で行われる可能性があります。
また、通常は予想されないパフォーマンスの大幅な低下を指摘する回答も歓迎します。私が書いたLR(1)パーサージェネレーターについて教授がかつて私に言ったことの例:
不要な継承と仮想性のインスタンスが多すぎます。継承は設計をはるかに複雑にします(RTTI(ランタイム型推論)サブシステムのために非効率的です)。したがって、それは意味のある場所でのみ使用する必要があります。解析テーブルのアクション用。テンプレートを集中的に使用するので、実際には継承は必要ありません。」
トーバルズはここで彼のお尻から話している。
OK、なぜ彼は彼のお尻から話しているのですか:
まず第一に、彼の怒りは本当に何も怒っていません。ここには実際のコンテンツはほとんどありません。それが本当に有名であるか、軽度に尊敬されている唯一の理由は、それがLinux Godによって作成されたからです。彼の主な主張は、C++はがらくたであり、C++の人々を怒らせるのが好きだということです。もちろんそれに答える理由はまったくなく、それを合理的な議論と考える人はとにかく会話以上のものです。
彼の最も客観的なポイントとして光っているかもしれないものに関して:
基本的に、トーバルズは彼のお尻から話している。何についても明確な議論はなされていません。そのようなナンセンスの深刻な反論を期待するのは、まったくばかげています。私は、私がそれを言ったところにあれば、拡大すると予想される何かの反論を「拡大」するように言われています。もしあなたが本当に、トーバルズが言ったことを正直に見て、彼が実際には何も言わなかったことがわかるでしょう。
神が言ったからといって、それが意味をなすことを意味しないか、ランダムなボゾが言ったよりも真剣に受け止められるべきです。正直に言うと、神は単なる別のボゾなのです。
実際の質問への回答:
おそらく、最悪の、そして最も一般的な、悪いC++の習慣は、Cのように扱うことです。printf、gets(Cでも悪いと考えられています)、strtokなどのC API関数の継続的な使用は、提供されたパワーを活用できないだけではありませんより厳密な型システムにより、「実際の」C++コードとやり取りしようとすると、必然的にさらなる複雑化につながります。したがって、基本的には、トーバルズが忠告していることとは正反対のことをしてください。
STLとBoostを活用して、バグのコンパイル時間をさらに検出し、他の一般的な方法で簡単に生活できるようにします(たとえば、Boost Tokenizerはタイプセーフであり、優れたインターフェイスです)。最初は気が遠くなるようなテンプレートエラーの読み方を学ぶ必要があるのは事実ですが、(とにかく私の経験では)実行時に未定義の動作を生成するものをデバッグしようとするよりもはるかに簡単です。とても簡単です。
Cがそれほど良くないことは言うまでもありません。もちろんC++の方が好きです。 CプログラマーはCがより好きです。トレードオフと主観的な好みが関係しています。また、多くの誤った情報やFUDが浮かんでいます。 C++についてはFUDや誤解のほうが多いと思いますが、私はこの点に偏っています。たとえば、C++にあると思われる「膨らみ」と「パフォーマンス」の問題は、ほとんどの場合実際には大きな問題ではなく、確かに現実の比率から吹き飛ばされています。
教授が言及している問題については、これらはC++に固有のものではありません。 OOP(および一般的なプログラミング)では、継承よりも合成を優先します。継承は、すべてのOO言語に存在する可能な最強の結合関係です。C++が追加しますもう1つ、より強力な友情です。ポリモーフィック継承は、抽象化と「is-a」関係を表すために使用する必要があります。再利用には使用しないでください。これは、C++で2番目に大きな間違いであり、かなり大きなものです、しかしそれは言語に固有のものからはほど遠いです。C#またはJavaでも非常に複雑な継承関係を作成することができ、それらはまったく同じ問題を抱えることになります。
私はいつも、C++の危険性は、経験の浅いCクラスのプログラマーによって非常に誇張されていると思っていました。
はい、C++はJavaのようなものよりもピックアップするのが難しいですが、最新の技術を使用してプログラミングする場合、堅牢なプログラムを書くのはかなり簡単です。正直なところ、Javaなどの言語で行うよりもC++でプログラミングするのがthat時間のプログラミングがはるかに難しいため、他の言語で設計するときに、テンプレートやRAIIなどの特定のC++抽象化が欠けていることがよくあります。
とはいえ、C++で何年にもわたってプログラミングを行った後でも、私は時々、高レベルの言語では不可能な、非常に愚かな間違いを犯します。 C++での一般的な落とし穴の1つは、オブジェクトの有効期間を無視することです。JavaおよびC#では、通常、オブジェクトの有効期間を気にする必要はありません*。すべてのオブジェクトがヒープ上に存在し、管理されているためです。魔法のガベージコレクターによって。
さて、最近のC++では通常オブジェクトの存続期間についてあまり気にする必要はありません。オブジェクトの存続期間を管理するデストラクタとスマートポインタがあります。 99%の確率で、これは素晴らしい働きをします。しかし、時々、ぶら下がっているポインター(または参照)によってねじ込まれます。たとえば、最近、別のオブジェクトへの内部参照変数を含むオブジェクト(Foo
と呼びましょう)ができました( Bar
)としましょう。ある時点で、私はBar
がFoo
の前にスコープから外れるように愚かに配置しましたが、Foo
のデストラクタはBar
のメンバー関数を呼び出しました。言うまでもなく、うまくいかなかった。
今、私はこれについてC++を本当に非難することはできません。それは私自身の悪いデザインでしたが、要点は、この種のことは、より高いレベルの管理された言語では起こらないということです。スマートポインターなどでも、オブジェクトの寿命を認識する必要がある場合があります。
*管理されているリソースがメモリの場合。
try/catch
ブロックの過剰使用。
File file("some.txt");
try
{
/**/
file.close();
}
catch(std::exception const& e)
{
file.close();
}
これは通常Javaのような言語に由来し、C++にはfinalize
句がないと主張します。
しかし、このコードには2つの問題があります。
file
に存在しないファイルをclose
することができないため、catch
をtry/catch
の前にビルドする必要があります。これにより、「スコープリーク」が発生し、file
は閉じた後に表示されます。あなたはブロックを追加できますが...:/return
スコープの真ん中にtry
を追加した場合、ファイルは閉じられません(そのため、人々はfinalize
句の欠如に悩まされます)ただし、C++では、この問題に対処するためのより効率的な方法があります。
finalize
using
defer
私たちにはRAIIがあり、その非常に興味深い特性はSBRM
(Scoped Bound Resources Management)としてまとめられます。
デストラクタが所有するリソースをクリーンアップするようにクラスを作成することにより、すべてのユーザーにリソースを管理する責任を負わせません!
これはthe機能です。他の言語では見逃しており、おそらく最も忘れられている機能です。
真実は、ロギングなしでの終了を回避するために、最上位で離れてC++でtry/catch
ブロックを書く必要さえほとんどないということです。
コードの違いは通常、言語よりもプログラマに関連しています。特に、優れたC++プログラマーとCプログラマーは、どちらも同じように優れた(異なる場合でも)ソリューションを実現します。現在、Cは(言語として)より単純な言語であり、これは、抽象化が少なく、コードが実際に行うことに対する可視性が高いことを意味します。
彼の怒りの一部(彼はC++に対する彼の怒りで知られています)は、より多くの人々がC++に取り組み、抽象化の一部が隠し間違った仮定を実際に理解せずにコードを書くという事実に基づいています。
基準に当てはまる1つの一般的なエラーは、クラスに割り当てられたメモリを処理するときにコピーコンストラクターがどのように機能するかを理解していないことです。 'noob'がオブジェクトをマップまたはベクターに入れ、コピーコンストラクターとデストラクターを適切に記述しなかったため、クラッシュまたはメモリリークの修正に費やした時間のカウントを失いました。
残念ながら、C++はこのような「隠された」落とし穴でいっぱいです。しかし、それについて不平を言うのは、あなたがフランスに行って、人々が何を言っているのか理解できなかったと不平を言うようなものです。あなたがそこに行くつもりなら、言語を学びましょう。
C++は、さまざまな機能とプログラミングスタイルを許可しますが、C++を実際に使用するのに優れた方法であるとは限りません。実際、C++を誤って使用するのは非常に簡単です。
学習して適切に理解する である必要があります。実行する(または他の言語を使用するように使用する)だけで学習すると、効率が悪く、エラーが発生しやすくなります。
まあ...初心者には C++ FAQ Lite を読むことができます
その後、数人がC++の複雑さについての本を書く経歴を築いてきました。
Herb Sutter および Scott Meyers つまり。
トーバルズの怒りが実体を欠いていることに関して...真剣に人々に来てください:言語のニュアンスに対処することについてそれほど多くのインクが流出した他の言語はありません。あなたのPython&Ruby&Java本はすべてアプリケーションの作成に焦点を当てています... C++本は愚かな言語機能に焦点を当てています/ tips/traps。
テンプレートが多すぎると、最初はバグが発生しない可能性があります。けれども時が経つにつれ、人々はそのコードを修正する必要があり、巨大なテンプレートを理解するのに苦労するでしょう。それはバグが入るときです-誤解は「コンパイルして実行する」コメントを引き起こし、それはしばしばほとんどではないが正しいコードにつながります。
一般的に私が3レベルのディープジェネリックテンプレートを実行しているのを見た場合、私は立ち止まり、それを1つに減らす方法を考えます。多くの場合、問題は関数またはクラスを抽出することで解決されます。
警告:これは、「ユーザーの知らない」が彼の回答でリンクした講演の批評ほど多くの回答ではありません。
彼の最初の主なポイントは、(おそらく)「常に変化する標準」です。実際には、彼が挙げた例はすべてC++の変更に関連していますbefore標準がありました。 1998年(最初のC++標準が完成したとき)から、言語への変更はごくわずかでした-実際、多くの人が本当の問題はmoreの変更が必要であると主張しています。元のC++標準に準拠したすべてのコードが、現在の標準に引き続き準拠していることは、かなり確実です。 somewhatはそれほど確実ではありませんが、何かがすぐに(そしてまったく予想外に)変更されない限り、次のC++標準でも同じことがほぼ当てはまります(理論的には、export
を使用したすべてのコードは機能しなくなりますが、実質的には機能しません存在する;実用的な観点からそれは問題ではない)。私は、そのような主張をすることができる他のいくつかの言語、OS(または他の多くのコンピュータ関連)を考えることができます。
その後、彼は「常に変化するスタイル」に入ります。繰り返しますが、彼の主張のほとんどは、ナンセンスにかなり近いです。彼はfor (int i=0; i<n;i++)
を「古くて破壊された」とfor (int i(0); i!=n;++i)
を「新しい熱さ」として特徴づけようとします。実際には、そのような変更が意味をなす可能性のある型はありますが、int
の場合、違いはありません。そして、何かを得られたとしても、良いまたは正しいコードを書くために必要になることはめったにありません。最高の状態でも、彼はモグラから山を作っています。
彼の次の主張は、C++が「間違った方向に最適化している」ということです。具体的には、優れたライブラリを使用するのは簡単であると認めている一方で、C++は「優れたライブラリの作成をほぼ不可能にする」と主張しています。ここで、私は彼の最も根本的な間違いの1つだと思います。実際には、ほぼany言語に適したライブラリを作成することは非常に困難です。少なくとも、適切なライブラリを作成するには、問題のあるドメインを十分に理解している必要があるため、コードはそのドメイン内の(または関連する)多数の可能なアプリケーションで機能します。 C++ reallyが行うことのほとんどは「レベルを上げる」ことです-ライブラリがどれほど優れているかを見た後canがそうであると、人々は、そうでなければ、彼らはドレックします。彼はまた、少数のreally優れたプログラマーがかなりの数のライブラリーを作成するという事実を無視します。それは、「私たちの残りの人」が(簡単に、彼が認めるように)使用できるようになります。これは本当に「それはバグではなく、機能です」というケースです。
私はすべてのポイントを順番にヒットする(ページを取得する)ことはしませんが、彼の終了ポイントに直接スキップします。 「プログラム全体の最適化を使用して、未使用の仮想関数テーブルとRTTIデータを排除できます。このような分析は、動的リンクを使用しない比較的小さなプログラムに特に適しています」と彼はBjarneを引用していると述べています。
彼は、「これは本当に難しい問題である」という支持されていない主張をすることでこれを批判し、それを停止問題と比較するまでさえ行っています。実際には、これは一種の問題ではありません。実際、Zortech C++に含まれているリンカー(1980年代のMS-DOS用のfirst C++コンパイラー)がこれを行いました。余分な可能性のあるデータがすべて削除されたことを確認することは困難ですが、かなり公平な仕事をするのは完全に合理的です。
しかし、それにもかかわらず、はるかに重要な点は、これはどんな場合でもほとんどのプログラマにとってまったく無関係であるということです。かなりのコードを逆アセンブルした私たちが知っているように、ライブラリをまったく使用せずにアセンブリ言語を作成しない限り、実行可能ファイルにはほぼ確実にかなりの量の「もの」(通常の場合はコードとデータの両方)が含まれます実際に使用していることは言うまでもなく、おそらく知らないでしょう。ほとんどの人にとって、ほとんどの場合、それは問題ではありません-あなたが最も小さな組み込みシステム用に開発しているのでなければ、その余分なストレージの消費は単に無関係です。
結局のところ、この暴言にはライナスの愚かさよりも実体が少し多いのは事実ですが、それはまさにそれに値するかすかな賞賛でのろのろを与えているのです。
避けられない状況のためにC++でコーディングしなければならなかったCプログラマーとして、ここに私の経験があります。私が使用しているものはC++でほとんどなく、ほとんどがCに固執しています。主な理由は、C++を十分に理解していないためです。私には、C++の複雑さと、それに優れたコードを記述する方法を示すメンターがいませんでした。そして、非常に優れたC++コードからのガイダンスがなければ、C++で優れたコードを書くことは非常に困難です。私見これはC++の最大の欠点です。初心者を手に入れようとする優れたC++プログラマーが手に入れるのは難しいからです。
通常見られるパフォーマンスヒットの一部は、STLの魔法のメモリ割り当てによるものです(そうです、アロケータを変更できますが、C++で始めたときに誰が変更しますか?)。ベクトルは配列を内部で使用し、抽象化は非常に効率的であるため、ベクトルと配列は同様のパフォーマンスを提供するというC++の専門家の意見をよく耳にします。ベクトルアクセスと既存の値の変更については、これが実際に当てはまることがわかりました。しかし、新しいエントリの追加、ベクターの構築および破棄には当てはまりません。 gprofは、アプリケーションの累積時間の25%が、ベクトルコンストラクター、デストラクタ、memmove(新しい要素を追加するためのベクトル全体の再配置)およびその他のオーバーロードされたベクトルオペレーター(++など)に費やされたことを示しました。
同じアプリケーションでは、somethingBigを表すためにsomethingSmallのベクターが使用されました。 somethingBigでsomethingSmallにランダムアクセスする必要はありませんでした。それでもリストの代わりにベクトルが使用されました。ベクトルが使用された理由は?元のコーダーは、ベクトルの構文のような配列に精通しており、リストに必要なイテレーターにはあまり精通していなかったため(そう、彼はCのバックグラウンドからです)。 C++を正しく実行するには、専門家による多くのガイダンスが必要であることを証明し続けます。 Cは、抽象化がまったくない基本的な構成をほとんど提供していないため、C++よりもはるかに簡単に正しく構築できます。
私はLinus Thorvaldsが好きですが、この怒りには実質がありません-ただ怒りです。
実質的な暴言を見たい場合は、「C++が環境に悪い理由、地球温暖化を引き起こし、子犬を殺す理由」 http://chaosradio.ccc.de/camp2007_m4v_1951.html 追加資料: http://www.fefe.de/c++/
面白い話、私見
STLとboostは、ソースコードレベルで移植可能です。 Linusが話していることは、C++にはABI(アプリケーションバイナリインターフェース)がないことだと思います。したがって、リンクするすべてのライブラリを同じコンパイラバージョンと同じスイッチでコンパイルするか、DLL境界でC ABIに制限する必要があります。私もannyoing ..を見つけますが、サードパーティのライブラリを作成しているのでない限り、ビルド環境を制御できるはずです。私は自分をC ABIに制限することは問題を起こす価値がないと思います。あるDLLから別のDLLに文字列、ベクトル、およびスマートポインターを渡すことができるという便利さは、コンパイラーのアップグレードまたはコンパイラースイッチの変更時にすべてのライブラリーを再構築する手間を省く価値があります。私が従う黄金のルールは:
-実装ではなくインターフェイスを再利用するために継承
-継承よりも集約を優先
-可能な限り無料の関数をメンバーメソッドより優先する
-常にRAIIイディオムを使用して、コードを非常に安全な例外にします。キャッチしようとしないでください。
-スマートポインターを使用し、ネイキッド(非所有)ポインターを回避する
-参照セマンティクスよりも値セマンティクスを優先
-ホイールを再発明せず、stlとboostを使用する
-Pimplイディオムを使用してプライベートを非表示にするか、コンパイラファイアウォールを提供する