38

私は過去数日間これを調査してきましたが、これまでのところ、独断的な議論や伝統への訴え(つまり「C ++の方法です!」 )以外に説得力のあるものは見つかりませんでした。

オブジェクトの配列を作成している場合、以下を使用する理由は何ですか(使いやすさ以外)。

#define MY_ARRAY_SIZE 10

//  ...

my_object * my_array=new my_object [MY_ARRAY_SIZE];

for (int i=0;i<MY_ARRAY_SIZE;++i) my_array[i]=my_object(i);

以上

#define MEMORY_ERROR -1
#define MY_ARRAY_SIZE 10

//  ...

my_object * my_array=(my_object *)malloc(sizeof(my_object)*MY_ARRAY_SIZE);
if (my_object==NULL) throw MEMORY_ERROR;

for (int i=0;i<MY_ARRAY_SIZE;++i) new (my_array+i) my_object (i);

私が知る限り、後者は前者よりもはるかに効率的であり(メモリをいくつかの非ランダム値に初期化したり、デフォルトのコンストラクターを不必要に呼び出したりしないため)、唯一の違いは、クリーンアップするという事実です。 :

delete [] my_array;

そしてあなたが片付ける他のもの:

for (int i=0;i<MY_ARRAY_SIZE;++i) my_array[i].~T();

free(my_array);

私はやむを得ない理由で外出しています。それがC++(Cではない)であり、したがって使用されるべきではないという事実にアピールすることはmallocfree私が知る限り、独断的であるほど説得力がありません。私が見逃しているものよりも優れているものはありますか?new []malloc

つまり、私が知る限りnew []、デフォルトのパラメーターなしのコンストラクターを持たないものの配列を作成するために、mallocメソッドを使用することはできますが、まったく使用することはできません。

4

11 に答える 11

64

私はやむを得ない理由で外出しています。

それはあなたが「強制」をどのように定義するかに依存します。あなたの提案はC++で裸の配列を割り当てる標準的な方法ではないため、これまでに拒否した議論の多くは、ほとんどのC++プログラマーにとって確かに説得力があります。

単純な事実はこれです:はい、あなたは絶対にあなたが説明する方法で物事を行うことができます。あなたが説明していることが機能しない理由はありません。

しかし、繰り返しになりますが、Cで仮想関数を使用できます。時間と労力を費やせば、プレーンCでクラスと継承を実装できます。それらも完全に機能します。

したがって、重要なのは何かが機能するかどうかではありませ。しかし、コストについてはもっと詳しく。C++よりもCで継承と仮想関数を実装する方がエラーが発生しやすくなります。Cで実装するには複数の方法があり、互換性のない実装になります。一方、これらはC ++のファーストクラスの言語機能であるため、誰かがその言語が提供するものを手動で実装する可能性はほとんどありません。したがって、すべての継承と仮想関数はC++のルールと連携できます。

これについても同じことが言えます。では、手動のmalloc / freeアレイ管理によるメリットとロスは何ですか?

私がこれから言おうとしていることのどれもがあなたにとって「やむを得ない理由」であるとは言えません。あなたが決心したように見えるので、私はむしろそうなるとは思えません。しかし、記録のために:

パフォーマンス

あなたは次のように主張します:

私が知る限り、後者は前者よりもはるかに効率的であり(メモリをいくつかの非ランダム値に初期化したり、デフォルトのコンストラクターを不必要に呼び出したりしないため)、唯一の違いは、クリーンアップするという事実です。 :

このステートメントは、効率の向上が主に問題のオブジェクトの構築にあることを示唆しています。つまり、どのコンストラクターが呼び出されます。このステートメントは、デフォルトのコンストラクターを呼び出さないことを前提としています。配列を作成するためだけにデフォルトのコンストラクターを使用してから、実際の初期化関数を使用して実際のデータをオブジェクトに配置します。

ええと...それがあなたがやりたいことではない場合はどうなりますか?あなたがしたいことが空の配列を作成することである場合はどうなりますか?それはデフォルトで構築されていますか?この場合、この利点は完全になくなります。

脆弱性

配列内の各オブジェクトには、特殊なコンストラクターなどが必要であり、配列の初期化にはこの種のものが必要であると想定します。しかし、あなたの破壊コードを考慮してください:

for (int i=0;i<MY_ARRAY_SIZE;++i) my_array[i].~T();

単純なケースでは、これで問題ありません。オブジェクトの数を示すマクロ変数またはconst変数があります。そして、各要素をループしてデータを破棄します。これは簡単な例としては素晴らしいことです。

