C ++に関する5つの人気のある神話、パート1

1.はじめに



この記事では、C ++についての5つの人気のある神話を探り、解明しようとします。



1 C ++を理解するには、まずCを学ぶ必要があります

2 。 C ++はオブジェクト指向プログラミング言語です

。 信頼できるプログラムにはガベージコレクションが必要です

4 。 効率を上げるには、低レベルのコードを書く必要があります

5 。 C ++は、大規模で複雑なプログラムにのみ適しています。



あなたやあなたの同僚がこれらの神話を信じているなら、この記事はあなたのためです。 いくつかの神話は、誰かに、ある時点でのいくつかのタスクに当てはまります。 ただし、ISO C ++ 2011コンパイラを使用する今日のC ++は、これらの主張を神話にします。



私はよく耳にするので、彼らは私に人気があるようです。 時にはそれらは合理的に証明されますが、公理としてより頻繁に使用されます。 多くの場合、問題の解決策の1つとしてC ++を置き換えるために使用されます。



本はそれぞれの神話に捧げることができますが、私は単純な陳述とそれらに対する私の議論の簡潔な陳述に自分自身を制限します。



2.神話1:C ++を理解するには、まずCを学ぶ必要があります



いや C ++では、Cよりプログラミングの基本を学ぶのが簡単です。Cは、C ++のほとんどのサブセットですが、C ++が持つ単純なタスクを簡単に実行できる型安全性と便利なライブラリがないため、C ++の一部ではありません。 メールアドレスを作成する簡単な例を考えてみましょう:



string compose(const string& name, const string& domain) { return name+'@'+domain; }
      
      







次のように使用されます。



 string addr = compose("gre","research.att.com");
      
      







当然、実際のプログラムでは、すべての引数が文字列ではありません。



