マップに挿入する推奨/慣用の方法は何ですか?


111

に要素を挿入する4つの異なる方法を特定しましたstd::map

std::map<int, int> function;

function[0] = 42;
function.insert(std::map<int, int>::value_type(0, 42));
function.insert(std::pair<int, int>(0, 42));
function.insert(std::make_pair(0, 42));

それらのうちどれが好ましい/慣用的な方法ですか?(そして私が考えていない別の方法がありますか?)


26
マップは「関数」ではなく「回答」と呼ぶ必要があります
Vincent Robert

2
@ビンセント:うーん?関数は基本的に2つのセット間のマップです。
fredoverflow 2010年

7
@FredOverflow:ヴィンセントのコメントは特定の本についての冗談のようです...
Victor Sorokin

1
オリジナルとは矛盾しているようです。42は、(a)生命、宇宙、すべてへの答えであると同時に、(b)何にも答えられません。しかし、生命、宇宙、そしてすべてを一体としてどのように表現しますか?
スチュアートゴロデッツ2010年

19
@sgolodetzすべてを十分な大きさのintで表現できます。
Yakov Galka、

回答:


90

まずoperator[]insertメンバー関数は機能的に同等ではありません。

  • operator[]ます検索、キーの挿入デフォルト構築が見つからない場合は、値を、そしてあなたが値を割り当て先の参照を返します。明らかに、mapped_typeデフォルトの構築および割り当てではなく、直接初期化することでメリットが得られる場合、これは非効率になる可能性があります。この方法では、挿入が実際に行われたかどうか、または以前に挿入されたキーの値のみを上書きしたかどうかを判別することもできません。
  • insertメンバ関数は、それがしばしば忘れられているが、キーがすでにマップ内に存在している場合には効果がないとなり、返しstd::pair<iterator, bool>興味のあることができるが、(挿入が実際に行われている場合は最も顕著なのかを決定するために)。

リストされているすべての呼び出し可能性からinsert、3つすべてがほぼ同等です。念のinsertため、標準の署名を見てみましょう:

typedef pair<const Key, T> value_type;

  /* ... */

pair<iterator, bool> insert(const value_type& x);

