วิธีที่ต้องการ / สำนวนในการแทรกลงในแผนที่คืออะไร?


115

ฉันได้ระบุสี่วิธีในการแทรกองค์ประกอบลงใน a 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));

วิธีใดที่เป็นที่ต้องการ / สำนวน? (และมีวิธีอื่นที่ฉันคิดไม่ถึง?)


27
แผนที่ของคุณควรเรียกว่า "คำตอบ" ไม่ใช่ "ฟังก์ชัน"
Vincent Robert

2
@ Vincent: หืม? โดยทั่วไปแล้วฟังก์ชันจะเป็นแผนที่ระหว่างสองชุด
fredoverflow

8
@FredOverflow: ดูเหมือนว่าความคิดเห็นของ Vincent จะเป็นเรื่องตลกเกี่ยวกับหนังสือบางเล่ม ...
Victor Sorokin

2
ดูเหมือนว่าจะขัดแย้งกับต้นฉบับ - 42 ไม่สามารถเป็นคำตอบของ (ก) ชีวิตจักรวาลและทุกสิ่งทุกอย่างได้ในเวลาเดียวกันและ (ข) ไม่มีอะไร แต่แล้วคุณจะแสดงชีวิตจักรวาลและทุกสิ่งเป็น int ได้อย่างไร?
Stuart Golodetz

20
@sgolodetz คุณสามารถแสดงทุกอย่างด้วย int ที่ใหญ่พอ
Yakov Galka

คำตอบ:


92

ครั้งแรกของทั้งหมดoperator[]และinsertฟังก์ชั่นสมาชิกจะไม่เทียบเท่าหน้าที่:

  • operator[]จะค้นหาสำหรับคีย์ใส่ค่าเริ่มต้นสร้างความคุ้มค่าหากไม่พบและกลับอ้างอิงที่คุณกำหนดค่า เห็นได้ชัดว่าสิ่งนี้จะไม่มีประสิทธิภาพหากmapped_typeสามารถได้รับประโยชน์จากการเริ่มต้นโดยตรงแทนที่จะสร้างและกำหนดค่าเริ่มต้น วิธีนี้ยังทำให้ไม่สามารถระบุได้ว่ามีการแทรกเกิดขึ้นจริงหรือว่าคุณได้เขียนทับเฉพาะค่าสำหรับคีย์ที่แทรกไว้ก่อนหน้านี้
  • insertฟังก์ชันสมาชิกจะไม่มีผลถ้าคีย์มีอยู่แล้วในแผนที่และแม้ว่ามันมักจะถูกลืมส่งกลับstd::pair<iterator, bool>ซึ่งสามารถที่น่าสนใจ (สะดุดตาที่สุดเพื่อตรวจสอบว่าได้รับการแทรกจริงทำ)

จากความเป็นไปได้ทั้งหมดที่ระบุไว้ในการโทรinsertทั้งสามเกือบจะเทียบเท่ากัน เพื่อเป็นการเตือนความจำเรามาดูinsertลายเซ็นในมาตรฐาน:

typedef pair<const Key, T> value_type;

  /* ... */

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

แล้วการเรียกทั้งสามแตกต่างกันอย่างไร?

  • 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และฉันไม่สนใจเกี่ยวกับการพิจารณาว่ามีการแทรกคีย์ใหม่อย่างมีประสิทธิภาพหรือไม่ เมื่อใช้การinsertสร้าง a value_typeอาจเป็นวิธีที่จะไป


การแปลงจาก Key เป็น const Key ใน make_pair () ขอเรียกใช้ฟังก์ชันอื่นจริงๆหรือไม่ ดูเหมือนว่านักแสดงโดยปริยายจะเพียงพอที่คอมไพเลอร์ควรยินดีที่จะทำเช่นนั้น
galactica

100

ตั้งแต่ C ++ 11 คุณมีสองตัวเลือกเพิ่มเติมที่สำคัญ ขั้นแรกคุณสามารถใช้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มักไม่ใช้วิธีการดังกล่าว


8
นอกจากนี้ยังมีinsert_or_assignและtry_emplace ใหม่
sp2danny

12

รุ่นแรก:

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อยู่แล้วในแผนที่ หากไม่มีคีย์จะแทรกคู่คีย์ / ค่า

