c ++マップに挿入対emplace対operator []


188

私は初めてマップを使用しましたが、要素を挿入する方法はたくさんあることに気付きました。あなたは使用することができemplace()operator[]またはinsert()、プラス使用してのようにバリアントvalue_typeまたはmake_pair。それらすべてに関する情報や特定のケースに関する質問はたくさんありますが、全体像を理解することはできません。したがって、私の2つの質問は次のとおりです。

  1. それらのそれぞれの利点は何ですか?

  2. 標準にエンプレースを追加する必要がありましたか?それがなければ以前は不可能であったことはありますか?


1
配置セマンティクスにより、明示的な変換と直接の初期化が可能になります。
Kerrek SB 2013

3
現在operator[]はに基づいていtry_emplaceます。それも言及する価値があるかもしれませんinsert_or_assign
FrankHB 2018

@FrankHBあなた(または他の誰か)が最新の回答を追加した場合、受け入れられた回答を変更できます。
ドイツのカプアノ2018

回答:


226

マップの特定のケースでは、古いオプションは2つのみでした:operator[]およびinsert(異なるフレーバーinsert)。それで私はそれらを説明し始めます。

operator[]検索・オア・アド演算子。マップ内で指定されたキーを持つ要素を見つけようとし、存在する場合は、格納されている値への参照を返します。そうでない場合は、デフォルトの初期化で挿入された新しい要素を作成し、その要素への参照を返します。

insert(単一要素風味)関数がかかりvalue_typestd::pair<const Key,Value>)は、キー(使用first部材)と、それを挿入しようとします。なぜならstd::map、それは何も挿入しません既存の要素がある場合は、重複を許可しません。

2つの最初の違いは、operator[]デフォルトの初期化を作成できる必要があるため、デフォルトの初期化ができない値タイプには使用できません。2つの2番目の違いは、指定されたキーを持つ要素がすでにある場合に何が起こるかです。このinsert関数はマップの状態を変更しませんが、代わりに要素へのイテレータ(およびfalse挿入されなかったことを示す)を返します。

// assume m is std::map<int,int> already has an element with key 5 and value 0
m[5] = 10;                      // postcondition: m[5] == 10
m.insert(std::make_pair(5,15)); // m[5] is still 10

insert引数の場合はのオブジェクトでありvalue_type、さまざまな方法で作成できます。オブジェクトを簡単に作成できるため、適切なタイプで直接作成するか、を作成value_typeできる任意のオブジェクトを渡すstd::make_pairことができstd::pairます。

次の呼び出しの最終的な効果は同様です。

K t; V u;
std::map<K,V> m;           // std::map<K,V>::value_type is std::pair<const K,V>

m.insert( std::pair<const K,V>(t,u) );      // 1
m.insert( std::map<K,V>::value_type(t,u) ); // 2
m.insert( std::make_pair(t,u) );            // 3

しかし、実際には同じではありません... [1]と[2]は実際には同等です。どちらの場合も、コードは同じタイプ(std::pair<const K,V>)の一時オブジェクトを作成し、それをinsert関数に渡します。このinsert関数は、バイナリ検索ツリーに適切なノードを作成value_typeし、引数からノードにパーツをコピーします。を使用する利点はvalue_type、まあ、value_type常にと一致 value_typeするので、std::pair引数のタイプを間違えないことです!

違いは[3]にあります。関数std::make_pairは、を作成するテンプレート関数ですstd::pair。署名は次のとおりです。

template <typename T, typename U>
std::pair<T,U> make_pair(T const & t, U const & u );

にテンプレート引数を意図的に提供していませんstd::make_pair。これが一般的な使用方法だからです。また、テンプレート引数は呼び出しから推定され、この場合はでT==K,U==Vあるため、への呼び出しstd::make_pairはaを返しますstd::pair<K,V>(がないことに注意してくださいconst)。シグニチャではvalue_type、それが近いことを要求しますが、への呼び出しからの戻り値と同じではありませんstd::make_pair。十分に近いため、正しいタイプの一時ファイルが作成され、コピー初期化されます。次に、ノードにコピーされ、合計2つのコピーが作成されます。

