C ++ 11で 'for'ループの範囲にあるときに、アイテムをベクトルから削除しますか?


97

私はIInventory *のベクトルを持っていて、C ++ 11の範囲を使用してリストをループし、それぞれを処理しています。

1つでいくつかのことを行った後、それをリストから削除して、オブジェクトを削除することができます。deleteいつでもポインターを呼び出してクリーンアップできることはわかっていますが、範囲forループ内にあるときに、ポインターをベクターから削除する適切な方法は何ですか?リストから削除すると、ループが無効になりますか?

std::vector<IInventory*> inv;
inv.push_back(new Foo());
inv.push_back(new Bar());

for (IInventory* index : inv)
{
    // Do some stuff
    // OK, I decided I need to remove this object from 'inv'...
}

8
凝ったものにしたい場合はstd::remove_if、「何かを行う」という述語を使用し、要素を削除する場合はtrueを返すことができます。
ベンジャミンリンドリー2012

インデックスカウンターを追加して、inv.erase(index)などを使用できない理由はありますか?
TomJ 2012

4
@TomJ:それでもイテレーションは失敗します。
Ben Voigt

@TomJを使用するとパフォーマンスが大幅に低下します。消去のたびに、消去された要素の後にすべての要素を移動できます。
Ivan

1
@BenVoigt std::list以下に切り替えることをお勧めします
bobobobo 2013

回答:


94

いいえ、できません。範囲ベースforは、コンテナの各要素に一度アクセスする必要がある場合に使用します。

for進行中にコンテナーを変更する必要がある場合、通常のループまたはそのいとこを使用する必要があります。要素に複数回アクセスする場合、またはコンテナーを介して非線形の方法で繰り返す場合。

例えば:

auto i = std::begin(inv);

while (i != std::end(inv)) {
    // Do some stuff
    if (blah)
        i = inv.erase(i);
    else
        ++i;
}

5
ここで適用できる消去-削除イディオムはありませんか?
Naveen、2012

4
@Naveen明らかに彼はすべてのアイテムを反復処理し、それを使って計算を実行し、それをコンテナーから削除する必要があるため、そうしないようにしました。Erase-removeは、述語がtrue、AFAIUを返す要素を単に消去することを示しています。この方法は、反復ロジックを述語と混合しない方が良いようです。
セスカーネギー

4
@SethCarnegie述語のラムダを使用したErase-removeにより、エレガントにそれが可能になります(これはすでにC ++ 11であるため)
Potatoswatter

11
このソリューションは好きではありません。ほとんどのコンテナではO(N ^ 2)です。 remove_if優れている。
Ben Voigt 2013

6
この答え正しいeraseです。新しい有効なイテレータを返します。効率的ではないかもしれませんが、動作することが保証されています。
sp2danny

56

要素がベクターから削除されるたびに、消去された要素以降の各要素が移動されるため、消去された要素以降のイテレータが無効であると想定する必要があります。

範囲ベースのforループは、反復子を使用する「通常の」ループの構文上の糖衣なので、上記が当てはまります。

そうは言っても、あなたは単純に:

inv.erase(
    std::remove_if(
        inv.begin(),
        inv.end(),
        [](IInventory* element) -> bool {
            // Do "some stuff", then return true if element should be removed.
            return true;
        }
    ),
    inv.end()
);

5
ベクターが要素を保持するメモリブロックを再割り当てした可能性があるため」いいえ、はvectorへの呼び出しのために再割り当てされることはありませんerase。イテレータが無効化されるのは、消去された要素に続く各要素が移動されるためです。
ildjarn 2012

2
キャプチャのデフォルトは[&]、ローカル変数で「何かをする」ことができるようにするために適切です。
Potatoswatter 2012

2
これは、ほかにあなた取り囲むように覚えておく必要があり、反復子ベースのループよりも、どんな単純に見えていないremove_if.eraseそうでない場合は何も起こりません。
bobobobo 2013年

2
@bobobobo「反復子ベースのループ」とは、セスカーネギーの答えを意味する場合、平均でO(n ^ 2)です。std::remove_ifO(n)です。
ブランコディミトリエビッチ2013年