では、3つの呼び出しはどのように違うのですか?

  • std::make_pairテンプレート引数控除に依存していると可能性(この場合のでしょう)、実際とは異なるタイプの農産物何かvalue_typeへの追加の呼び出しが必要になりますマップの、std::pairに変換するために、テンプレートコンストラクタをvalue_type(例:追加constしますfirst_type
  • std::pair<int, int>またstd::pair、パラメータをに変換するためにvalue_type(つまり:に追加するconstためfirst_type)のテンプレートコンストラクタへの追加の呼び出しが必要になります。
  • std::map<int, int>::value_typeこれは、insertメンバー関数が期待するパラメーター型であるため、疑いの余地はありません。

最後に、operator[]デフォルトの作成と割り当てに追加のコストがなくmapped_type、新しいキーが効果的に挿入されたかどうかを判断する必要がない限り、挿入が目的の場合は使用しません。を使用する場合は、おそらくinserta を作成するのvalue_typeがよいでしょう。


make_pair()でのKeyからconst Keyへの変換は、本当に別の関数呼び出しを要求しますか?暗黙のキャストは、どのコンパイラーが喜んでそうする必要があるかで十分だと思われます。
galactica

99

C ++ 11以降、2つの主要な追加オプションがあります。まず、insert()リストの初期化構文で使用できます。

function.insert({0, 42});

これは機能的には

function.insert(std::map<int, int>::value_type(0, 42));

しかし、はるかに簡潔で読みやすいです。他の回答が指摘しているように、これには他の形式に比べていくつかの利点があります。

  • このoperator[]アプローチでは、マップされたタイプを割り当て可能にする必要がありますが、常にそうであるとは限りません。
  • このoperator[]アプローチは既存の要素を上書きする可能性があり、これが発生したかどうかを判断する方法を提供しません。
  • insertリストする他の形式には、コードの速度を低下させる可能性がある暗黙の型変換が含まれます。

主な欠点は、このフォームはキーと値をコピー可能にする必要があったため、たとえばunique_ptr値を持つマップでは機能しないことです。これは標準で修正されましたが、標準ライブラリの実装にまだ到達していない可能性があります。

次に、メソッドを使用できますemplace()

function.emplace(0, 42);

これは、のどの形式よりも簡潔で、のinsert()ような移動のみのタイプでも問題なく機能しunique_ptr、理論的には少し効率的です(ただし、適切なコンパイラは違いを最適化する必要があります)。唯一の大きな欠点は、emplaceメソッドは通常そのようには使用されないため、読者を少し驚かせる可能性があることです。



11

最初のバージョン:

function[0] = 42; // version 1

値42をマップに挿入する場合としない場合があります。キー0が存在する場合は、そのキーに42を割り当て、そのキーの値を上書きします。それ以外の場合は、キーと値のペアを挿入します。

挿入関数:

function.insert(std::map<int, int>::value_type(0, 42));  // version 2
function.insert(std::pair<int, int>(0, 42));             // version 3
function.insert(std::make_pair(0, 42));                  // version 4

一方、キーが0すでにマップに存在する場合は何もしないでください。キーが存在しない場合は、キーと値のペアが挿入されます。

3つの挿入関数はほとんど同じです。std::map<int, int>::value_typetypedeffor std::pair<const int, int>であり、std::make_pair()明らかにstd::pair<>viaテンプレートの控除マジックを生成します。ただし、最終結果はバージョン2、3、および4で同じになるはずです。

どちらを使用しますか?私は個人的にバージョン1を好みます。簡潔で「自然」です。その上書き動作が望ましくない場合、それは単一があるかどうかはわからないバージョン2および3未満のタイピングを必要とするので、もちろん、私は、バージョン4を好むだろうデファクトにキー/値のペアを挿入する方法std::map

コンストラクターの1つを介してマップに値を挿入する別の方法:

std::map<int, int> quadratic_func;

quadratic_func[0] = 0;
quadratic_func[1] = 1;
quadratic_func[2] = 4;
quadratic_func[3] = 9;

std::map<int, int> my_func(quadratic_func.begin(), quadratic_func.end());


5

以来C ++ 17の std::map申し出二つの新しい挿入方法:insert_or_assign()try_emplace()としてもで述べたように、sp2dannyでコメント

insert_or_assign()

基本的に、はのinsert_or_assign()「改良」バージョンですoperator[]。とは対照的にoperator[]insert_or_assign()マップの値タイプをデフォルトで構築可能にする必要はありません。たとえば、次のコードにはMyClassデフォルトのコンストラクタがないため、コンパイルされません。

class MyClass {
public:
    MyClass(int i) : m_i(i) {};
    int m_i;
};

int main() {
    std::map<int, MyClass> myMap;

    // VS2017: "C2512: 'MyClass::MyClass' : no appropriate default constructor available"
    // Coliru: "error: no matching function for call to 'MyClass::MyClass()"
    myMap[0] = MyClass(1);

    return 0;
}

ただし、myMap[0] = MyClass(1);次の行で置き換えると、コードがコンパイルされ、挿入は意図したとおりに行われます。

myMap.insert_or_assign(0, MyClass(1));

さらに、と同様にinsert()、をinsert_or_assign()返しますpair<iterator, bool>。ブール値はtrue、挿入が発生したfalseか、割り当てが行われたかを示します。イテレータは、挿入または更新された要素を指します。

try_emplace()

上記と同様に、try_emplace()の「改善」ですemplace()。とは対照的にemplace()try_emplace()マップにすでに存在するキーが原因で挿入が失敗した場合、は引数を変更しません。たとえば、次のコードは、すでにマップに格納されているキー(*を参照)を使用して要素を配置しようとします。

int main() {
    std::map<int, std::unique_ptr<MyClass>> myMap2;
    myMap2.emplace(0, std::make_unique<MyClass>(1));

    auto pMyObj = std::make_unique<MyClass>(2);    
    auto [it, b] = myMap2.emplace(0, std::move(pMyObj));  // *

    if (!b)
        std::cout << "pMyObj was not inserted" << std::endl;

    if (pMyObj == nullptr)
        std::cout << "pMyObj was modified anyway" << std::endl;
    else
        std::cout << "pMyObj.m_i = " << pMyObj->m_i <<  std::endl;

    return 0;
}

出力(少なくともVS2017とColiruの場合):

pMyObjが挿入されなかった
pMyObjがとにかく変更された

ご覧のとおりpMyObj、は元のオブジェクトを指していません。ただし、auto [it, b] = myMap2.emplace(0, std::move(pMyObj));次のコードに置き換えると、出力はpMyObj変更されないため、見た目が異なります。

auto [it, b] = myMap2.try_emplace(0, std::move(pMyObj));

出力:

pMyObjは挿入されませんでした
pMyObj pMyObj.m_i = 2

コリルのコード

注:この回答に合わせるために、説明はできるだけ短く、シンプルにするようにしました。より正確で包括的な説明については、Fluent C ++に関するこの記事を読むことをお勧めします


3

私は上記のバージョン間のいくつかの時間比較を実行しています:

function[0] = 42;
function.insert(std::map<int, int>::value_type(0, 42));
function.insert(std::pair<int, int>(0, 42));
function.insert(std::make_pair(0, 42));

挿入バージョン間の時間差はわずかであることがわかります。

#include <map>
#include <vector>
#include <boost/date_time/posix_time/posix_time.hpp>
using namespace boost::posix_time;
class Widget {
public:
    Widget() {
        m_vec.resize(100);
        for(unsigned long it = 0; it < 100;it++) {
            m_vec[it] = 1.0;
        }
    }
    Widget(double el)   {
        m_vec.resize(100);
        for(unsigned long it = 0; it < 100;it++) {
            m_vec[it] = el;
        }
    }
private:
    std::vector<double> m_vec;
};


int main(int argc, char* argv[]) {



    std::map<int,Widget> map_W;
    ptime t1 = boost::posix_time::microsec_clock::local_time();    
    for(int it = 0; it < 10000;it++) {
        map_W.insert(std::pair<int,Widget>(it,Widget(2.0)));
    }
    ptime t2 = boost::posix_time::microsec_clock::local_time();
    time_duration diff = t2 - t1;
    std::cout << diff.total_milliseconds() << std::endl;

    std::map<int,Widget> map_W_2;
    ptime t1_2 = boost::posix_time::microsec_clock::local_time();    
    for(int it = 0; it < 10000;it++) {
        map_W_2.insert(std::make_pair(it,Widget(2.0)));
    }
    ptime t2_2 = boost::posix_time::microsec_clock::local_time();
    time_duration diff_2 = t2_2 - t1_2;
    std::cout << diff_2.total_milliseconds() << std::endl;

    std::map<int,Widget> map_W_3;
    ptime t1_3 = boost::posix_time::microsec_clock::local_time();    
    for(int it = 0; it < 10000;it++) {
        map_W_3[it] = Widget(2.0);
    }
    ptime t2_3 = boost::posix_time::microsec_clock::local_time();
    time_duration diff_3 = t2_3 - t1_3;
    std::cout << diff_3.total_milliseconds() << std::endl;

    std::map<int,Widget> map_W_0;
    ptime t1_0 = boost::posix_time::microsec_clock::local_time();    
    for(int it = 0; it < 10000;it++) {
        map_W_0.insert(std::map<int,Widget>::value_type(it,Widget(2.0)));
    }
    ptime t2_0 = boost::posix_time::microsec_clock::local_time();
    time_duration diff_0 = t2_0 - t1_0;
    std::cout << diff_0.total_milliseconds() << std::endl;

    system("pause");
}

これはバージョンごとにそれぞれ与えます(ファイルを3回実行したため、それぞれに3つの連続した時間差があります)。

map_W.insert(std::pair<int,Widget>(it,Widget(2.0)));

2198 ms、2078 ms、2072 ms

map_W_2.insert(std::make_pair(it,Widget(2.0)));

2290 ms、2037 ms、2046 ms

 map_W_3[it] = Widget(2.0);

2592 ms、2278 ms、2296 ms

 map_W_0.insert(std::map<int,Widget>::value_type(it,Widget(2.0)));

2234 ms、2031 ms、2027 ms

したがって、異なる挿入バージョン間の結果は無視できます(ただし、仮説検定は実行しませんでした)。

map_W_3[it] = Widget(2.0);バージョンが原因ウィジェットのデフォルトコンストラクタで初期化する。この例では約10から15パーセントより多くの時間がかかります。


2

つまり、[]演算子は値の型のデフォルトコンストラクターを呼び出して新しい値を割り当てるので、値を更新するのにより効率的です。insert()を追加する場合には効率的です。

効果的なSTLから引用した抜粋スコットマイヤーズによる標準テンプレートライブラリの使用を改善するための50の特定の方法、アイテム24が役立つかもしれません。

template<typename MapType, typename KeyArgType, typename ValueArgType>
typename MapType::iterator
insertKeyAndValue(MapType& m, const KeyArgType&k, const ValueArgType& v)
{
    typename MapType::iterator lb = m.lower_bound(k);

    if (lb != m.end() && !(m.key_comp()(k, lb->first))) {
        lb->second = v;
        return lb;
    } else {
        typedef typename MapType::value_type MVT;
        return m.insert(lb, MVT(k, v));
    }
}

ジェネリックプログラミングフリーのバージョンを選択することもできますが、重要なのは、このパラダイム(「追加」と「更新」の区別)が非常に役立つことです。


1

std :: mapに要素を挿入したい場合-insert()関数を使用し、要素を(キーで)見つけて要素に割り当てたい場合-operator []を使用します。

挿入を簡単にするために、次のようにboost :: assignライブラリを使用します。

using namespace boost::assign;

// For inserting one element:

insert( function )( 0, 41 );

// For inserting several elements:

insert( function )( 0, 41 )( 0, 42 )( 0, 43 );

1

問題を少し変更して(文字列のマップ)、挿入の別の興味を示します。

std::map<int, std::string> rancking;

rancking[0] = 42;  // << some compilers [gcc] show no error

rancking.insert(std::pair<int, std::string>(0, 42));// always a compile error

コンパイラが「rancking [1] = 42;」でエラーを表示しないという事実 壊滅的な影響を与えることができます!


コンパイラーはstd::string::operator=(char)存在するため、前者のエラーを表示std::string::string(char)しませんが、コンストラクターが存在しないため、後者のエラーを表示します。C ++は常に整数スタイルのリテラルをと自由に解釈するため、エラー生成されません。charこれはコンパイラのバグではなく、プログラマのエラーです。基本的には、それがコードにバグをもたらすかどうかは、自分自身に気をつけなければならないことです。ところで、あなたは印刷することができますrancking[0]し、コンパイラはASCIIが出力されます使用して*います、(char)(42)
キースM

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