これは、テンプレート引数を指定することで修正できます。

m.insert( std::make_pair<const K,V>(t,u) );  // 4

しかし、それでも、ケース[1]で型を明示的に入力するのと同じ方法でエラーが発生しやすくなります。

この時点まで、外部での作成とそのオブジェクトのコンテナーへのコピーinsertを必要とするさまざまな呼び出し方法がありvalue_typeます。別の方法としては、使用することができるoperator[]タイプであれば、デフォルト構築可能割り当て可能(意図的にのみ焦点を当てm[k]=v)、そして、それは1つのオブジェクトのデフォルトの初期化と要求するコピーそのオブジェクトに値のを。

C ++ 11では、可​​変個のテンプレートと完全転送を使用して、配置(インプレース作成)によって要素をコンテナーに追加する新しい方法があります。emplace異なるコンテナーの関数は基本的に同じことを行います。コンテナーにコピーするソースを取得する代わりに、関数は、コンテナーに格納されているオブジェクトのコンストラクターに転送されるパラメーターを受け取ります。

m.emplace(t,u);               // 5

[5]では、std::pair<const K, V>が作成されてに渡されるのemplaceではなく、tおよびuオブジェクトへの参照が渡され、データ構造内のサブオブジェクトemplaceのコンストラクターに転送されますvalue_type。この場合コピーはstd::pair<const K,V>まったく行われません。これはemplace、C ++ 03の代替よりも優れている点です。の場合と同様に、insertマップの値はオーバーライドされません。


私が考えていなかった興味深い質問は、emplace実際にマップにどのように実装できるかであり、それは一般的な場合の単純な問題ではありません。


5
これは答えに示唆されていますが、map [] = valは以前の値が存在する場合、それを上書きします。
dk123 2013

私の意味でより興味深い質問は、それがほとんど目的を果たしていないということです。ペアコピーを保存するため、ペアコピーがないことはmapped_typeインスタンスコピーがないことを意味するので、これは良いことです。必要なのはmapped_type、ペアの構造をペアに配置し、ペアの構造をマップに配置することです。したがって、std::pair::emplace関数とその転送サポートのmap::emplace両方がありません。現在の形式では、構築されたmapped_typeをペアのコンストラクターに渡して、一度コピーする必要があります。2倍よりは良いですが、それでもダメです。
v.oddou 2014

実際に私はそのコメントを修正します。C++ 11には、1つの引数の構築の場合に配置するのとまったく同じ目的を果たすテンプレートペアコンストラクタがあります。そして、いくつかの奇妙な区分的コンストラクトは、タプルを使用して引数を転送するので、完全な転送を行うことができます。
v.oddou 2014

:unordered_mapとマップの挿入のパフォーマンスのバグがあるように見えるリンク
徳清

1
insert_or_assignand try_emplace(両方ともC ++ 17から)の情報でこれを更新すると、既存のメソッドとの機能のギャップを埋めるのに役立ちます。
ShadowRanger

14

Emplace:右辺値参照を利用して、すでに作成した実際のオブジェクトを使用します。これは、コピーまたは移動コンストラクターが呼び出されないことを意味し、LARGEオブジェクトに適しています。O(log(N))時間。

挿入:標準の左辺値参照と右辺値参照のオーバーロード、および挿入する要素のリストへの反復子と、要素が属する位置に関する「ヒント」があります。「ヒント」イテレータを使用すると、挿入にかかる時間が一定の時間に短縮されます。それ以外の場合は、O(log(N))時間になります。