要素をリストの後ろに入れ替えて、「削除する」すべての処理が完了するまで(内部で実行する必要がある)要素が実際に移動しないようにすることで、本当に良い点がremove_ifあります。ただし、vector要素が5つ.erase() あり、一度に1つしか存在しない場合、イテレータとの使用によるパフォーマンスへの影響はありませんremove_if。リストの方が大きい場合は、リストstd::listの途中の削除が多い場所に切り替える必要があります。
ボボボボ2013年

16

理想的には、繰り返し処理中にベクターを変更しないでください。erase-removeイディオムを使用します。その場合、いくつかの問題が発生する可能性があります。であるので無効化すべてのイテレータは、要素で始まる件まで消去されますが、使用して、イテレータが有効なままであることを確認する必要があります。vectoreraseend()

for (MyVector::iterator b = v.begin(); b != v.end();) { 
    if (foo) {
       b = v.erase( b ); // reseat iterator to a valid value post-erase
    else {
       ++b;
    }
}

b != v.end()テストは現状のまま必要であることに注意してください。次のように最適化しようとすると:

for (MyVector::iterator b = v.begin(), e = v.end(); b != e;)

e最初のerase呼び出しの後で無効化されるため、UBに実行されます。


@ildjarn:はい、そうです!それはタイプミスでした。
12

1
これは、erase-removeイディオムではありません。への呼び出しはなくstd::remove、O(N)ではなくO(N ^ 2)です。
Potatoswatter 2012

1
@Potatoswatter:もちろん違います。私はあなたが繰り返しながら削除することの問題を指摘しようとしていました。私の言い回しが合格しなかったと思いますか?
12

5

そのループ中に要素を削除することは厳密な要件ですか?それ以外の場合は、削除するポインタをNULLに設定し、ベクトルをもう一度渡してすべてのNULLポインタを削除することができます。

std::vector<IInventory*> inv;
inv.push_back( new Foo() );
inv.push_back( new Bar() );

for ( IInventory* &index : inv )
{
    // do some stuff
    // ok I decided I need to remove this object from inv...?
    if (do_delete_index)
    {
        delete index;
        index = NULL;
    }
}
std::remove(inv.begin(), inv.end(), NULL);

1

ネクロポスティングと申し訳ありませんが、私のc ++の専門知識が私の答えの邪魔になる場合も申し訳ありませんが、各項目を反復処理して可能な変更(インデックスの消去など)を行う場合は、ループのバックワードを使用してみてください。

for(int x=vector.getsize(); x>0; x--){

//do stuff
//erase index x

}

インデックスxを消去すると、次のループは、最後の反復の「前」のアイテムになります。これが誰かを助けてくれることを本当に願っています


xを使用して特定のインデックスにアクセスするときは、忘れずにx-1を実行してくださいlol
lilbigwill99

1

OK、私は遅れてんだけど、とにかく:申し訳ありませんが、私がこれまで読んだものは修正されませ-それがある可能性、あなただけの2つの反復子が必要になります。

std::vector<IInventory*>::iterator current = inv.begin();
for (IInventory* index : inv)
{
    if(/* ... */)
    {
        delete index;
    }
    else
    {
        *current++ = index;
    }
}
inv.erase(current, inv.end());

イテレータが指す値を変更するだけで他のイテレータが無効になることはないので、心配することなくこれを行うことができます。実際、std::remove_if(少なくともgccの実装)は非常によく似た(クラシックループを使用した...)処理を行い、何も削除せず、消去もしません。

ただし、これはスレッドセーフではない(!)ことに注意してください。ただし、これは上記の他のソリューションの一部にも適用されます...


一体何ですか。これはやり過ぎです。
ケッセ2017年

@ケッセ本当に?これは、ベクトルを使用して取得できる最も効率的なアルゴリズムです(範囲ベースのループまたはクラシックイテレータループのどちらでもかまいません)。この方法では、ベクトルの各要素を最大で1回移動し、ベクトル全体を1回だけ反復します。erase(もちろん、複数の単一の要素を削除する場合)を介して一致する各要素を削除した場合、後続の要素を何回移動してベクトルを反復しますか?
アコンカグア2017年

1

例を示します。以下の例では、ベクトルから奇数の要素を削除します。

void test_del_vector(){
    std::vector<int> vecInt{0, 1, 2, 3, 4, 5};

    //method 1
    for(auto it = vecInt.begin();it != vecInt.end();){
        if(*it % 2){// remove all the odds
            it = vecInt.erase(it);
        } else{
            ++it;
        }
    }

    // output all the remaining elements
    for(auto const& it:vecInt)std::cout<<it;
    std::cout<<std::endl;

    // recreate vecInt, and use method 2
    vecInt = {0, 1, 2, 3, 4, 5};
    //method 2
    for(auto it=std::begin(vecInt);it!=std::end(vecInt);){
        if (*it % 2){
            it = vecInt.erase(it);
        }else{
            ++it;
        }
    }

    // output all the remaining elements
    for(auto const& it:vecInt)std::cout<<it;
    std::cout<<std::endl;

    // recreate vecInt, and use method 3
    vecInt = {0, 1, 2, 3, 4, 5};
    //method 3
    vecInt.erase(std::remove_if(vecInt.begin(), vecInt.end(),
                 [](const int a){return a % 2;}),
                 vecInt.end());

    // output all the remaining elements
    for(auto const& it:vecInt)std::cout<<it;
    std::cout<<std::endl;

}

以下のawを出力します。

024
024
024

このメソッドeraseは、渡された反復子の次の反復子を返すことに注意してください。

よりここで、我々はより多くの生成方法を使用することができます。

template<class Container, class F>
void erase_where(Container& c, F&& f)
{
    c.erase(std::remove_if(c.begin(), c.end(),std::forward<F>(f)),
            c.end());
}

void test_del_vector(){
    std::vector<int> vecInt{0, 1, 2, 3, 4, 5};
    //method 4
    auto is_odd = [](int x){return x % 2;};
    erase_where(vecInt, is_odd);

    // output all the remaining elements
    for(auto const& it:vecInt)std::cout<<it;
    std::cout<<std::endl;    
}

使い方はこちらをご覧くださいstd::remove_ifhttps://en.cppreference.com/w/cpp/algorithm/remove


1

このスレッドのタイトルに反対して、私は2つのパスを使用します。

#include <algorithm>
#include <vector>

std::vector<IInventory*> inv;
inv.push_back(new Foo());
inv.push_back(new Bar());

std::vector<IInventory*> toDelete;

for (IInventory* index : inv)
{
    // Do some stuff
    if (deleteConditionTrue)
    {
        toDelete.push_back(index);
    }
}

for (IInventory* index : toDelete)
{
    inv.erase(std::remove(inv.begin(), inv.end(), index), inv.end());
}

0

よりエレガントな解決策は、に切り替えることですstd::list(高速のランダムアクセスが必要ない場合)。

list<Widget*> widgets ; // create and use this..

次に.remove_if、1行でC ++ファンクタを削除できます。

widgets.remove_if( []( Widget*w ){ return w->isExpired() ; } ) ;

したがって、ここでは1つの引数(Widget*)を受け入れるファンクタを記述しています。戻り値はWidget*、リストからaを削除するための条件です。

この構文は口に合うと思います。私は私が今まで使用しないと思うremove_ifのためのstd ::ベクトルをそんなにあり- inv.begin()そしてinv.end()、あなたがより良い使用してオフおそらくだノイズが整数インデックスベースの削除示すように、あるいは単なる古い定期的な反復子ベースの削除は(未満)。しかしstd::vector、とにかく本当に途中から削除するべきではないのでlist、リストの途中の削除が頻繁に発生するこのケースでは、に切り替えることをお勧めします。

ただしdeleteWidget*削除されたを呼び出す機会はありませんでした。それを行うには、次のようになります。

widgets.remove_if( []( Widget*w ){
  bool exp = w->isExpired() ;
  if( exp )  delete w ;       // delete the widget if it was expired
  return exp ;                // remove from widgets list if it was expired
} ) ;

次のように、通常のイテレータベースのループを使用することもできます。

//                                                              NO INCREMENT v
for( list<Widget*>::iterator iter = widgets.begin() ; iter != widgets.end() ; )
{
  if( (*iter)->isExpired() )
  {
    delete( *iter ) ;
    iter = widgets.erase( iter ) ; // _advances_ iter, so this loop is not infinite
  }
  else
    ++iter ;
}

の長さが気に入らない場合はfor( list<Widget*>::iterator iter = widgets.begin() ; ...

for( auto iter = widgets.begin() ; ...

1
私はあなたremove_ifstd::vector実際にどのように機能するか、そしてそれがどのように複雑さをO(N)に保つかを理解していないと思います。
Ben Voigt 2013

それは問題ではありません。aの真ん中から削除するstd::vectorと、常に、削除した要素の後に各要素がスライドするためstd::list、はるかに優れた選択になります。
bobobobo 2013

1
いいえ、「各要素を1つ上にスライド」しません。 remove_if解放されたスペースの数だけ各要素を上にスライドします。キャッシュ使用量を考慮に入れるまでに、からの削除よりもパフォーマンスが優れremove_ifているstd::vector可能性がありstd::listます。O(1)ランダムアクセスを保持します。
Ben Voigt 2013

1
それからあなたは質問を探す上で素晴らしい答えを持っています。 この質問は、両方のコンテナのO(N)であるリストの反復について話します。また、両方のコンテナーのO(N)でもあるO(N)要素を削除します。
Ben Voigt 2013

2
事前マーキングは必要ありません。これを1回のパスで実行することは完全に可能です。「検査される次の要素」と「埋められる次のスロット」を追跡する必要があるだけです。述語に基づいてフィルタリングされたリストのコピーを作成するものと考えてください。述語がtrueを返す場合は要素をスキップし、それ以外の場合はコピーします。ただし、リストのコピーは適切に作成され、コピーの代わりにスワッピング/移動が使用されます。
Ben Voigt 2013

0

私は次のようにすると思います...

for (auto itr = inv.begin(); itr != inv.end();)
{
   // Do some stuff
   if (OK, I decided I need to remove this object from 'inv')
      itr = inv.erase(itr);
   else
      ++itr;
}

0

ループ反復中にイテレーターを削除することはできません。イテレーターカウントが不一致になり、一部の反復後に無効なイテレーターが存在するためです。

解決策:1)元のベクターのコピーを取ります2)このコピーを使用してイテレーターを反復します2)いくつかのことを行い、元のベクターから削除します。