ここで、例ではなく、実際のアプリケーションについて考えてみましょう。アレイを作成する場所はいくつありますか?数十?数百?forアレイを初期化するために、一人一人が独自のループを持つ必要があります。forアレイを破壊するために、一人一人が独自のループを持つ必要があります。

これを一度でも入力ミスすると、メモリが破損する可能性があります。または何かを削除しないでください。または他の恐ろしいことの数。

そして、ここに重要な質問があります。特定の配列について、サイズをどこに保持しますか?作成するすべての配列に割り当てたアイテムの数を知っていますか?各配列には、格納するアイテムの数を知る独自の方法があります。したがって、各デストラクタループはこのデータを適切にフェッチする必要があります。それが間違っている場合...ブーム。

そして、例外安全性があります。これは、まったく新しいワームの缶です。コンストラクターの1つが例外をスローした場合、以前に作成されたオブジェクトを破棄する必要があります。あなたのコードはそれをしません。例外安全ではありません。

ここで、代替案を検討してください。

delete[] my_array;

これは失敗することはできません。それは常にすべての要素を破壊します。配列のサイズを追跡し、例外安全です。したがって、動作することが保証されています。動作しませ(で割り当てている限りnew[])。

もちろん、配列をオブジェクトでラップできると言うこともできます。それは理にかなっている。配列の型要素でオブジェクトをテンプレート化することもできます。そうすれば、すべてのdesturctorコードは同じです。サイズはオブジェクトに含まれています。そして、たぶん、たぶん、あなたは、ユーザーがメモリが割り当てられる特定の方法をある程度制御する必要があることに気づきます。そうすれば、それはだけではありませんmalloc/free

おめでとうございます:あなたはちょうど再発明しましstd::vectorた。

これが、多くのC++プログラマーがもう入力すらしない理由new[]です。

柔軟性

コードはを使用しmalloc/freeます。しかし、私がプロファイリングを行っているとしましょう。そして、malloc/free頻繁に作成される特定のタイプでは、コストが高すぎることを認識しています。私は彼らのために特別なメモリマネージャを作成します。しかし、すべての配列割り当てをそれらにフックする方法は?

そうですね、これらのタイプの配列を作成/破棄する場所をコードベースで検索する必要があります。そして、それに応じて彼らのメモリアロケータを変更する必要があります。そして、他の誰かがそれらのアロケータを元に戻したり、異なるアロケータを使用する新しい配列コードを導入したりしないように、コードベースを継続的に監視する必要があります。

代わりにを使用している場合はnew[]/delete[]、演算子のオーバーロードを使用できます。演算子new[]delete[]それらのタイプにオーバーロードを提供するだけです。コードを変更する必要はありません。誰かがこれらの過負荷を回避することははるかに困難です。彼らは積極的にしようとしなければなりません。などなど。

そのため、アロケータが使用されるべき場所で使用されるという柔軟性と合理的な保証が得られます。

読みやすさ

このことを考慮:

my_object *my_array = new my_object[10];
for (int i=0; i<MY_ARRAY_SIZE; ++i)
  my_array[i]=my_object(i);

//... Do stuff with the array

delete [] my_array;

これと比較してください:

my_object *my_array = (my_object *)malloc(sizeof(my_object) * MY_ARRAY_SIZE);
if(my_object==NULL)
  throw MEMORY_ERROR;

int i;
try
{
    for(i=0; i<MY_ARRAY_SIZE; ++i)
      new(my_array+i) my_object(i);
}
catch(...)  //Exception safety.
{
    for(i; i>0; --i)  //The i-th object was not successfully constructed
        my_array[i-1].~T();
    throw;
}

//... Do stuff with the array

for(int i=MY_ARRAY_SIZE; i>=0; --i)
  my_array[i].~T();
free(my_array);

客観的に言えば、何が起こっているのかを読み、理解するのが簡単なのはどれですか?

このステートメントを見てください:(my_object *)malloc(sizeof(my_object) * MY_ARRAY_SIZE)。これは非常に低レベルのものです。あなたは何の配列も割り当てていません。大量のメモリを割り当てています。オブジェクトのサイズ*必要なオブジェクトの数と一致するように、メモリの塊のサイズを手動で計算する必要があります。キャストも特徴です。