Operator []:オブジェクトが存在するかどうかを確認し、存在する場合はこのオブジェクトへの参照を変更します。それ以外の場合は、提供されたキーと値を使用して2つのオブジェクトでmake_pairを呼び出し、挿入関数と同じ処理を行います。これはO(log(N))時間です。

make_pair:ペアを作成するだけです。

標準にemplaceを追加する必要はありませんでした。c ++ 11では、&&タイプの参照が追加されたと思います。これにより、移動のセマンティクスが不要になり、特定のタイプのメモリ管理を最適化できるようになりました。特に、右辺値参照。オーバーロードされたinsert(value_type &&)演算子は、in_placeセマンティクスを利用しないため、効率が大幅に低下します。これは右辺値参照を処理する機能を提供しますが、オブジェクトの適切な構築であるそれらの主要な目的を無視します。


4
標準にemplaceを追加する必要はありませんでした。」これは明らかに誤りです。emplace()コピーまたは移動できない要素を挿入する唯一の方法です。それが「ない程度だ:また、あなたのアイデアを間違って持っているようだ(そのようなものが存在する場合&はい、おそらく、ほとんどの効率的な1のインサートのコピーや移動コンストラクタは、より多くの建設よりも多くの費用)右辺値参照の[撮る]利点すでに作成した実際のオブジェクトを使用するには "; 何のオブジェクトがまだ作成されていない、とあなたは転送map引数、それは自分自身の内側にそれを作成する必要があります。オブジェクトを作成しません。
underscore_d

10

最適化の機会とより単純な構文は別として、挿入と配置の重要な違いは、後者が明示的な変換を許可することです。(これは、マップだけでなく、標準ライブラリ全体に適用されます。)

以下に例を示します。

#include <vector>

struct foo
{
    explicit foo(int);
};

int main()
{
    std::vector<foo> v;

    v.emplace(v.end(), 10);      // Works
    //v.insert(v.end(), 10);     // Error, not explicit
    v.insert(v.end(), foo(10));  // Also works
}

これは確かに非常に具体的な詳細ですが、ユーザー定義の変換のチェーンを扱う場合は、これを覚えておく価値があります。


fooがそのctorに1つではなく2つの整数を必要としたと想像してください。この通話を使用できますか? v.emplace(v.end(), 10, 10); ...または使用する必要があります v.emplace(v.end(), foo(10, 10) ); か?
2015

今はコンパイラにアクセスできませんが、これは両方のバージョンが機能することを意味すると思います。emplace単一のパラメータを取るクラスを利用するために見られるほとんどすべての例。IMOは、複数のパラメーターが例で使用されている場合、実際にemplaceの可変部分の構文の性質をかなり明確にします。
2015

9

次のコードは、との「全体像」がどのようにinsert()異なるかを理解するのに役立ちますemplace()

#include <iostream>
#include <unordered_map>
#include <utility>

//Foo simply outputs what constructor is called with what value.
struct Foo {
  static int foo_counter; //Track how many Foo objects have been created.
  int val; //This Foo object was the val-th Foo object to be created.

  Foo() { val = foo_counter++;
    std::cout << "Foo() with val:                " << val << '\n';
  }
  Foo(int value) : val(value) { foo_counter++;
    std::cout << "Foo(int) with val:             " << val << '\n';
  }
  Foo(Foo& f2) { val = foo_counter++;
    std::cout << "Foo(Foo &) with val:           " << val
              << " \tcreated from:      \t" << f2.val << '\n';
  }
  Foo(const Foo& f2) { val = foo_counter++;
    std::cout << "Foo(const Foo &) with val:     " << val
              << " \tcreated from:      \t" << f2.val << '\n';
  }
  Foo(Foo&& f2) { val = foo_counter++;
    std::cout << "Foo(Foo&&) moving:             " << f2.val
              << " \tand changing it to:\t" << val << '\n';
  }
  ~Foo() { std::cout << "~Foo() destroying:             " << val << '\n'; }

