Cách ưa thích / thành ngữ để chèn vào bản đồ là gì?


111

Tôi đã xác định được bốn cách khác nhau để chèn các phần tử vào 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));

Cách nào trong số đó là cách ưa thích / thành ngữ? (Và có cách nào khác mà tôi chưa nghĩ ra không?)


26
Bản đồ của bạn phải được gọi là "câu trả lời", không phải "chức năng"
Vincent Robert

2
@Vincent: Hửm? Một hàm về cơ bản là một bản đồ giữa hai tập hợp.
fredoverflow

7
@FredOverflow: dường như bình luận của Vincent là kinda đùa về cuốn sách nào đó ...
Victor Sorokin

1
Nó có vẻ mâu thuẫn với bản gốc - 42 không thể đồng thời là câu trả lời cho (a) sự sống, vũ trụ và mọi thứ, và (b) không có gì. Nhưng sau đó làm thế nào để bạn thể hiện cuộc sống, vũ trụ và mọi thứ như một int?
Stuart Golodetz

19
@sgolodetz Bạn có thể thể hiện mọi thứ với một int đủ lớn.
Yakov Galka

Câu trả lời:


90

Trước hết, operator[]và các insertchức năng thành viên không tương đương về mặt chức năng:

  • Hàm operator[]sẽ tìm kiếm khóa, chèn giá trị được xây dựng mặc định nếu không tìm thấy và trả về một tham chiếu mà bạn gán giá trị. Rõ ràng, điều này có thể không hiệu quả nếu mapped_typelợi ích có thể được khởi tạo trực tiếp thay vì được xây dựng và gán mặc định. Phương pháp này cũng giúp bạn không thể xác định xem liệu việc chèn đã thực sự diễn ra hay chưa hay bạn chỉ ghi đè giá trị cho một khóa đã được chèn trước đó
  • Hàm insertthành viên sẽ không có hiệu lực nếu khóa đã có trong bản đồ và, mặc dù nó thường bị quên, trả về một giá trị std::pair<iterator, bool>có thể được quan tâm (đáng chú ý nhất là để xác định xem việc chèn đã thực sự được thực hiện chưa).

Từ tất cả các khả năng được liệt kê để gọi insert, cả ba đều gần như tương đương. Xin nhắc lại, chúng ta hãy xem xét insertchữ ký trong tiêu chuẩn:

typedef pair<const Key, T> value_type;

  /* ... */

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

Vậy ba cách gọi khác nhau như thế nào?

  • std::make_pairdựa vào suy luận đối số mẫu và có thể (và trong trường hợp này sẽ ) tạo ra một thứ gì đó thuộc loại khác với thực tế value_typecủa bản đồ, điều này sẽ yêu cầu một lệnh gọi bổ sung đến hàm std::pairtạo mẫu để chuyển đổi thành value_type(tức là: thêm constvào first_type)
  • std::pair<int, int>cũng sẽ yêu cầu một lệnh gọi bổ sung tới hàm tạo mẫu std::pairđể chuyển đổi tham số thành value_type(tức là: thêm constvào first_type)
  • std::map<int, int>::value_typehoàn toàn không có chỗ để nghi ngờ vì nó trực tiếp là kiểu tham số mà inserthàm thành viên mong đợi .

Cuối cùng, tôi sẽ tránh sử dụng operator[]khi mục tiêu là để chèn, trừ khi không có thêm chi phí trong việc xây dựng và gán mặc định mapped_type, và tôi không quan tâm đến việc xác định xem khóa mới có được chèn hiệu quả hay không. Khi sử dụng insert, xây dựng một value_typecó lẽ là một cách tốt nhất.


Việc chuyển đổi từ Key thành const Key trong make_pair () có thực sự yêu cầu một cuộc gọi hàm khác không? Có vẻ như một dàn diễn viên ngầm sẽ là đủ mà trình biên dịch sẽ vui lòng làm như vậy.
galactica

99

Đối với C ++ 11, bạn có hai tùy chọn bổ sung chính. Đầu tiên, bạn có thể sử dụng insert()với cú pháp khởi tạo danh sách:

function.insert({0, 42});

Chức năng này tương đương với

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