対照的にnew my_object[10]、物語を語ります。new「型のインスタンスを作成する」のC++キーワードです。my_object[10]タイプの10要素配列ですmy_object。シンプルでわかりやすく、直感的です。キャスティングも、バイトサイズの計算も、何もありません。

この方法では、慣用的に使用する方法を学ぶmalloc必要があります。この方法では、どのように機能するかを理解する必要があります。それははるかに冗長ではなく、何が起こっているのかがはるかに明白です。mallocnewnew

さらに、mallocステートメントの後、実際にはオブジェクトの配列はありません。mallocC ++コンパイラにオブジェクトへのポインタ(キャスト付き)のふりをするように指示したメモリのブロックを返すだけです。C ++のオブジェクトには有効期間があるため、オブジェクトの配列ではありません。そして、オブジェクトの存続期間は、それが構築されるまで始まりません。そのメモリにはまだコンストラクタが呼び出されていないため、生きているオブジェクトはありません。

my_arrayその時点では配列ではありません。それは単なるメモリのブロックです。my_object次のステップでそれらを構築するまで、それはsの配列にはなりません。これは、新しいプログラマーにとっては信じられないほど直感的ではありません。それらが生きているオブジェクトではなく、注意して扱われるべきであることを知るには、熟練したC ++の手(おそらくCから学んだ人)が必要です。ポインタはまだsmy_object*を指していないため、まだ適切な動作をしていませmy_objectん。

対照的に、ケースには生き物がいますnew[]。オブジェクトが作成されました。彼らは生きていて、完全に形成されています。このポインタは、他のポインタと同じように使用できますmy_object*

フィン

上記のいずれも、このメカニズムが適切な状況で潜在的に有用ではないことを示していません。しかし、特定の状況で何かの有用性を認めることは1つのことです。それが物事を行うデフォルトの方法であるべきだと言うのはまったく別のことです。

于 2012-01-22T09:51:31.353 に答える
38

暗黙的なコンストラクター呼び出しによってメモリを初期化する必要がなく、確実なメモリ割り当てが必要な場合は、andの代わりにandplacement newを使用しても問題ありません。 mallocfreenew[]delete[]

overを使用する説得力のある理由は、コンストラクター呼び出しによる暗黙的な初期化を提供し、追加または関連する関数呼び出しを保存することです。また、割り当てのたびにチェックする必要がないため、例外ハンドラーを囲むだけで、冗長なエラーを節約できます。とは異なりチェックします。 これらの両方の説得力のある理由は、あなたの使用法には当てはまりません。newmallocnewmemsetmallocnewNULLmalloc

どちらがパフォーマンス効率が良いかは、プロファイリングによってのみ判断できます。現在のアプローチに問題はありません。malloc余談ですが、どちらを使用するのかについての説得力のある理由はわかりませんnew[]

于 2012-01-22T07:19:55.590 に答える
19

どちらとも言えません。

それを行うための最良の方法は次のとおりです。

std::vector<my_object>   my_array;
my_array.reserve(MY_ARRAY_SIZE);

for (int i=0;i<MY_ARRAY_SIZE;++i)
{    my_array.push_back(my_object(i));
}

これは、内部ベクトルがおそらく新しい配置を行っているためです。また、考慮していないメモリ管理に関連する他のすべての問題も管理します。

于 2012-01-22T07:56:19.097 に答える
10

あなたはnew[]/delete[]ここで再実装しました、そしてあなたが書いたものは専門のアロケータを開発するのにかなり一般的です。

単純なコンストラクターを呼び出すオーバーヘッドは、割り当てと比較してほとんど時間がかかりません。これは必ずしも「はるかに効率的」ではありません。デフォルトのコンストラクターとの複雑さに依存しますoperator=

まだ言及されていない良い点の1つは、配列のサイズがnew[]/で知られていることdelete[]です。delete[]ちょうど正しいことをし、尋ねられたときにすべての要素を破壊します。追加の変数(または3つ)をドラッグして、配列を正確に破棄する方法は面倒です。ただし、専用のコレクションタイプが適しています。

new[]/delete[]は利便性のために望ましいです。それらはほとんどオーバーヘッドを導入せず、多くのばかげたエラーからあなたを救うことができます。この機能を取り除いて、カスタム構築をサポートするためにどこでもコレクション/コンテナーを使用するのに十分な力がありますか?私はこのアロケータを実装しました-本当の混乱は、実際に必要なすべての構造バリエーションのファンクターを作成することです。いずれにせよ、プログラムを犠牲にしてより正確に実行できることがよくありますが、これは、誰もが知っているイディオムよりも保守が難しいことがよくあります。

