私は初心者の C++ プログラマーなので、ベクトルではなく配列を使用することを学びました (これが一般的な方法のようで、後でベクトルに移ります)。
SO に関する多くの回答が、配列よりもベクトルを使用し、文字配列よりも文字列を使用することを提案していることに気付きました。これが C++ でコーディングする「適切な」方法のようです。
とはいえ、古典的な配列/ char *を使用する価値があるのはいつですか(もしあれば)?
他のプロジェクトで使用する必要があるコードを記述する場合。特に、STL が存在しない可能性がある特別なプラットフォーム (組み込み、ゲーム コンソールなど) をターゲットにする場合。
古いプロジェクトや特別な要件を持つプロジェクトでは、STL ライブラリへの依存関係を導入したくない場合があります。配列、char* などに依存するインターフェイスは、言語の一部であるため、あらゆるものと互換性があります。ただし、STL がすべてのビルド環境に存在するとは限りません。
一度もない。
生の配列がベクトルよりも優れたソリューションであると思われる場合 (ここで他の理由により) 、C++11 コンパイラ (またはboost::array ) でstd::tr1::arrayまたは std::array を使用します。とにかく確実に行うチェックを行うだけで、サイズ値の使用により DRY が自動的に実装されます (たとえば、配列宣言の将来の変更が自動的に機能するように、ループでサイズを使用します)。
とにかく、配列の実装はチェック付きの生の配列であり、サイズ定数が提供されているため、埋め込みコードでも配列コードを取得するのは簡単です。コンパイラがテンプレートをサポートしている限り、コード内のブースト ヘッダーをコピーして、生の配列の代わりにこれを使用できるようにします。生の配列で間違いを犯すのは明らかに簡単すぎるからです。生の配列は悪です。エラーが発生しやすいです。
また、STL アルゴリズム (利用可能な場合) で非常にうまく機能します。
現在、生の配列を使用する必要がある場合(義務) が 2 つあります: C のみのコードを使用している場合 (C コードと通信するのではなく、C ライブラリのようにコードの C のみの部分を記述している場合)。しかし、それは別の言語です。
もう 1 つの理由は、コンパイラがテンプレートをまったくサポートしていない場合です...
この質問は、実際には 2 つの部分に分けることができます。
個人的には、STL を使用しないコードとの互換性を維持する必要がある場合 (つまり、ストレートな C コードとインターフェースする場合) を除いて、メモリ管理に std::vector を使用することを好みます。new または malloc によって割り当てられた生の配列を使用して、例外セーフなコードを作成するのははるかに困難です (その理由の 1 つは、心配する必要があることを簡単に忘れてしまうためです)。理由については、RAIIに関する記事を参照してください。
実際には、std::vector はフラット配列として実装されます。そのため、生の配列を引き出して、C スタイルのアクセス パターンを使用することは常に可能です。私は通常、ベクトル添え字演算子の構文から始めます。一部のコンパイラでは、デバッグ バージョンを生成するときに、ベクトルによって自動境界チェックが提供されます。これは遅いですが (多くの場合、タイトなループでは 10 倍の速度低下)、特定の種類のバグを見つけるのに役立ちます。
特定のプラットフォームでのプロファイリングにより、operator[] がボトルネックであることが示された場合は、生配列への直接アクセスに切り替えます。興味深いことに、コンパイラと OS によっては、生の配列よりも STL ベクトルを使用する方が高速な場合があります。
以下は、簡単なテスト アプリケーションの結果です。Visual Studio 2008 で /O2 最適化を使用して 32 ビット リリース モードでコンパイルされ、Vista x64 で実行されます。64 ビット テスト アプリケーションでも同様の結果が得られます。
Binary search...
fill vector (for reference) : 0.27 s
array with ptr math : 0.38 s <-- C-style pointers lose
array with int index : 0.23 s <-- [] on raw array wins
array with ptrdiff_t index : 0.24 s
vector with int index : 0.30 s <-- small penalty for vector abstraction
vector with ptrdiff_t index : 0.30 s
Counting memory (de)allocation...
memset (for reference) : 2.85 s
fill malloc-ed raw array with [] : 2.66 s
fill malloc-ed raw array with ptr : 2.81 s
fill new-ed raw array with [] : 2.64 s
fill new-ed raw array with ptr : 2.65 s
fill vector as array : 3.06 s \ something's slower
fill vector : 3.05 s / with vector!
NOT counting memory (de)allocation...
memset (for reference) : 2.57 s
fill malloc-ed raw array with [] : 2.86 s
fill malloc-ed raw array with ptr : 2.60 s
fill new-ed raw array with [] : 2.63 s
fill new-ed raw array with ptr : 2.78 s
fill vector as array : 2.49 s \ after discounting the
fill vector : 2.54 s / (de)allocation vector is faster!
コード:
#define WINDOWS_LEAN_AND_MEAN
#include <windows.h>
#include <string>
#include <vector>
#include <stdio.h>
using namespace std;
__int64 freq; // initialized in main
int const N = 1024*1024*1024/sizeof(int)/2; // 1/2 GB of data
int const nIter = 10;
class Timer {
public:
Timer(char *name) : name(name) {
QueryPerformanceCounter((LARGE_INTEGER*)&start);
}
~Timer() {
__int64 stop;
QueryPerformanceCounter((LARGE_INTEGER*)&stop);
printf(" %36s : % 4.2f s\n", name.c_str(), (stop - start)/double(freq));
}
private:
string const name;
__int64 start;
};
template <typename Container, typename Index>
int binarySearch_indexed(Container sortedArray, Index first, Index last, int key) {
while (first <= last) {
Index mid = (first + last) / 2; // NOT safe if (first+last) is too big!
if (key > sortedArray[mid]) first = mid + 1;
else if (key < sortedArray[mid]) last = mid - 1;
else return mid;
}
return 0; // Use "(Index)-1" in real code
}
int Dummy = -1;
int const *binarySearch_ptr(int const *first, int const *last, int key) {
while (first <= last) {
int const *mid = (int const *)(((unsigned __int64)first + (unsigned __int64)last) / 2);
if (key > *mid) first = mid + 1;
else if (key < *mid) last = mid - 1;
else return mid;
}
return &Dummy; // no NULL checks: don't do this for real
}
void timeFillWithAlloc() {
printf("Counting memory (de)allocation...\n");
{
Timer tt("memset (for reference)");
int *data = (int*)malloc(N*sizeof(int));
for (int it=0; it<nIter; it++) memset(data, 0, N*sizeof(int));
free(data);
}
{
Timer tt("fill malloc-ed raw array with []");
int *data = (int*)malloc(N*sizeof(int));
for (int it=0; it<nIter; it++) for (size_t i=0; i<N; i++) data[i] = (int)i;
free(data);
}
{
Timer tt("fill malloc-ed raw array with ptr");
int *data = (int*)malloc(N*sizeof(int));
for (int it=0; it<nIter; it++) {
int *d = data;
for (size_t i=0; i<N; i++) *d++ = (int)i;
}
free(data);
}
{
Timer tt("fill new-ed raw array with []");
int *data = new int[N];
for (int it=0; it<nIter; it++) for (size_t i=0; i<N; i++) data[i] = (int)i;
delete [] data;
}
{
Timer tt("fill new-ed raw array with ptr");
int *data = new int[N];
for (int it=0; it<nIter; it++) {
int *d = data;
for (size_t i=0; i<N; i++) *d++ = (int)i;
}
delete [] data;
}
{
Timer tt("fill vector as array");
vector<int> data(N);
for (int it=0; it<nIter; it++) {
int *d = &data[0];
for (size_t i=0; i<N; i++) *d++ = (int)i;
}
}
{
Timer tt("fill vector");
vector<int> data(N);
for (int it=0; it<nIter; it++) for (size_t i=0; i<N; i++) data[i] = (int)i;
}
printf("\n");
}
void timeFillNoAlloc() {
printf("NOT counting memory (de)allocation...\n");
{
int *data = (int*)malloc(N*sizeof(int));
{
Timer tt("memset (for reference)");
for (int it=0; it<nIter; it++) memset(data, 0, N*sizeof(int));
}
free(data);
}
{
int *data = (int*)malloc(N*sizeof(int));
{
Timer tt("fill malloc-ed raw array with []");
for (int it=0; it<nIter; it++) for (size_t i=0; i<N; i++) data[i] = (int)i;
}
free(data);
}
{
int *data = (int*)malloc(N*sizeof(int));
{
Timer tt("fill malloc-ed raw array with ptr");
for (int it=0; it<nIter; it++) {
int *d = data;
for (size_t i=0; i<N; i++) *d++ = (int)i;
}
}
free(data);
}
{
int *data = new int[N];
{
Timer tt("fill new-ed raw array with []");
for (int it=0; it<nIter; it++) for (size_t i=0; i<N; i++) data[i] = (int)i;
}
delete [] data;
}
{
int *data = new int[N];
{
Timer tt("fill new-ed raw array with ptr");
for (int it=0; it<nIter; it++) {
int *d = data;
for (size_t i=0; i<N; i++) *d++ = (int)i;
}
}
delete [] data;
}
{
vector<int> data(N);
{
Timer tt("fill vector as array");
for (int it=0; it<nIter; it++) {
int *d = &data[0];
for (size_t i=0; i<N; i++) *d++ = (int)i;
}
}
}
{
vector<int> data(N);
{
Timer tt("fill vector");
for (int it=0; it<nIter; it++) for (size_t i=0; i<N; i++) data[i] = (int)i;
}
}
printf("\n");
}
void timeBinarySearch() {
printf("Binary search...\n");
vector<int> data(N);
{
Timer tt("fill vector (for reference)");
for (size_t i=0; i<N; i++) data[i] = (int)i;
}
{
Timer tt("array with ptr math");
int sum = 0;
for (int i=-1000000; i<1000000; i++) {
sum += *binarySearch_ptr(&data[0], &data[0]+data.size(), i);
}
}
{
Timer tt("array with int index");
int sum = 0;
for (int i=-1000000; i<1000000; i++) {
sum += data[binarySearch_indexed<int const *, int>(
&data[0], 0, (int)data.size(), -1)];
}
}
{
Timer tt("array with ptrdiff_t index");
int sum = 0;
for (int i=-1000000; i<1000000; i++) {
sum += data[binarySearch_indexed<int const *, ptrdiff_t>(
&data[0], 0, (ptrdiff_t)data.size(), -1)];
}
}
{
Timer tt("vector with int index");
int sum = 0;
for (int i=-1000000; i<1000000; i++) {
sum += data[binarySearch_indexed<vector<int> const &, int>(
data, 0, (int)data.size(), -1)];
}
}
{
Timer tt("vector with ptrdiff_t index");
int sum = 0;
for (int i=-1000000; i<1000000; i++) {
sum += data[binarySearch_indexed<vector<int> const &, ptrdiff_t>(
data, 0, (ptrdiff_t)data.size(), -1)];
}
}
printf("\n");
}
int main(int argc, char **argv)
{
QueryPerformanceFrequency((LARGE_INTEGER*)&freq);
timeBinarySearch();
timeFillWithAlloc();
timeFillNoAlloc();
return 0;
}
Array/char* は、互換性またはパフォーマンスが非常に優先される場合に役立ちます。ベクトルと文字列は、コードの保守性、読みやすさ、および全体的な使いやすさが重要な場合に優れた高レベルのオブジェクトです。ほとんどの場合、そうです。
コンパイル時にサイズがわかっている場合は常に配列を使用することをお勧めします。vector を使用できますが、vector にはヒープで行われるメモリ割り当てに関連するオーバーヘッドが伴うことを覚えておく必要があります。サイズがわからない場合は、もちろんベクトルを使用します。
構造化データへのアクセスが必要な共有ライブラリに取り組んでいます。このデータはコンパイル時に認識されるため、POD (plain old data) 構造体のファイル スコープの定数配列を使用してデータを保持します。
これにより、コンパイラとリンカはほとんどのデータを読み取り専用セクションに配置し、次の 2 つの利点があります。
唯一の例外は、コンパイラが浮動小数点定数をロードするための初期化コードを引き続き生成するため、浮動小数点数を含む構造体はすべて書き込み可能なセクションになります。これは、浮動例外または浮動小数点丸めモードと関係があると思われますが、どちらの仮説を検証するかはわかりません。
これにベクトル オブジェクトと文字列オブジェクトを使用すると、共有ライブラリが読み込まれるたびに実行される初期化コードがコンパイラによってさらに多く生成されます。定数データはヒープに割り当てられるため、プロセス間で共有できません。
ディスク上のファイルからデータを読み取った場合、C++ コンパイラに代わってデータのフォーマットをチェックするのではなく、データのフォーマットをチェックする必要があります。また、このグローバル データが最初から「焼き付けられた」コードベース内のデータの有効期間を管理する必要もあります。
私が考えることができる唯一の理由は速度です。対応するオブジェクトよりも配列/ポインター型でより適切な最適化を行うことができます。しかし、データ構造が保持する必要があるデータの量を完全に知っていれば、STL を使用することさえあります。最適化ステップで STL からプリミティブ型に変更する方が、読みにくいコードでプロジェクトを開始するよりも優れています。