Bir haritaya eklemenin tercih edilen / deyimsel yolu nedir?


113

Öğeleri bir içine eklemenin dört farklı yolunu belirledim 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));

Bunlardan hangisi tercih edilen / deyimsel yoldur? (Ve düşünmediğim başka bir yol var mı?)


26
Haritanıza "işlev" değil, "yanıtlar" adı verilmelidir
Vincent Robert

2
@Vincent: Hm? Bir fonksiyon temelde iki küme arasındaki bir haritadır.
fredoverflow

7
@FredOverflow: Vincent'ın yorumu belirli bir kitap hakkında biraz şaka gibi görünüyor ...
Victor Sorokin

1
Orijinalle çelişiyor gibi görünüyor - 42 aynı anda (a) hayata, evrene ve her şeye ve (b) hiçbir şeye cevap olamaz. Peki yaşamı, evreni ve her şeyi int olarak nasıl ifade edersiniz?
Stuart Golodetz

19
@sgolodetz Her şeyi yeterince büyük bir int ile ifade edebilirsiniz.
Yakov Galka

Yanıtlar:


90

Her şeyden önce, operator[]ve insertüye fonksiyonları işlevsel olarak eşdeğer değildir:

  • operator[]Olacaktır arama , anahtar için bir insert varsayılan inşa bulunmazsa eğer değerini ve bir değer atamak hangi bir başvuru döndürür. Açıkçası, bu, mapped_typevarsayılan olarak yapılandırılıp atanmak yerine doğrudan başlatılmasından fayda sağlayabilirse, bu verimsiz olabilir. Bu yöntem ayrıca, bir eklemenin gerçekten gerçekleşip gerçekleşmediğini veya yalnızca önceden eklenen bir anahtarın değerinin üzerine yazıp yazmadığınızı belirlemeyi imkansız kılar.
  • insertÜye işlev genellikle unutulur rağmen, anahtar zaten harita mevcut ise hiçbir etkisi olacak ve bir döner std::pair<iterator, bool>ilgi olabilen (yerleştirme aslında yapılıp yapılmadığının araştırılması en önemlisi belirlemek için).

Listelenen tüm olasılıklardan insertüçü de neredeyse eşdeğerdir. Bir hatırlatma olarak, insertstandarttaki imzaya bakalım :

typedef pair<const Key, T> value_type;

  /* ... */

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

Öyleyse üç çağrı nasıl farklı?

  • std::make_pairŞablon argümanı kesinti dayanır ve could (ve bu durumda olacak ) fiili daha farklı türde üretim şey value_typeiçin ek çağrı gerektirecektir haritanın, std::pairhiç dönüştürmek amacıyla şablon yapıcısı value_type(yani: ekleme constiçin first_type)
  • std::pair<int, int>Ayrıca şablon yapıcısı için ek çağrıyı gerektirecektir std::pairparametreyi dönüştürmek için value_type(yani: ekleme constiçin first_type)
  • std::map<int, int>::value_typedoğrudan insertüye işlevinin beklediği parametre türü olduğundan şüpheye yer bırakmaz .

Sonunda, operator[]varsayılan oluşturma ve atama için ek bir maliyet olmadıkça mapped_typeve yeni bir anahtarın etkili bir şekilde eklenip eklenmediğini belirlemeyi umursamadığım sürece, hedef eklemek olduğunda kullanmaktan kaçınırım . Kullanırken insert, value_typemuhtemelen gitmek için yoldur.


make_pair () içindeki Key'den const Key'e dönüşüm gerçekten başka bir işlev çağrısı istiyor mu? Görünüşe göre örtük bir atama, hangi derleyicinin bunu yapmaktan mutlu olması gerektiğini gösterir.
galactica

100

C ++ 11 itibariyle, iki ana ek seçeneğiniz vardır. İlk olarak, insert()liste başlatma sözdizimi ile kullanabilirsiniz :

function.insert({0, 42});

Bu işlevsel olarak eşdeğerdir

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