Cバージョンでは、シンボルとメモリを直接操作する必要があります。



 char* compose(const char* name, const char* domain) { char* res = malloc(strlen(name)+strlen(domain)+2); //   , '@',  0 char* p = strcpy(res,name); p += strlen(name); *p = '@'; strcpy(p+1,domain); return res; }
      
      







次のように使用されます。



 char* addr = compose("gre","research.att.com"); // … free(addr); //    
      
      







どのオプションを教えるのが簡単ですか? どちらが使いやすいですか? Cバージョンで何か混乱したことがありますか? そう? なんで?



最後に、compose()のどのバージョンがより効率的ですか? C ++-引数の文字を数える必要がなく、短い文字列に動的メモリを使用しないため。



2.1 C ++の学習


これは奇妙なエキゾチックな例ではありません。 私の意見では、それは典型的です。 では、なぜ多くの教師が「First C」アプローチを説くのでしょうか? なぜなら:



-彼らはいつもやった

-カリキュラムに必要なもの

-彼ら自身がそう勉強したこと

-CはC ++よりも小さいため、より簡単になります。

-学生は気にしない、遅かれ早かれ、Cを学ばなければならない



しかし、CはC ++の最も単純なまたは最も有用なサブセットではありません。 十分なC ++を知っていると、Cを簡単に学ぶことができます。C++の前にCを勉強すると、C ++で簡単に回避できる多くのエラーが発生し、それらを回避する方法を学ぶことに時間を費やします。 C ++を学習するための正しいアプローチについては、私の著書「プログラミング:C ++を使用した原則と実践」を参照してください。 最後に、Cの使用方法に関する章もあります。これは、多くの学生の指導に使用されています。 研究を簡素化するために、第2版ではC ++ 11およびC ++ 14を使用します。



C ++ 11のおかげで、C ++は初心者にとってより使いやすくなりました。 たとえば、要素のシーケンスによって初期化された標準ライブラリのベクトルは次のとおりです。



 vector<int> v = {1,2,3,5,8,13};
      
      







C ++ 98では、配列でのみリストを初期化できました。 C ++ 11では、任意の型のリスト{}を受け入れるコンストラクターを指定できます。 ベクトルサイクルを実行できます。



 for (int x : v) test(x);
      
      







test()は、v要素ごとに呼び出されます。



forループは任意のシーケンスを通過できるため、次のように記述できます。



 for (int x : {1,2,3,5,8,13}) test(x);
      
      







C ++ 11では、シンプルなものをシンプルにしようとしました。 当然、速度を犠牲にすることなく。



3.神話2:C ++はオブジェクト指向プログラミング言語です



いや C ++はOOPおよびその他のスタイルをサポートしていますが、特に制限されていません。 OOPや汎用プログラミングなどのソフトウェアスタイルの合成をサポートしています。 多くの場合、最良の解決策はいくつかのスタイルを使用することです。 最良の手段とは、より短く、よりわかりやすく、効率的で、整備されているなどです。



この神話は、あらゆる種類の仮想関数を備えた大規模なクラス階層を必要としない限り、C ++(Cと比較して)を必要としないという結論に人々を導きます。 神話を確信していたC ++は、純粋にオブジェクト指向ではないという事実を非難しています。 「良い」と「OOP」を同一視すると、OOPに関係のない多くのすべてを含むC ++が自動的に「不良」になります。 いずれにせよ、この神話はC ++を学ばない言い訳です。



例:



 void rotate_and_draw(vector<Shape*>& vs, int r) { for_each(vs.begin(),vs.end(), [](Shape* p) { p->rotate(r); }); //    vs for (Shape* p : vs) p->draw(); //    vs }
      
      







OOPですか? もちろん、クラス階層と仮想関数があります。 これは一般的なプログラミングですか? もちろん、パラメーター化されたコンテナー(ベクター)と通常の関数があります。



for_each。 関数型プログラミングですか? そのようなもの。 ラムダが使用されます(構築[])。 そして、このスタイルは何ですか? これは、C ++ 11のモダンなスタイルです。



可能性を示すために、標準のforループとfor_eachライブラリアルゴリズムの両方を使用しました。 このコードでは、いずれか1つのループのみを使用します。



3.1一般化されたプログラミング。


より一般的なコードが必要ですか? 最終的に、シェイプ上のポインターベクトルでのみ機能します。 リストとインライン配列はどうですか? shared_ptrやunique_ptrなどのスマートポインターはどうですか? そして、シェイプとは呼ばれないが、描画()および回転()できるオブジェクトは? 注意してください:



 template<typename Iter> void rotate_and_draw(Iter first, Iter last, int r) { for_each(first,last,[](auto p) { p->rotate(r); }); //    [first:last) for (auto p = first; p!=last; ++p) p->draw(); //    [first:last) }
      
      







これはどのシーケンスでも機能します。 これは標準ライブラリのアルゴリズムのスタイルです。 autoを使用して、オブジェクトのインターフェイスタイプに名前を付けません。 これはC ++ 11の機能であり、「初期化中に使用された式のタイプを使用する」ことを意味するため、pのタイプは最初と同じになります。



別の例:



 void user(list<unique_ptr<Shape>>& lus, Container<Blob>& vb) { rotate_and_draw(lus.begin(),lus.end()); rotate_and_draw(begin(vb),end(vb)); }
      
      







ここで、Blobは描画()および回転()操作を持つ特定のグラフィックタイプであり、コンテナはコンテナの一種です。 標準ライブラリのリスト(std :: list)には、シーケンスの実行に役立つbegin()およびend()メソッドがあります。 これは美しい古典的なOOPです。 しかし、コンテナがハーフオープンシーケンス反復の標準反復をサポートしていない場合はどうでしょうか[b:e)? begin()およびend()メソッドがない場合 さて、通過できないコンテナのようなものを見たことがないので、begin()とend()を別々に定義できます。 標準ライブラリは、Cスタイルの配列に対してこの機能を提供するため、ContainerがCの配列である場合、問題は解決します。



3.2適応


ケースはより複雑です:コンテナーにオブジェクトへのポインターが含まれていて、アクセスと通過のための異なるモデルがある場合はどうでしょうか? たとえば、次のように彼に連絡する必要があります。



 for (auto p = c.first(); p!=nullptr; p=c.next()) { /*  -  *p */}
      
      







このスタイルは珍しくありません。 次のようなシーケンス[b:e)に減らすことができます。



 template<typename T> struct Iter { T* current; Container<T>& c; }; template<typename T> Iter<T> begin(Container<T>& c) { return Iter<T>{c.first(),c}; } template<typename T> Iter<T> end(Container<T>& c) { return Iter<T>{nullptr,c}; } template<typename T> Iter<T> operator++(Iter<T> p) { p.current = pcnext(); return p; } template<typename T> T* operator*(Iter<T> p) { return p.current; }
      
      







この変更は積極的ではありません。標準C ++ライブラリでサポートされているパスモデルにそれをもたらすために、コンテナまたはそのクラスの階層を変更する必要はありませんでした。 これは適応であり、リファクタリングではありません。 このような一般的なプログラミング手法が標準ライブラリに限定されないことを示すために、この例を選択しました。 また、「OO」の定義に該当しません。



C ++コードはオブジェクト指向(階層と仮想関数をどこでも使用する)でなければならないという概念は、プログラムの速度に悪影響を及ぼします。 実行時に一連の型を解析する必要がある場合、これは良いアプローチであり、私はよくそれを使用します。 ただし、かなり柔軟性がなく(すべての型が階層に収まるわけではありません)、仮想関数を呼び出すとインライン化が妨げられ、プログラムが50回ごとに遅くなる可能性があります



4.神話3:信頼できるプログラムではガベージコレクションが必要です。



ガベージコレクションは適切ですが、未使用のメモリの回復には最適ではありません。 これは万能薬ではありません。 メモリは直接占有されない場合があり、多くのリソースは単なるメモリではありません。 例:



 class Filter { //     iname      oname public: Filter(const string& iname, const string& oname); //  ~Filter(); //  // … private: ifstream is; ofstream os; // … };
      
      







Filterコンストラクターは2つのファイルを開きます。 その後、特定のタスクが実行され、ファイルからの入力が受け入れられ、結果が別のファイルに出力されます。 Filterでタスクをハードコード化してラムダとして使用するか、仮想関数をオーバーロードする継承クラスが提供する関数として使用できます。 リソース管理については、問題ではありません。 次のようにフィルターを定義できます。



 void user() { Filter flt {“books”,”authors”}; Filter* p = new Filter{“novels”,”favorites”}; //  flt  *p delete p; }
      
      







リソース管理の観点から見ると、問題は、ファイルが閉じられ、2つのスレッドに関連付けられたリソースが将来の使用のために正しく返されるようにする方法です。



ガベージコレクターに依存するシステムでの通常の解決策は、削除とデストラクタを削除することです(ガベージコレクタにデストラクタが含まれることはめったにないため、アルゴリズムの問​​題につながり、パフォーマンスに悪影響を与える可能性があるため、デストラクタを避けるほうがよいためです)。 ガベージコレクターはすべてのメモリをクリアできますが、ファイル(ファイル)を閉じて、メモリ(ロック)ではなくスレッドに関連するすべてのリソースを返す必要があります。 メモリは自動的に返されますが、他のリソースの管理は手動で実行されるため、リークやエラーが発生しやすいことがわかります。



C ++で一般的で推奨されるアプローチは、デストラクタに依存してリソースが返されるようにすることです。 通常、コンストラクターでリソースが削除されるため、この手法には「Resource Acquisition Is Initialization」(RAII)という名前が付けられます。 ユーザー()で、fltデストラクタは暗黙的にisおよびosスレッドデストラクタを呼び出します。 次に、ファイルを閉じて、スレッド関連のリソースを解放します。 deleteは* pに対しても同じことを行います。



最新のC ++の経験豊富なユーザーは、ユーザー()が不格好でエラーが発生しやすいことに気付くでしょう。 だからそれは良いでしょう:



 void user2() { Filter flt {“books”,”authors”}; unique_ptr<Filter> p {new Filter{“novels”,”favorites”}}; //  flt  *p }
      
      







現在、ユーザーの終了時に()* pは自動的に解放されます。 プログラマはこれを忘れないでしょう。 unique_ptrは、組み込みポインターと比較してパフォーマンスやメモリを犠牲にすることなく、リソースを確実に解放する標準ライブラリクラスです。



このソリューションは冗長すぎる(フィルターの繰り返し)ため、通常のポインター(新規)とスマート(unique_ptr)のコンストラクターを分離するには最適化が必要です。 これを改善するには、C ++ 14 make_uniqueヘルパー関数を使用します。この関数は、指定されたタイプのオブジェクトを作成し、それを指すunique_ptrを返します。



 void user3() { Filter flt {“books”,”authors”}; auto p = make_unique<Filter>(“novels”,”favorites”); //  flt  *p }
      
      







または、ポインターを介してすべてを書き込むために2番目のフィルターが必要でない限り、さらに優れたオプションです。



 void user4() { Filter flt {“books”,”authors”}; Filter flt2 {“novels”,”favorites”}; //  flt  flt2 }
      
      







要するに、より単純で、より明確で、より高速です。



しかし、Filterデストラクタは何をしますか? フィルタリソースを解放します-ファイルを閉じます(デストラクタを呼び出します)。 これは暗黙的に行われるため、Filterから他に何も必要ない場合は、そのデストラクタについての言及を取り除いて、コンパイラにそれ自体をすべて実行させることができます。 したがって、次のように書くだけです。



 class Filter { //     iname      oname public: Filter(const string& iname, const string& oname); // … private: ifstream is; ofstream os; // … }; void user3() { Filter flt {“books”,”authors”}; Filter flt2 {“novels”,”favorites”}; //  flt  flt2 }
      
      







このエントリは、自動ガベージコレクションを使用する言語(Java、C#)のほとんどのエントリよりも単純であり、物忘れによるリークはありません。 また、明らかな代替手段よりも高速です。



これがリソース管理の理想です。 メモリだけでなく、ファイル、ストリーム、ロックなどの他のリソースも管理します。 しかし、それは本当に包括的なものですか? 明らかな所有者が1人もいないオブジェクトについてはどうですか?



4.1所有権の移転:移転


スコープ間でオブジェクトを転送する問題を考慮してください。 問題は、不要なコピーやエラーの発生しやすいポインターの使用を行わずに、大量の情報をスコープから取得する方法です。 従来使用されていたポインター:



 X* make_X() { X* p = new X: // …  X … return p; } void user() { X* q = make_X(); // …  *q … delete q; }
      
      







そして、オブジェクトを削除する責任は誰にありますか? make_X()を呼び出す単純なケースではありますが、一般的なケースでは、答えはそれほど明白ではありません。 make_X()がオブジェクトをキャッシュしてメモリ使用量を最小限に抑えるとどうなりますか? user()はother_user()にポインターを渡す必要がありますか? 混乱する可能性のある場所は多くあり、このプログラミングスタイルではリークは珍しくありません。 shared_ptrまたはunique_ptrを使用して、オブジェクトの所有者を直接決定できます。



 unique_ptr<X> make_X();
      
      







しかし、なぜポインターを使用するのでしょうか? 多くの場合、それは必要ではありません。多くの場合、オブジェクトの通常の使用から注意をそらします。 たとえば、Matrix加算関数は2つの引数の新しいオブジェクト、合計を作成しますが、ポインターを返すと奇妙なコードになります。



 unique_ptr<Matrix> operator+(const Matrix& a, const Matrix& b); Matrix res = *(a+b);
      
      







*文字は、ポインターではなく、合計を含むオブジェクトを取得するために必要です。 本当に必要なのは、オブジェクトへのポインタではなく、オブジェクトです。 小さなオブジェクトはすぐにコピーされ、ポインターは使用しません。



 double sqrt(double); //    double s2 = sqrt(2); //     
      
      







一方、大量のデータを含むオブジェクトは通常、そのデータのハンドラーです。 istream、string、vector、list、thread-これらはすべて、ほんの数バイトを使用してより大きなデータにアクセスします。 マトリックスの追加に戻ります。 必要なもの:



 Matrix operator+(const Matrix& a, const Matrix& b); //   a  b Matrix r = x+y;
      
      







簡単:



 Matrix operator+(const Matrix& a, const Matrix& b) { Matrix res; // …  res  … return res; }
      
      







デフォルトでは、res要素はrにコピーされますが、resは削除され、そのメモリは解放されるため、要素をコピーする必要はありません。要素を「盗む」ことができます。 これはC ++の最初の日から行うことができましたが、実装するのは難しく、誰もがこの手法を理解していませんでした。 C ++ 11は、オブジェクトの所有権を転送する移動操作の形式で、「表現の盗難」を直接サポートします。 double要素の単純な2次元マトリックスを考えます。



 class Matrix { double* elem; //    int nrow; //   int ncol; //   public: Matrix(int nr, int nc) // :   :elem{new double[nr*nc]}, nrow{nr}, ncol{nc} { for(int i=0; i<nr*nc; ++i) elem[i]=0; //  } Matrix(const Matrix&); //   Matrix operator=(const Matrix&); //   Matrix(Matrix&&); //   Matrix operator=(Matrix&&); //   ~Matrix() { delete[] elem; } // :   // … };
      
      







コピー操作は&によって認識されます。 移動操作-&&による。 移動操作はビューを「スチール」し、「空のオブジェクト」を残す必要があります。 マトリックスの場合、これは次のようなものを意味します。



 Matrix::Matrix(Matrix&& a) //   :nrow{a.nrow}, ncol{a.ncol}, elem{a.elem} // “”  { a.elem = nullptr; //     }
      
      







以上です。 コンパイラーがreturn resを検出すると、 彼は解像度がすぐに破壊されることを理解しています。 返却後は使用されません。 次に、コピーする代わりに移動コンストラクターを使用して戻り値を渡します。 のために



 Matrix r = a+b;
      
      







演算子+()内のresは空になります。 デストラクタに残っている作業はほとんどなく、rはres要素を所有しています。 関数(演算子+())から結果の要素(メガバイトのメモリの場合もあります)を変数に取得しました。 そして、彼らは最小限のコストでそれをしました。



C ++の専門家は、場合によっては、優れたコンパイラーがコピーを完全に排除して返すことができると指摘しています。 しかし、それはそれらの実装に依存します。そして、単純なことの速度がコンパイラーがどれほどスマートに出会ったかに依存するのは好きではありません。 さらに、コピーを排除するコンパイラは、再配置も排除できます。 大量の情報をあるスコープから別のスコープに移動する複雑さとコストを排除するための、シンプルで信頼性の高い普遍的な方法があります。



さらに、移動セマンティクスは割り当てに対して機能するため、



 r = a+b;
      
      







割り当て演算子の移動最適化を取得します。 コンパイラへの割り当てを最適化することはさらに困難です。



多くの場合、これらすべてのコピーおよび移動操作を定義する必要さえありません。 クラスが期待どおりに動作するメンバーで構成されている場合、デフォルトの操作に単純に依存できます。 例:



 class Matrix { vector<double> elem; //  int nrow; //   int ncol; //   public: Matrix(int nr, int nc) // constructor: allocate elements :elem(nr*nc), nrow{nr}, ncol{nc} { } // … };
      
      







このオプションは前のオプションと同じように動作しますが、エラーをより適切に処理し、スペースをもう少し使用する点が異なります(ベクトルは通常3ワードです)。



ハンドラーではないハンドルはどうですか? intやcomplexのように小さい場合でも心配する必要はありません。 それ以外の場合は、ハンドラーを作成するか、スマートポインターunique_ptrおよびshared_ptrを介してそれらを返します。 裸の操作newとdeleteを使用しないでください。 残念ながら、この例のマトリックスは標準のISO C ++ライブラリには含まれていませんが、いくつかのライブラリがあります。 たとえば、「Origin Matrix Sutton」を探し、その実装に関するコメントについては、C ++プログラミング言語(第4版)の第29章を参照してください。



パート2



All Articles