  Foo& operator=(const Foo& rhs) {
    std::cout << "Foo& operator=(const Foo& rhs) with rhs.val: " << rhs.val
              << " \tcalled with lhs.val = \t" << val
              << " \tChanging lhs.val to: \t" << rhs.val << '\n';
    val = rhs.val;
    return *this;
  }

  bool operator==(const Foo &rhs) const { return val == rhs.val; }
  bool operator<(const Foo &rhs)  const { return val < rhs.val;  }
};

int Foo::foo_counter = 0;

//Create a hash function for Foo in order to use Foo with unordered_map
namespace std {
   template<> struct hash<Foo> {
       std::size_t operator()(const Foo &f) const {
           return std::hash<int>{}(f.val);
       }
   };
}

int main()
{
    std::unordered_map<Foo, int> umap;  
    Foo foo0, foo1, foo2, foo3;
    int d;

    //Print the statement to be executed and then execute it.

    std::cout << "\numap.insert(std::pair<Foo, int>(foo0, d))\n";
    umap.insert(std::pair<Foo, int>(foo0, d));
    //Side note: equiv. to: umap.insert(std::make_pair(foo0, d));

    std::cout << "\numap.insert(std::move(std::pair<Foo, int>(foo1, d)))\n";
    umap.insert(std::move(std::pair<Foo, int>(foo1, d)));
    //Side note: equiv. to: umap.insert(std::make_pair(foo1, d));

    std::cout << "\nstd::pair<Foo, int> pair(foo2, d)\n";
    std::pair<Foo, int> pair(foo2, d);

    std::cout << "\numap.insert(pair)\n";
    umap.insert(pair);

    std::cout << "\numap.emplace(foo3, d)\n";
    umap.emplace(foo3, d);

    std::cout << "\numap.emplace(11, d)\n";
    umap.emplace(11, d);

    std::cout << "\numap.insert({12, d})\n";
    umap.insert({12, d});

    std::cout.flush();
}

私が得た出力は:

Foo() with val:                0
Foo() with val:                1
Foo() with val:                2
Foo() with val:                3

umap.insert(std::pair<Foo, int>(foo0, d))
Foo(Foo &) with val:           4    created from:       0
Foo(Foo&&) moving:             4    and changing it to: 5
~Foo() destroying:             4

umap.insert(std::move(std::pair<Foo, int>(foo1, d)))
Foo(Foo &) with val:           6    created from:       1
Foo(Foo&&) moving:             6    and changing it to: 7
~Foo() destroying:             6

std::pair<Foo, int> pair(foo2, d)
Foo(Foo &) with val:           8    created from:       2

umap.insert(pair)
Foo(const Foo &) with val:     9    created from:       8

umap.emplace(foo3, d)
Foo(Foo &) with val:           10   created from:       3

umap.emplace(11, d)
Foo(int) with val:             11

umap.insert({12, d})
Foo(int) with val:             12
Foo(const Foo &) with val:     13   created from:       12
~Foo() destroying:             12

~Foo() destroying:             8
~Foo() destroying:             3
~Foo() destroying:             2
~Foo() destroying:             1
~Foo() destroying:             0
~Foo() destroying:             13
~Foo() destroying:             11
~Foo() destroying:             5
~Foo() destroying:             10
~Foo() destroying:             7
~Foo() destroying:             9