ama çok daha kısa ve okunaklı. Diğer yanıtların da belirttiği gibi, bunun diğer formlara göre birkaç avantajı vardır:

  • operator[]Yaklaşımı her zaman böyle değildir ki, devredilebilmelidir eşlenen türü gerektirir.
  • operator[]Yaklaşım mevcut elemanlarını üzerine yazmak ve bu oldu olmadığını anlamanın bir yol verir olabilir.
  • Listelediğiniz diğer biçimler, insertkodunuzu yavaşlatabilecek örtük bir tür dönüşümü içerir.

En büyük dezavantajı, bu formun anahtar ve değerin kopyalanabilir olmasını gerektirmesidir, bu nedenle örneğin unique_ptrdeğerler içeren bir harita ile çalışmaz . Bu, standartta düzeltilmiştir, ancak düzeltme, standart kitaplık uygulamanıza henüz ulaşmamış olabilir.

İkincisi, emplace()yöntemi kullanabilirsiniz :

function.emplace(0, 42);

Bu, formlarının herhangi birinden daha özlüdür insert(), yalnızca taşıma türleriyle iyi çalışır unique_ptrve teorik olarak biraz daha verimli olabilir (iyi bir derleyici farkı optimize etse de). Tek büyük dezavantaj, emplaceyöntemler genellikle bu şekilde kullanılmadığından okuyucularınızı biraz şaşırtmasıdır .



12

İlk versiyon:

function[0] = 42; // version 1

42 değerini haritaya ekleyebilir veya eklemeyebilir. Anahtar 0varsa, o anahtara 42 atar ve anahtarın sahip olduğu değerin üzerine yazar. Aksi takdirde anahtar / değer çiftini ekler.

Ekleme işlevleri:

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

Öte yandan, anahtar 0haritada zaten varsa hiçbir şey yapmayın . Anahtar yoksa, anahtar / değer çiftini ekler.

Üç ekleme işlevi neredeyse aynıdır. std::map<int, int>::value_typeolduğu typedefiçin std::pair<const int, int>, ve std::make_pair()tabii ki üreten std::pair<>şablon kesinti büyü yoluyla. Ancak sonuç, sürüm 2, 3 ve 4 için aynı olmalıdır.

Hangisini kullanırdım? Ben şahsen 1. sürümü tercih ediyorum; özlü ve "doğal". Elbette, üzerine yazma davranışı istenmiyorsa, sürüm 4'ü tercih ederim, çünkü sürüm 2 ve 3'e göre daha az yazma gerektirdi. Bir anahtar / değer çiftini eklemenin tek bir fiili yolu olup olmadığını bilmiyorum std::map.

Oluşturucularından biri aracılığıyla bir haritaya değer eklemenin başka bir yolu:

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

0 tuşuyla öğenin üzerine yazmak istiyorsanız

function[0] = 42;

Aksi takdirde:

function.insert(std::make_pair(0, 42));

5

Yana C ++ 17 std::map : teklifler iki yeni ekleme yöntemleri insert_or_assign()ve try_emplace()aynı zamanda sözü edilen, sp2danny tarafından comment .

insert_or_assign()

Temel olarak, insert_or_assign()"geliştirilmiş" bir sürümüdür operator[]. Aksine operator[], insert_or_assign()varsayılan constructible olmak haritanın değer türü gerektirmez. Örneğin, aşağıdaki kod derlenmez, çünkü MyClassvarsayılan bir kurucuya sahip değildir:

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;
}

Bununla birlikte, myMap[0] = MyClass(1);aşağıdaki satırla değiştirirseniz , kod derlenir ve ekleme amaçlandığı gibi gerçekleşir:

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

Ayrıca, benzer şekilde insert(), insert_or_assign()bir döner pair<iterator, bool>. Boole değeri, truebir ekleme gerçekleşmişse ve falsebir atama yapılmışsa olur. Yineleyici, eklenen veya güncellenen öğeyi gösterir.

try_emplace()