于 2012-01-22T07:41:45.380 に答える
6

私見は両方とも醜いです、ベクトルを使用する方が良いです。パフォーマンスのために、事前にスペースを割り当てるようにしてください。

また:

std::vector<my_object> my_array(MY_ARRAY_SIZE);

すべてのエントリをデフォルト値で初期化する場合。

my_object basic;
std::vector<my_object> my_array(MY_ARRAY_SIZE, basic);

または、オブジェクトを作成したくないがスペースを予約したい場合は、次のようにします。

std::vector<my_object> my_array;
my_array.reserve(MY_ARRAY_SIZE);

次に、Cスタイルのポインター配列としてアクセスする必要がある場合(古いポインターを保持したまま追加しないように注意してください。ただし、通常のCスタイルの配列では追加できません)。

my_object* carray = &my_array[0];      
my_object* carray = &my_array.front(); // Or the C++ way

個々の要素にアクセスします。

my_object value = my_array[i];    // The non-safe c-like faster way
my_object value = my_array.at(i); // With bounds checking, throws range exception

かなりのTypedef:

typedef std::vector<my_object> object_vect;

それらを参照付きの関数に渡します。

void some_function(const object_vect& my_array);

編集:C ++ 11には、std::arrayもあります。ただし、サイズはテンプレートを介して行われるため、実行時に異なるサイズのものを作成することはできず、まったく同じサイズ(またはテンプレート関数自体)を期待しない限り、関数に渡すことができないという問題があります。ただし、バッファなどには便利です。

std::array<int, 1024> my_array;

EDIT2:C ++ 11にも、push_backの代わりに新しいemplace_backがあります。これにより、基本的にオブジェクトを「移動」(またはベクター内でオブジェクトを直接構築)して、コピーを保存できます。

std::vector<SomeClass> v;
SomeClass bob {"Bob", "Ross", 10.34f};
v.emplace_back(bob);
v.emplace_back("Another", "One", 111.0f); // <- Note this doesn't work with initialization lists ☹
于 2012-01-22T07:38:59.107 に答える
5

まあ、答えの数を考えると、介入する理由はないだろうと思っていました...しかし、私は他の人と同じように引き込まれていると思います。さあ行こう

  1. ソリューションが壊れている理由
  2. 生のメモリを処理するためのC++11の新しい機能
  3. これを行うためのより簡単な方法
  4. アドバイス

1.ソリューションが壊れている理由

まず、提示した2つのスニペットは同等ではありません。正しく機能しますが、例外が存在するとnew[]ひどく失敗します。

隠蔽されてnew[]いるのは、構築されたオブジェクトの数を追跡することです。これにより、たとえば3番目のコンストラクター呼び出し中に例外が発生した場合、既に構築された2つのオブジェクトのデストラクタが適切に呼び出されます。

しかし、あなたの解決策はひどく失敗します:

  • 例外をまったく処理しない(そしてひどくリークする)
  • または、配列が半分構築されていても、配列全体でデストラクタを呼び出そうとします(クラッシュする可能性がありますが、未定義の動作を知っている人)

したがって、この2つは明らかに同等ではありません。あなたは壊れています

2.rawメモリを処理するためのC++11の新しい機能

C ++ 11では、委員会のメンバーは、私たちが生の記憶をいじるのがどれほど好きかを理解し、より効率的かつ安全にそれを行うのに役立つ機能を導入しました。

cppreferenceの<memory>概要を確認してください。この例は、新しいグッズ(*)を示しています。

#include <iostream>
#include <string>
#include <memory>
#include <algorithm>

int main()
{
    const std::string s[] = {"This", "is", "a", "test", "."};
    std::string* p = std::get_temporary_buffer<std::string>(5).first;

    std::copy(std::begin(s), std::end(s),
              std::raw_storage_iterator<std::string*, std::string>(p));

    for(std::string* i = p; i!=p+5; ++i) {
        std::cout << *i << '\n';
        i->~basic_string<char>();
    }
    std::return_temporary_buffer(p);
}

スローではないことに注意してください。これは、メモリが(したがって、ポインタを取得するための)get_temporary_buffer2番目のメンバーとして実際に割り当てられた要素の数を返します。pair.first

(*)あるいは、MooingDuckが述べたほど新しくはないかもしれません。

3.これを行うためのより簡単な方法