std::vector<IInventory*> inv;
inv.push_back(new Foo());
inv.push_back(new Bar());

std::vector<IInventory*> copyinv = inv;
iteratorCout = 0;
for (IInventory* index : copyinv)
{
    // Do some stuff
    // OK, I decided I need to remove this object from 'inv'...
    inv.erase(inv.begin() + iteratorCout);
    iteratorCout++;
}  

0

要素を1つずつ消去すると、簡単にN ^ 2のパフォーマンスが得られます。消去する必要のある要素にマークを付け、ループ後にそれらを一度に消去することをお勧めします。あなたのベクトルの無効な要素にnullptrがあると推定する場合は、

std::vector<IInventory*> inv;
// ... push some elements to inv
for (IInventory*& index : inv)
{
    // Do some stuff
    // OK, I decided I need to remove this object from 'inv'...
    {
      delete index;
      index =nullptr;
    }
}
inv.erase( std::remove( begin( inv ), end( inv ), nullptr ), end( inv ) ); 

うまくいくはずです。

「Do some stuff」がベクターの要素を変更せず、要素を削除または保持する決定を行うためだけに使用される場合は、それをラムダに変換して(誰かの以前の投稿で提案されたように)、使用できます。

inv.erase( std::remove_if( begin( inv ), end( inv ), []( Inventory* i )
  {
    // DO some stuff
    return OK, I decided I need to remove this object from 'inv'...
  } ), end( inv ) );
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.