Yukarıdakine benzer try_emplace()bir "iyileştirme" dir emplace(). Buna karşılık için emplace(), try_emplace()yerleştirme zaten harita mevcut bir anahtar nedeniyle başarısız olursa onun argümanları değiştirmez. Örneğin, aşağıdaki kod, haritada zaten depolanmış bir anahtarla bir öğeyi yerleştirmeye çalışır (bkz. *):

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;
}

Çıktı (en azından VS2017 ve Coliru için):

pMyObj eklenmedi
pMyObj yine de değiştirildi

Gördüğünüz gibi, pMyObjartık orijinal nesneyi göstermiyor. Ancak, auto [it, b] = myMap2.emplace(0, std::move(pMyObj));aşağıdaki kodla değiştirirseniz , çıktı pMyObjdeğişmeden kaldığından farklı görünür :

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

Çıktı:

pMyObj eklenmedi
pMyObj pMyObj.m_i = 2

Coliru Kodu

Lütfen dikkat: Açıklamalarımı bu cevaba sığdırmak için olabildiğince kısa ve basit tutmaya çalıştım. Daha kesin ve kapsamlı bir açıklama için Fluent C ++ ile ilgili bu makaleyi okumanızı tavsiye ederim .


3

Yukarıda belirtilen sürümler arasında bazı zaman karşılaştırmaları yapıyorum:

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));

Ekleme sürümleri arasındaki zaman farklarının çok küçük olduğu ortaya çıktı.

#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");
}

Bu, sırasıyla sürümler için verir (dosyayı 3 kez çalıştırdım, dolayısıyla her biri için 3 ardışık zaman farkı):

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

Bu nedenle, farklı uç sürümleri arasındaki sonuçlar ihmal edilebilir (yine de bir hipotez testi gerçekleştirmedi)!

map_W_3[it] = Widget(2.0);Versiyon nedeniyle Widget için varsayılan kurucu ile bir başlatma için bu örnek için% 10-15 daha fazla zaman sürer.


2

Kısacası, []operatör değerleri güncellemek için daha verimlidir, çünkü değer türünün varsayılan kurucusunu çağırmayı ve ardından ona yeni bir değer atamayı içerirken insert(), değerler eklemek için daha etkilidir.

Effective STL'den alıntılanan pasaj : Standart Şablon Kitaplığı Kullanımınızı Geliştirmenin 50 Özel Yolu, Scott Meyers, Madde 24 yardımcı olabilir.

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));
    }
}

Bunun genel programlama içermeyen bir versiyonunu seçmeye karar verebilirsiniz, ancak asıl mesele şu ki, ben bu paradigmayı ('ekle' ve 'güncelle'yi ayırarak) son derece yararlı buluyorum.


1

Std :: map'e eleman eklemek istiyorsanız - insert () işlevini kullanın ve elemanı (anahtar ile) bulmak ve ona bazılarını atamak istiyorsanız - [] operatörünü kullanın.

Eklemeyi basitleştirmek için boost :: assign library kullanın, örneğin:

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

Eklemenin başka bir ilgisini göstermek için sorunu biraz değiştiriyorum (dizelerin haritası):

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

derleyicinin "rancking [1] = 42" üzerinde hata göstermemesi; yıkıcı bir etkiye sahip olabilir!


Derleyiciler std::string::operator=(char), var olduğu için ilki için bir hata göstermezler, ancak kurucu std::string::string(char)mevcut olmadığı için ikincisi için bir hata gösterirler . Bu olmamalı C ++ her zaman serbestçe gibi herhangi bir tamsayı tarzı değişmezi yorumlaması nedeniyle bir hata üretmek charbu derleyici hata değildir, ancak bunun yerine bir programcı hatadır yüzden. Temel olarak, bunun kodunuzda bir hataya yol açıp açmadığını söylüyorum, kendinize dikkat etmeniz gereken bir şeydir. BTW, yazdırabilirsiniz rancking[0]ve ASCII kullanan bir derleyici çıktı *, yani (char)(42).
Keith M

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.