私が懸念している限り、あなたが本当に求めているように見えるのは、一部の配置を初期化できなかった一種の型付きメモリプールです。

あなたは知っていboost::optionalますか?

これは基本的に、特定のタイプ(テンプレートパラメータ)の1つのアイテムに適合することができる生のメモリの領域ですが、デフォルトでは何もありません。ポインタと同様のインターフェイスを備えており、メモリが実際に占有されているかどうかを照会できます。最後に、インプレースファクトリを使用すると、問題が発生した場合にオブジェクトをコピーせずに安全に使用できます。

std::vector< boost::optional<T> >さて、あなたのユースケースは本当に私には(またはおそらくdeque?)のように見えます

4.アドバイス

最後に、学習のためであろうと、STLコンテナが本当に自分に合っていないためであろうと、本当に自分でそれをやりたい場合は、コードがいたるところに広がるのを避けるために、これをオブジェクトにラップすることをお勧めします。

忘れないでください:自分を繰り返さないでください!

オブジェクト(テンプレート化)を使用すると、デザインの本質を1つの場所にキャプチャして、どこでも再利用できます。

そしてもちろん、そうしている間、新しいC ++ 11機能を利用してみませんか:)?

于 2012-01-22T12:26:57.567 に答える
3

を使用する必要がありますvectors

于 2012-01-22T07:40:47.497 に答える
2

独断的であろうとなかろうと、それはまさにすべてのSTLコンテナが割り当てと初期化のために行うことです。

アロケータを使用してから、初期化されていないスペースを割り当て、コンテナコンストラクタを使用して初期化します。

これが(多くの人が言うように)「c ++ではない」場合、標準ライブラリをそのように実装するにはどうすればよいでしょうか。

malloc / freeを使用したくない場合は、「バイト」をnew char[]

myobjet* pvext = reinterpret_cast<myobject*>(new char[sizeof(myobject)*vectsize]);
for(int i=0; i<vectsize; ++i) new(myobject+i)myobject(params);
...
for(int i=vectsize-1; i!=0u-1; --i) (myobject+i)->~myobject();
delete[] reinterpret_cast<char*>(myobject);

これにより、初期化と割り当ての分離を利用しながら、new割り当て例外メカニズムを利用できます。

私の最初と最後の行をmyallocator<myobject>クラスに入れ、2番目と最後から2番目の行をクラスに入れると、 myvector<myobject>...再実装されただけであることに注意してください。std::vector<myobject, std::allocator<myobject> >

于 2012-01-22T08:10:50.010 に答える
1

ここで示したのは、実際には、システムの一般的なアロケータとは異なるメモリアロケータを使用する場合の方法です。この場合、アロケータ(alloc-> malloc(sizeof(my_object)))を使用してメモリを割り当て、新しい演算子を配置して初期化します。これには、効率的なメモリ管理に多くの利点があり、標準テンプレートライブラリでは非常に一般的です。

于 2012-01-22T07:34:34.323 に答える
1

std::vectorメモリ割り当て/オブジェクト作成(配列への挿入/削除など)の機能を模倣するか、制御が必要なクラスを作成している場合は、それが方法です。この場合、「デフォルトのコンストラクターを呼び出さない」という問題ではありません。memmove「生のメモリと古いオブジェクトをそこに割り当ててから、古いアドレスに新しいオブジェクトを作成する」ことができるかどうか、何らかの形式を使用できるかどうかなどの問題にreallocなります。間違いなく、カスタムの割り当てと配置newははるかに柔軟です...私は少し酔っていますが、弱虫のためです...効率について-少なくとも同じくらい速く(そしてほとんどの)std::vector独自のバージョンを書くことができます)std::vectorの観点から、おそらくより小さく、sizeof()80%が最も使用されていますstd::vectorおそらく3時間以内に機能します。

于 2012-01-22T08:52:54.897 に答える
0
my_object * my_array=new my_object [10];

これは、オブジェクトを含む配列になります。

my_object * my_array=(my_object *)malloc(sizeof(my_object)*MY_ARRAY_SIZE);

これはオブジェクトのサイズの配列になりますが、「壊れている」可能性があります。たとえば、クラスに仮想機能がある場合、それらを呼び出すことはできません。一貫性がない可能性があるのはメンバーデータだけではなく、オブジェクト全体が実際に「壊れている」ことに注意してください(より適切な言葉がないため)

あなたがこれを知っている限り、私は2番目のことをするのが間違っていると言っているのではありません。

于 2012-01-22T07:37:07.600 に答える