ฟังก์ชันเม็ดมีดทั้งสามเหมือนกันเกือบทั้งหมด std::map<int, int>::value_typeเป็นtypedefสำหรับstd::pair<const int, int>และstd::make_pair()เห็นได้ชัดว่าสร้างไฟล์std::pair<>เวทมนตร์การหักผ่านเทมเพลต อย่างไรก็ตามผลลัพธ์สุดท้ายควรจะเหมือนกันสำหรับเวอร์ชัน 2, 3 และ 4

ฉันจะใช้อันไหน? ฉันชอบเวอร์ชัน 1 เป็นการส่วนตัว มันกระชับและ "เป็นธรรมชาติ" แน่นอนว่าหากไม่ต้องการพฤติกรรมการเขียนทับฉันจะชอบเวอร์ชัน 4 เนื่องจากต้องพิมพ์น้อยกว่าเวอร์ชัน 2 และ 3 ฉันไม่รู้ว่ามีวิธีการแทรกคู่คีย์ / ค่าโดยพฤตินัยเพียงวิธีเดียวหรือไม่std::map.

อีกวิธีหนึ่งในการแทรกค่าลงในแผนที่โดยใช้หนึ่งในตัวสร้าง:

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

function[0] = 42;

มิฉะนั้น:

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

5

ตั้งแต่C ++ 17 std::mapข้อเสนอสองวิธีการแทรกใหม่insert_or_assign()และtry_emplace()ยังเป็นที่กล่าวถึงในการแสดงความคิดเห็นโดย sp2danny

insert_or_assign()

โดยทั่วไปinsert_or_assign()เป็น "ดีขึ้น" operator[]รุ่น ในทางตรงกันข้ามกับoperator[], insert_or_assign()ไม่จำเป็นต้องของแผนที่ประเภทค่าที่จะเริ่มต้น constructible ตัวอย่างเช่นโค้ดต่อไปนี้ไม่ได้คอมไพล์เนื่องจาก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

รหัสบน Coliru

โปรดทราบ: ฉันพยายามทำให้คำอธิบายของฉันสั้นและเรียบง่ายที่สุดเท่าที่จะทำได้เพื่อให้พอดีกับคำตอบนี้ สำหรับคำอธิบายที่แม่นยำมากขึ้นและครอบคลุมผมขอแนะนำให้อ่านบทความนี้เกี่ยวกับเจ้าของภาษา 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 มิลลิวินาที, 2078 มิลลิวินาที, 2072 มิลลิวินาที

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

2290 มิลลิวินาที, 2037 มิลลิวินาที, 2046 มิลลิวินาที

 map_W_3[it] = Widget(2.0);

2592 มิลลิวินาที 2278 มิลลิวินาที 2296 มิลลิวินาที

 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% มากขึ้นเช่นนี้เนื่องจากมีการเริ่มต้นด้วยการสร้างการเริ่มต้นสำหรับ Widget


2

ในระยะสั้น[]ตัวดำเนินการมีประสิทธิภาพมากกว่าในการอัปเดตค่าเนื่องจากเกี่ยวข้องกับการเรียกใช้ตัวสร้างเริ่มต้นของประเภทค่าแล้วกำหนดค่าใหม่ในขณะที่insert()มีประสิทธิภาพในการเพิ่มค่า

ข้อมูลโค้ดที่ยกมาจากEffective STL: 50 วิธีเฉพาะในการปรับปรุงการใช้ไลบรารีเทมเพลตมาตรฐานโดย Scott Meyers รายการ 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 () และหากคุณต้องการค้นหาองค์ประกอบ (ตามคีย์) และกำหนดให้ใช้ตัวดำเนินการ []

เพื่อให้การแทรกง่ายขึ้นให้ใช้ boost :: กำหนดไลบรารีดังนี้:

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ดังนั้นนี้ไม่ได้เป็นข้อผิดพลาดคอมไพเลอร์ แต่แทนที่จะเป็นข้อผิดพลาดโปรแกรมเมอร์ โดยพื้นฐานแล้วฉันแค่บอกว่าการแนะนำข้อบกพร่องในโค้ดของคุณหรือไม่นั้นเป็นสิ่งที่คุณต้องระวังด้วยตัวคุณเอง BTW คุณสามารถพิมพ์rancking[0]และเรียบเรียงโดยใช้ส่งออกจะ ASCII ซึ่งเป็น* (char)(42)
Keith M

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.