nhưng ngắn gọn và dễ đọc hơn nhiều. Như các câu trả lời khác đã lưu ý, điều này có một số lợi thế so với các hình thức khác:

  • Phương operator[]pháp này yêu cầu loại được ánh xạ có thể gán được, điều này không phải lúc nào cũng đúng.
  • Phương operator[]pháp này có thể ghi đè các phần tử hiện có và không cho bạn biết liệu điều này đã xảy ra hay chưa.
  • Các dạng khác insertmà bạn liệt kê liên quan đến chuyển đổi kiểu ngầm, có thể làm chậm mã của bạn.

Hạn chế chính là biểu mẫu này được sử dụng để yêu cầu khóa và giá trị có thể sao chép được, vì vậy nó sẽ không hoạt động với ví dụ như một bản đồ có unique_ptrcác giá trị. Điều đó đã được khắc phục trong tiêu chuẩn, nhưng bản sửa lỗi có thể chưa đạt đến việc triển khai thư viện tiêu chuẩn của bạn.

Thứ hai, bạn có thể sử dụng emplace()phương pháp:

function.emplace(0, 42);

Điều này ngắn gọn hơn bất kỳ hình thức nào insert(), hoạt động tốt với các kiểu chỉ di chuyển như unique_ptrvà về mặt lý thuyết có thể hiệu quả hơn một chút (mặc dù một trình biên dịch tốt sẽ tối ưu hóa sự khác biệt). Hạn chế lớn duy nhất là nó có thể làm độc giả của bạn ngạc nhiên một chút, vì emplacecác phương pháp thường không được sử dụng theo cách đó.


8
đó cũng là mới insert_or_assigntry_emplace
sp2danny

11

Phiên bản đầu tiên:

function[0] = 42; // version 1

có thể chèn hoặc không thể chèn giá trị 42 vào bản đồ. Nếu khóa 0tồn tại, thì nó sẽ gán 42 cho khóa đó, ghi đè lên bất kỳ giá trị nào mà khóa đó có. Nếu không, nó sẽ chèn cặp khóa / giá trị.

Các chức năng chèn:

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

mặt khác, không làm bất cứ điều gì nếu khóa 0đã tồn tại trong bản đồ. Nếu khóa không tồn tại, nó sẽ chèn cặp khóa / giá trị.

Ba chức năng chèn gần như giống hệt nhau. std::map<int, int>::value_typetypedeffor std::pair<const int, int>, và std::make_pair()rõ ràng là tạo ra std::pair<>phép thuật khấu trừ thông qua mẫu. Tuy nhiên, kết quả cuối cùng sẽ giống nhau cho các phiên bản 2, 3 và 4.

Tôi sẽ sử dụng cái nào? Cá nhân tôi thích phiên bản 1 hơn; nó ngắn gọn và "tự nhiên". Tất nhiên, nếu hành vi ghi đè của nó không được mong muốn, thì tôi sẽ thích phiên bản 4 hơn, vì nó yêu cầu nhập ít hơn so với phiên bản 2 và 3. Tôi không biết liệu có một cách thực tế duy nhất để chèn các cặp khóa / giá trị vào một std::map.

Một cách khác để chèn giá trị vào bản đồ thông qua một trong các hàm tạo của nó:

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

Nếu bạn muốn ghi đè phần tử bằng phím 0

function[0] = 42;

Nếu không thì:

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

5

C ++ 17 std::map cung cấp hai phương thức chèn mới: insert_or_assign()try_emplace(), như đã được đề cập trong nhận xét của sp2danny .

insert_or_assign()

Về cơ bản, insert_or_assign()là một phiên bản "cải tiến" của operator[]. Ngược lại operator[], insert_or_assign()không yêu cầu loại giá trị của bản đồ phải có thể xây dựng mặc định. Ví dụ: mã sau không biên dịch, vì MyClasskhông có hàm tạo mặc định:

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

Tuy nhiên, nếu bạn thay thế myMap[0] = MyClass(1);bằng dòng sau, thì mã biên dịch và việc chèn diễn ra như dự định:

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

Hơn nữa, tương tự như insert(), insert_or_assign()trả về a pair<iterator, bool>. Giá trị Boolean là truenếu một lần chèn xảy ra và falsenếu một phép gán đã được thực hiện. Trình lặp trỏ đến phần tử đã được chèn hoặc cập nhật.