次のことに注意してください。

  1. unordered_map常に内部的にFooオブジェクト(Foo *s などではなく)をキーとして格納します。これらは、unordered_mapが破棄されるときにすべて破棄されます。ここで、unordered_mapの内部キーはfoos 13、11、5、10、7、および9でした。

    • したがって、技術的には、unordered_map実際にstd::pair<const Foo, int>オブジェクトを格納し、次にFooオブジェクトを格納します。しかし、「全体像」がどのようにemplace()異なるかを理解するにはinsert()(下の強調表示されたボックスを参照)、このオブジェクトが完全に受動的であると一時的に想像しstd::pairても問題ありません。この「全体像」について理解したら、この中間std::pairオブジェクトを使用して、unordered_map微妙だが重要な技術を導入する方法をバックアップして理解することが重要です。
  2. それぞれの挿入foo0foo1foo2の1への2つの呼び出しに必要なFooのコピー/移動コンストラクタとの2つの呼び出しFooの(私は今説明するように)デストラクタを:

    a。それぞれを挿入するfoo0し、foo1(一時的なオブジェクト作成foo4及びfoo6そのデストラクタ挿入が完了した後、直ちに呼び出されたそれぞれ)。さらに、unordered_mapの内部Foos(Foo5と7)も、unordered_mapが破棄されたときにデストラクタが呼び出されました。

    b。を挿入するためにfoo2、代わりに最初に非一時的なペアオブジェクト(と呼ばれるpair)を明示的に作成しました。これは、Fooのコピーコンストラクターを呼び出します(の内部メンバーとしてfoo2作成)。次に、このペアを編集しました。その結果、コピーコンストラクターが再び呼び出され(on )、独自の内部コピー(foo8pairinsert()unordered_mapfoo8foo9)。foos 0および1の場合と同様に、最終結果はこの挿入に対する2つのデストラクタ呼び出しでしたが、唯一の違いは、foo8デストラクタが、終了main()直後に呼び出されるのではなく、最後に到達したときにのみ呼び出されることinsert()です。

  3. 強調 foo3により、コピー/移動コンストラクター呼び出し(foo10内部で作成unordered_map)が1つだけ、Fooのデストラクタが1つだけ呼び出されました。(後でこれに戻ります)。

  4. の場合foo11、整数11を直接渡したemplace(11, d)ので、メソッドの実行中にコンストラクターunordered_mapが呼び出されます。(2)と(3)とは異なり、これを行うためにいくつかの既存のオブジェクトは必要ありませんでした。重要なことに、コンストラクターへの呼び出しが1つだけ発生したことに注意してください(これによりが作成されました)。Foo(int)emplace()fooFoofoo11

  5. 次に、整数12をに直接渡しましたinsert({12, d})。とは異なりemplace(11, d)(リコールの結果、Fooコンストラクターが1回だけ呼び出された)、このへの呼び出しinsert({12, d})により、Fooのコンストラクターが2回呼び出された(作成foo12foo13)。

このショー何の間の主な「全体像」の違いinsert()emplace()次のとおりです。

insert() ほとんどの場合、を使用するには、のスコープ内のFooオブジェクトの作成または存在main()(その後にコピーまたは移動)が必要ですが、使用する場合emplace()Fooコンストラクターへの呼び出しは完全に内部でunordered_map(つまり、emplace()メソッドの定義のスコープ内で)行われます。渡すキーの引数は、内部のコンストラクター呼び出しにemplace()直接転送されFooますunordered_map::emplace()の定義のます(オプションの追加詳細:この新しく構築されたオブジェクトは、unordered_mapのメンバー変数の1つにすぐに組み込まれるため、デストラクタが呼び出されないようにします実行は終了emplace()し、移動またはコピーコンストラクターは呼び出されません)。

注:「ほぼ」の理由」「で、ほとんどの場合、以下の上記Iで説明されます」)。

  1. 続き:umap.emplace(foo3, d)called Fooの非constコピーコンストラクターを呼び出す理由は次のとおりです:を使用しているemplace()ため、コンパイラーはfoo3(非const Fooオブジェクト)がFooコンストラクターの引数であることを認識しています。この場合、最も適合するFooコンストラクタは、非constコピーコンストラクタFoo(Foo& f2)です。umap.emplace(foo3, d)コピーコンストラクターが呼び出されumap.emplace(11, d)なかったのはこのためです。

エピローグ:

I.の1つのオーバーロードinsert()は実際にはと同等 であることに注意してくださいemplace()このcppreference.comページで説明されているよう、オーバーロードtemplate<class P> std::pair<iterator, bool> insert(P&& value)( overload(2))insert()はと同等emplace(std::forward<P>(value))です。

II。ここからどこへ行く?

a。以下のための上記のソースコードとドキュメント研究で遊んでinsert()(例えばここ)とemplace()(例えばここ)さんがオンラインで見つけること。EclipseやNetBeansなどのIDEを使用している場合は、IDEに簡単にオーバーロードされている、insert()またはemplace()呼び出されていることを通知させることができます(Eclipseでは、マウスのカーソルを関数呼び出しの上で一秒間動かし続けるだけです)。以下に、試してみるコードをいくつか示します。

std::cout << "\numap.insert({{" << Foo::foo_counter << ", d}})\n";
umap.insert({{Foo::foo_counter, d}});
//but umap.emplace({{Foo::foo_counter, d}}); results in a compile error!

std::cout << "\numap.insert(std::pair<const Foo, int>({" << Foo::foo_counter << ", d}))\n";
umap.insert(std::pair<const Foo, int>({Foo::foo_counter, d}));
//The above uses Foo(int) and then Foo(const Foo &), as expected. but the
// below call uses Foo(int) and the move constructor Foo(Foo&&). 
//Do you see why?
std::cout << "\numap.insert(std::pair<Foo, int>({" << Foo::foo_counter << ", d}))\n";
umap.insert(std::pair<Foo, int>({Foo::foo_counter, d}));
//Not only that, but even more interesting is how the call below uses all 
// three of Foo(int) and the Foo(Foo&&) move and Foo(const Foo &) copy 
// constructors, despite the below call's only difference from the call above 
// being the additional { }.
std::cout << "\numap.insert({std::pair<Foo, int>({" << Foo::foo_counter << ", d})})\n";
umap.insert({std::pair<Foo, int>({Foo::foo_counter, d})});


//Pay close attention to the subtle difference in the effects of the next 
// two calls.
int cur_foo_counter = Foo::foo_counter;
std::cout << "\numap.insert({{cur_foo_counter, d}, {cur_foo_counter+1, d}}) where " 
  << "cur_foo_counter = " << cur_foo_counter << "\n";
umap.insert({{cur_foo_counter, d}, {cur_foo_counter+1, d}});

std::cout << "\numap.insert({{Foo::foo_counter, d}, {Foo::foo_counter+1, d}}) where "
  << "Foo::foo_counter = " << Foo::foo_counter << "\n";
umap.insert({{Foo::foo_counter, d}, {Foo::foo_counter+1, d}});


//umap.insert(std::initializer_list<std::pair<Foo, int>>({{Foo::foo_counter, d}}));
//The call below works fine, but the commented out line above gives a 
// compiler error. It's instructive to find out why. The two calls
// differ by a "const".
std::cout << "\numap.insert(std::initializer_list<std::pair<const Foo, int>>({{" << Foo::foo_counter << ", d}}))\n";
umap.insert(std::initializer_list<std::pair<const Foo, int>>({{Foo::foo_counter, d}}));

std::pairコンストラクターのオーバーロードがすぐにわかります(リファレンスを参照))が最終的に使用されるunordered_mapようになると、コピー、移動、作成、および/または破棄されるオブジェクトの数、およびこれらすべてが発生するタイミングに重要な影響を与えるます。

b。代わりに他のコンテナクラス(std::setまたはまたはstd::unordered_multiset)を使用するとどうなるかを確認します。std::unordered_map

c。ここで、の範囲タイプとしての代わりにGooオブジェクト(の名前を変更したのコピーFoo)を使用し(つまり、の代わりに使用)、呼び出されるコンストラクターの数とコンストラクターを確認します。(ネタバレ:効果はありますが、それほど劇的ではありません。)intunordered_mapunordered_map<Foo, Goo>unordered_map<Foo, int>Goo


弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.