Effective STL条項16

5474 ワード

条項16:vectorとstringのデータを従来のAPIにどのように伝えるか
C++言語は1998年に標準化されたため、C++の中堅分子はプログラマーが配列からvectorに移行するように努力している間に何の心配もなかった.開発者がchar*ポインタからstringオブジェクトに移行しようとする過程でも明らかです.これらの変更には、一般的なプログラミングエラーを解消し(条項13を参照)、STLアルゴリズムのすべての強力な能力を得る機会がある(例えば、条項31を参照).
しかし、障害はまだあります.最も一般的なのは、vectorやstringオブジェクトではなく、既存の従来のCスタイルのAPIが受け入れている配列とchar*ポインタです.このようなAPI関数はまた長い間存在し,STLを効率的に使用するにはそれらと平和に共存しなければならない.
幸いなことに、これは簡単です.vectorオブジェクトvがある場合、&v[0]を使用すれば、vのデータを指すポインタを得る必要があります.stringオブジェクトsに対して、対応する呪文は簡単なs.c_です.str().でも読み取り専用です.広告の難解な条文がよく指摘されているように、必然的にいくつかの制限があります.
1つ与える
vector<int> v;

式v[0]はvectorの最初の要素を指す参照を生成するので、&v[0]はその最初の要素を指すポインタである.vectorの要素はC++規格によって連続メモリに格納されるように限定されており、配列のようなものであるため、このようなCスタイルのAPIにvを渡す場合は、次のようにします.
void doSomething(const int* pInts, size_t numInts);

私たちはこのようにすることができます.
doSomething(&v[0], v.size());

そうかもしれません.たぶんね.唯一の問題は、vが空であればです.もしそうなら、v.size()は0であり、&v[0]は存在しないものを指すポインタを生成しようとする.これはいいことではない.結果は定義されていません.より安全な方法は次のとおりです.
if (!v.empty()) {
 doSomething(&v[0], v.size());
}

道を間違えたら、中途半端な人物にぶつかるかもしれません.彼らはあなたに&v[0]の代わりにv.begin()を使うことができると言っています.beginはvectorの内部を指す反復器に戻りますが、vectorの場合、その反復器は実際にはポインタです.それは常に正しいが、条項50が言ったように、必ずしもそうではないので、これに依存してはいけない.beginの戻りタイプはポインタではなくiteratorであり、vector内部のデータを指すポインタが必要な場合はbeginを使用しないでください.いくつかの理由に基づいてv.begin()を入力することを決定した場合は、&*v.begin()を入力する必要があります.これは&v[0]と同じポインタを生成するため、より多くのタイプの機会を持つことができ、他のコードを理解する人にもっと難解な感じを与えることができます.率直に言って、&v[0]の代わりにv.begin()を使うことを教えてくれた人と付き合っているなら、社交界を考え直すべきです.(注:VC 6では&v[0]の代わりにv.begin()を使うとコンパイラは何も言わないが、VC 7やGCCでそうするとコンパイルエラーが発生する)
vectorから内部データへのポインタを取得する方法と同様に、stringは信頼できない.(1)stringのデータは連続メモリに格納されることを約束していないため、(2)stringの内部表現形式はnull文字で終わることを約束していない.これはstringのメンバー関数c_を説明する.strが存在する理由は、stringの値を指すCスタイルの設計ポインタを返します.stringオブジェクトsをこの関数に渡すことができます
void doSomething(const char *pString);

次のようになります.
doSomething(s.c_str());

文字列の長さが0であっても動作します.その場合、c_strはnull文字を指すポインタを返します.文字列の内部にnullが含まれている場合でも、同じように動作します.しかし、本当にそうであれば、doSomethingは最初のnullを文字列の終わりとして解釈する可能性が高い.stringオブジェクトは,終端子が収容されているかどうかは気にしないが,char*ベースのCスタイルAPIは気にする.
doSomethingの声明をもう一度見てみましょう.
void doSomething(const int* pInts, size_t numInts);
void doSomething(const char *pString);