try_emplace()

Tương tự như trên, try_emplace()là một "cải tiến" của emplace(). Ngược lại emplace(), try_emplace()không sửa đổi các đối số của nó nếu việc chèn không thành công do một khóa đã tồn tại trong bản đồ. Ví dụ: mã sau cố gắng thay thế một phần tử bằng khóa đã được lưu trữ trong bản đồ (xem *):

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

Đầu ra (ít nhất cho VS2017 và Coliru):

pMyObj chưa được chèn
pMyObj vẫn được sửa đổi

Như bạn có thể thấy, pMyObjkhông còn trỏ đến đối tượng ban đầu. Tuy nhiên, nếu bạn thay thế auto [it, b] = myMap2.emplace(0, std::move(pMyObj));bằng đoạn mã sau, thì kết quả đầu ra sẽ khác, vì pMyObjkhông thay đổi:

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

Đầu ra:

pMyObj chưa được chèn
pMyObj pMyObj.m_i = 2

Mã trên Coliru

Xin lưu ý: Tôi đã cố gắng giữ cho lời giải thích của mình ngắn gọn và đơn giản nhất có thể để phù hợp với câu trả lời này. Để có mô tả chính xác và toàn diện hơn, tôi khuyên bạn nên đọc bài viết này về Fluent C ++ .


3

Tôi đã chạy một số so sánh thời gian giữa các phiên bản nói trên:

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

Hóa ra rằng sự khác biệt về thời gian giữa các phiên bản chèn là rất nhỏ.

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

Điều này tương ứng cho các phiên bản (tôi đã chạy tệp 3 lần, do đó có 3 thời gian khác biệt liên tiếp cho mỗi phiên bản):

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

Do đó, kết quả giữa các phiên bản chèn khác nhau có thể bị bỏ qua (mặc dù không thực hiện kiểm tra giả thuyết)!

Các map_W_3[it] = Widget(2.0);phiên bản mất khoảng hơn 10-15% thời gian cho ví dụ này do một khởi với constructor mặc định cho Widget.


2

Tóm lại, []toán tử hiệu quả hơn để cập nhật giá trị vì nó liên quan đến việc gọi hàm tạo mặc định của kiểu giá trị và sau đó gán cho nó một giá trị mới, trong khi insert()hiệu quả hơn để thêm giá trị.

Đoạn trích được trích dẫn từ STL Hiệu quả: 50 Cách Cụ thể để Cải thiện Việc Sử dụng Thư viện Mẫu Chuẩn của Scott Meyers, Mục 24 có thể hữu ích.

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

Bạn có thể quyết định chọn một phiên bản không lập trình chung chung của điều này, nhưng điểm mấu chốt là tôi thấy mô hình này (phân biệt 'thêm' và 'cập nhật') cực kỳ hữu ích.


1

Nếu bạn muốn chèn phần tử trong std :: map - hãy sử dụng hàm insert (), và nếu bạn muốn tìm phần tử (bằng phím) và gán một số cho nó - hãy sử dụng toán tử [].

Để đơn giản hóa việc chèn, hãy sử dụng boost :: gán thư viện, như sau:

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

Tôi chỉ thay đổi vấn đề một chút (bản đồ của các chuỗi) để thể hiện sự quan tâm khác của chèn:

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

thực tế là trình biên dịch không hiển thị lỗi trên "rancking [1] = 42;" có thể có tác động tàn khốc!


Các trình biên dịch không hiển thị lỗi cho cái trước vì std::string::operator=(char)tồn tại, nhưng chúng hiển thị lỗi cho cái sau vì hàm tạo std::string::string(char)không tồn tại. Nó không nên tạo ra lỗi vì C ++ luôn tự do diễn giải bất kỳ ký tự kiểu số nguyên nào char, vì vậy đây không phải là lỗi trình biên dịch, mà thay vào đó là lỗi của lập trình viên. Về cơ bản, tôi chỉ nói rằng liệu điều đó có tạo ra một lỗi trong mã của bạn hay không là điều bạn phải tự xem xét. BTW, bạn có thể in rancking[0]và một trình biên dịch sử dụng ASCII chí đầu ra *, đó là (char)(42).
Keith M

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.