両方の形式では、ポインタはconstを指すポインタとして渡される.vectorおよびstringのデータは、APIを変更せずに読み取りのみに送信することができる.これは今までで一番安全なことです.stringにとって、これも唯一できることです.c_と約束していないからです.strが生成するポインタはstringデータの内部表現形式を指す.CスタイルAPIのフォーマットに対する要求を満たすポインタがデータを指す修正不可能なコピーを返すことができます.△もしこの恐喝があなたを不気味にさせたら、安心してください.それは成立しないかもしれませんから.今のところどのライブラリの実現がこの自由権を使ったのか聞いていません.
vectorの場合、より柔軟性があります.要素を変更するCスタイルAPIにvを渡すと、典型的には問題ありませんが、呼び出された関数はvectorの要素の個数を変更しようとすることはできません.たとえば、vectorがまだ使用していない容量で新しい要素を「作成」しようとすることはできません.そうすると、vはもう自分の正確な大きさを知らないので、内部状態が一致しません.v.size()は不正な結果を得ます.また、呼び出された関数が、サイズと容量(条項14参照)が等しいvectorにデータを追加しようとすると、本当に災害的なイベントが発生します.私はそれを想像したくない.本当に恐ろしいです.
私が前の「典型的な状況は大丈夫」という言葉で「典型的に」という言葉を使っていることに気づきましたか?もちろん気づいたよ.一部のvectorでは、データに追加の制限がありますが、vectorデータを変更する必要があるAPIにvectorを渡すと、これらの追加の制限が満たされ続けることを確認する必要があります.例えば、条項23は、順序付けされたvectorが関連容器を実現するために実行可能な選択であることを説明するが、これらのvectorにとって順序を維持することは非常に重要である.ソートされたvectorをデータを変更する可能性のあるAPI関数に渡す場合は、呼び出しが返された後もvectorが順序を維持しないことを重視する必要があります.
CスタイルAPIで返される要素でvectorを初期化したい場合は、vectorと配列の潜在的なメモリ分散互換性を使用してvecotrを格納する要素の空間をAPI関数に渡すことができます.
// C API:              ,     arraySize double
//           。      double ,    maxNumDoubles
size_t fillArray(double *pArray, size_t arraySize);
vector<double> vd(maxNumDoubles);   //     vector,
      //      maxNumDoubles
vd.resize(fillArray(&vd[0], vd.size()));  //  fillArray   
      //   vd,    vd   
      //  fillArray       

このテクニックはvectorでのみ動作します.vectorだけが配列と同じ潜在的なメモリ分布を約束しているからです.ただし、CスタイルAPIのデータからstringオブジェクトを初期化したい場合は、簡単です.APIがvectorにデータを入れ、vectorからstringにコピーする限り:
// C API:              ,     arraySize char
//           。      char ,    maxNumChars
size_t fillString(char *pArray, size_t arraySize);
vector<char> vc(maxNumChars);   //     vector,
      //      maxNumChars
size_t charsWritten = fillString(&vc[0], vc.size()); //  fillString     vc
string s(vc.begin(), vc.begin()+charsWritten); //  vc        
      //      s(    5)

実際、CスタイルAPIにvectorにデータを入れて、実際に欲しいSTLコンテナにコピーするアイデアは常に有効です.
size_t fillArray(double *pArray, size_t arraySize); //   
vector<double> vd(maxNumDoubles);   //     
vd.resize(fillArray(&vd[0], vd.size());
deque<double> d(vd.begin(), vd.end());  //      deque
list<double> l(vd.begin(), vd.end());  //      list
set<double> s(vd.begin(), vd.end());   //      set

さらに,vectorとstring以外のSTLコンテナがそれらのデータをCスタイルAPIにどのように伝えるかを示唆した.コンテナの各データをvectorにコピーし、APIに渡す限り:
void doSomething(const int* pInts, size_t numInts); // C API (  )
set<int> intSet;     //       API   set
...
vector<int> v(intSet.begin(), intSet.end());  //   set   vector
if (!v.empty()) doSomething(&v[0], v.size());  //      API

データを配列にコピーして、配列をCスタイルのAPIに渡すこともできますが、なぜそうしたいのですか?コンパイル期間中にコンテナのサイズを知らない限り、動的配列を割り当てなければなりません.条項13では、動的配列の代わりにvectorを常に使用すべき理由を説明します.