Trong bản đồ STL, sử dụng map :: insert tốt hơn []?


201

Cách đây một thời gian, tôi đã thảo luận với một đồng nghiệp về cách chèn giá trị vào bản đồ STL . Tôi thích map[key] = value; bởi vì nó cảm thấy tự nhiên và rõ ràng để đọc trong khi anh ấy thích map.insert(std::make_pair(key, value))

Tôi chỉ hỏi anh ấy và không ai trong chúng tôi có thể nhớ lý do tại sao chèn tốt hơn, nhưng tôi chắc chắn rằng đó không chỉ là một sở thích phong cách hơn là có một lý do kỹ thuật như hiệu quả. Các tài liệu tham khảo SGI STL chỉ đơn giản nói "Nói đúng ra, chức năng thành viên này là không cần thiết:. Nó chỉ tồn tại để thuận tiện"

Bất cứ ai có thể cho tôi biết lý do đó, hoặc tôi chỉ đang mơ rằng có một?


2
Cảm ơn tất cả các phản hồi tuyệt vời - chúng thực sự hữu ích. Đây là một bản demo tuyệt vời của stack overflow ở mức tốt nhất của nó. Tôi đã bị rách vì đó là câu trả lời được chấp nhận: netjeff rõ ràng hơn về các hành vi khác nhau, Greg Rogers đã đề cập đến các vấn đề về hiệu suất. Ước gì tôi có thể đánh dấu cả hai.
danio

6
Trên thực tế, với C ++ 11, có lẽ bạn tốt nhất nên sử dụng map :: emplace tránh cấu trúc kép
einpoklum

@einpoklum: Thật ra, Scott Meyers gợi ý khác trong bài nói chuyện của anh ấy "Việc tìm kiếm tiến hóa cho C ++ hiệu quả".
Thomas Việc chỉnh sửa

3
@einpoklum: Đó là trường hợp khi đặt vào bộ nhớ mới được xây dựng. Nhưng do một số yêu cầu tiêu chuẩn cho bản đồ, có những lý do kỹ thuật tại sao emplace có thể chậm hơn chèn. Cuộc nói chuyện có sẵn miễn phí trên youtube, chẳng hạn như liên kết này youtube.com/watch?v=smqT9Io_bKo @ ~ 38-40 phút đánh dấu. Đối với liên kết SO, đây là stackoverflow.com/questions/26446352/ từ
Thomas Eding

1
Tôi thực sự sẽ tranh luận với một số những gì Meyers đã trình bày, nhưng điều đó vượt quá phạm vi của chủ đề bình luận này và dù sao, tôi đoán tôi phải rút lại nhận xét trước đó của mình.
einpoklum

Câu trả lời:


240

Khi bạn viết

map[key] = value;

không có cách nào để biết bạn thay thế các valuecho key, hoặc nếu bạn tạo ra một mới keyvới value.

map::insert() sẽ chỉ tạo:

using std::cout; using std::endl;
typedef std::map<int, std::string> MyMap;
MyMap map;
// ...
std::pair<MyMap::iterator, bool> res = map.insert(MyMap::value_type(key,value));
if ( ! res.second ) {
    cout << "key " <<  key << " already exists "
         << " with value " << (res.first)->second << endl;
} else {
    cout << "created key " << key << " with value " << value << endl;
}

Đối với hầu hết các ứng dụng của tôi, tôi thường không quan tâm nếu tôi đang tạo hoặc thay thế, vì vậy tôi sử dụng dễ đọc hơn map[key] = value.


16
Cần lưu ý rằng map :: insert không bao giờ thay thế các giá trị. Và trong trường hợp chung tôi sẽ nói rằng tốt hơn là sử dụng (res.first)->secondthay vì valuetrong trường hợp thứ hai.
dalle

1
Tôi đã cập nhật để rõ ràng hơn rằng map :: insert không bao giờ thay thế. Tôi rời khỏi elsevì tôi nghĩ rằng việc sử dụng valuelà rõ ràng hơn so với trình vòng lặp. Chỉ khi loại giá trị có một ctor sao chép bất thường hoặc op == thì nó sẽ khác và loại đó sẽ gây ra các vấn đề khác khi sử dụng các thùng chứa STL như bản đồ.
netjeff

1
map.insert(std::make_pair(key,value))nên map.insert(MyMap::value_type(key,value)). Loại được trả về make_pairkhông khớp với loại được thực hiện insertvà giải pháp hiện tại yêu cầu chuyển đổi
David Rodríguez - dribeas

1
có một cách để biết bạn đã chèn hay chỉ định operator[], chỉ cần so sánh kích thước trước và sau. Imho chỉ có thể gọi map::operator[]cho các kiểu xây dựng mặc định là quan trọng hơn nhiều.
idclev 463035818

@ DavidRodríguez-dribeas: Gợi ý hay, tôi đã cập nhật mã trong câu trả lời của mình.
netjeff

53

Cả hai có ngữ nghĩa khác nhau khi nói đến khóa đã tồn tại trên bản đồ. Vì vậy, họ không thực sự so sánh trực tiếp.

Nhưng phiên bản toán tử [] yêu cầu xây dựng giá trị mặc định, sau đó gán, vì vậy nếu điều này đắt hơn thì sao chép xây dựng, thì nó sẽ đắt hơn. Đôi khi, cấu trúc mặc định không có ý nghĩa, và sau đó không thể sử dụng phiên bản toán tử [].


1
make_ Pair có thể yêu cầu một hàm tạo sao chép - điều đó sẽ tệ hơn so với trình tạo mặc định. +1 dù sao đi nữa.

1
Điều chính là, như bạn đã nói, họ có ngữ nghĩa khác nhau. Vì vậy, không phải là tốt hơn so với người khác, chỉ cần sử dụng một trong đó làm những gì bạn cần.
jalf

Tại sao hàm tạo sao chép sẽ tệ hơn hàm tạo mặc định theo sau là gán? Nếu đúng như vậy, thì người đã viết lớp đã bỏ lỡ điều gì đó, bởi vì bất kỳ toán tử nào = không, họ nên làm điều tương tự trong hàm tạo sao chép.
Steve Jessop

1
Đôi khi việc xây dựng mặc định cũng tốn kém như chính bài tập. Đương nhiên giao và sao chép xây dựng sẽ tương đương.
Greg Rogers

@Arkadiy Trong một bản dựng được tối ưu hóa, trình biên dịch sẽ thường loại bỏ nhiều lệnh gọi xây dựng sao chép không cần thiết.
antred

35

Một điều cần lưu ý với std::map:

myMap[nonExistingKey];sẽ tạo một mục mới trong bản đồ, được khóa để nonExistingKeykhởi tạo thành giá trị mặc định.

Điều này làm tôi sợ địa ngục ngay lần đầu tiên tôi nhìn thấy nó (trong khi đập đầu vào một lỗi di sản khó chịu). Sẽ không mong đợi nó. Đối với tôi, nó trông giống như một hoạt động có được và tôi không mong đợi "tác dụng phụ". Thích map.find()khi nhận được từ bản đồ của bạn.


3
Đó là một cái nhìn tốt, mặc dù bản đồ băm là khá phổ biến cho định dạng này. Nó có thể là một trong những "điều kỳ lạ mà không ai nghĩ là lạ" chỉ vì mức độ phổ biến của chúng sử dụng cùng một quy ước
Stephen J

19

Nếu thành tích hiệu năng của hàm tạo mặc định không phải là vấn đề, xin vui lòng, vì tình yêu của chúa, hãy đi với phiên bản dễ đọc hơn.

:)


5
Thứ hai! Phải đánh dấu điều này lên. Quá nhiều người đánh đổi sự mập mờ để tăng tốc nano giây. Xin thương xót chúng tôi những linh hồn tội nghiệp phải duy trì sự tàn bạo như vậy!
Mr.Ree

6
Như Greg Rogers đã viết: "Cả hai có ngữ nghĩa khác nhau khi nói đến chìa khóa đã tồn tại trên bản đồ. Vì vậy, chúng không thực sự có thể so sánh trực tiếp."
dalle

Đây là một câu hỏi và câu trả lời cũ. Nhưng "phiên bản dễ đọc hơn" là một lý do ngu ngốc. Bởi vì cái nào dễ đọc nhất phụ thuộc vào người.
vallentin

14

insert là tốt hơn từ điểm an toàn ngoại lệ.

Biểu thức map[key] = valuethực sự là hai thao tác:

  1. map[key] - tạo một yếu tố bản đồ với giá trị mặc định.
  2. = value - sao chép giá trị vào phần tử đó.

Một ngoại lệ có thể xảy ra ở bước thứ hai. Kết quả là thao tác sẽ chỉ được thực hiện một phần (một phần tử mới đã được thêm vào bản đồ, nhưng phần tử đó không được khởi tạo với value). Tình huống khi một hoạt động không hoàn thành, nhưng trạng thái hệ thống được sửa đổi, được gọi là hoạt động với "tác dụng phụ".

insertHoạt động mang lại sự đảm bảo mạnh mẽ, có nghĩa là nó không có tác dụng phụ ( https://en.wikipedia.org/wiki/Exception_safe ).insertđược thực hiện hoàn toàn hoặc nó rời khỏi bản đồ ở trạng thái không thay đổi.

http://www.cplusplus.com/reference/map/map/insert/ :

Nếu một phần tử duy nhất được chèn vào, không có thay đổi nào trong vùng chứa trong trường hợp ngoại lệ (bảo đảm mạnh).


1
quan trọng hơn, insert không yêu cầu giá trị là có thể xây dựng mặc định.
UmNyobe

13

Nếu ứng dụng của bạn có tốc độ quan trọng, tôi sẽ tư vấn sử dụng toán tử [] bởi vì nó tạo ra tổng số 3 bản sao của đối tượng ban đầu trong đó có 2 đối tượng tạm thời và sớm muộn bị phá hủy.

Nhưng trong insert (), 4 bản sao của đối tượng ban đầu được tạo ra trong đó 3 bản sao là đối tượng tạm thời (không nhất thiết là "tạm thời") và bị hủy.

Có nghĩa là thêm thời gian cho: 1. Cấp phát bộ nhớ cho một đối tượng 2. Một lệnh gọi hàm tạo thêm 3. Một lệnh gọi hàm hủy thêm 4. Phân bổ bộ nhớ một đối tượng

Nếu các đối tượng của bạn lớn, các hàm tạo là điển hình, các hàm hủy sẽ giải phóng rất nhiều tài nguyên, các điểm trên còn nhiều hơn thế. Về khả năng đọc, tôi nghĩ cả hai đều đủ công bằng.

Câu hỏi tương tự xuất hiện trong đầu tôi nhưng không quá dễ đọc mà là tốc độ. Đây là một mã mẫu mà qua đó tôi đã biết về điểm tôi đã đề cập.

class Sample
{
    static int _noOfObjects;

    int _objectNo;
public:
    Sample() :
        _objectNo( _noOfObjects++ )
    {
        std::cout<<"Inside default constructor of object "<<_objectNo<<std::endl;
    }

    Sample( const Sample& sample) :
    _objectNo( _noOfObjects++ )
    {
        std::cout<<"Inside copy constructor of object "<<_objectNo<<std::endl;
    }

    ~Sample()
    {
        std::cout<<"Destroying object "<<_objectNo<<std::endl;
    }
};
int Sample::_noOfObjects = 0;


int main(int argc, char* argv[])
{
    Sample sample;
    std::map<int,Sample> map;

    map.insert( std::make_pair<int,Sample>( 1, sample) );
    //map[1] = sample;
    return 0;
}

Đầu ra khi insert () được sử dụng Đầu ra khi toán tử [] được sử dụng


4
Bây giờ chạy thử nghiệm đó một lần nữa với tối ưu hóa đầy đủ được kích hoạt.
antred

2
Ngoài ra, hãy xem xét những gì toán tử [] thực sự làm. Đầu tiên, nó tìm kiếm bản đồ cho một mục phù hợp với khóa được chỉ định. Nếu nó tìm thấy một, thì nó sẽ ghi đè lên giá trị của mục đó với giá trị được chỉ định. Nếu không, nó sẽ chèn một mục mới với khóa và giá trị được chỉ định. Bản đồ của bạn càng lớn, người điều hành sẽ mất nhiều thời gian hơn để tìm kiếm bản đồ. Tại một số điểm, điều này sẽ nhiều hơn bù cho một cuộc gọi c'tor sao chép bổ sung (nếu điều đó thậm chí còn ở trong chương trình cuối cùng sau khi trình biên dịch đã thực hiện phép thuật tối ưu hóa của nó).
antred

1
@antred, insertphải thực hiện cùng một tìm kiếm, vì vậy không có sự khác biệt trong đó [](vì các khóa bản đồ là duy nhất).
Sz.

Minh họa tuyệt vời về những gì đang diễn ra với bản in - nhưng 2 bit mã này thực sự đang làm gì? Tại sao 3 bản sao của đối tượng ban đầu cần thiết?
rbennett485

1. phải thực hiện toán tử gán, hoặc bạn sẽ nhận được các số sai trong hàm hủy 2. sử dụng std :: move khi xây dựng cặp để tránh việc sao chép quá mức "map.insert (std :: make_ Pair <int, Sample> (1, std: : di chuyển (mẫu))); "
Fl0

10

Bây giờ trong c ++ 11 Tôi nghĩ rằng cách tốt nhất để chèn một cặp trong bản đồ STL là:

typedef std::map<int, std::string> MyMap;
MyMap map;

auto& result = map.emplace(3,"Hello");

Các kết quả sẽ là một cặp với:

  • Phần tử đầu tiên (result.first), trỏ đến cặp được chèn hoặc trỏ đến cặp có khóa này nếu khóa đã tồn tại.

  • Phần tử thứ hai (result.second), đúng nếu phần chèn là đúng hoặc sai thì có gì đó không đúng.

PS: Nếu bạn không đặt trường hợp về đơn đặt hàng, bạn có thể sử dụng std :: unordered_map;)

Cảm ơn!


9

Một gotcha với map :: insert () là nó sẽ không thay thế một giá trị nếu khóa đã tồn tại trong bản đồ. Tôi đã thấy mã C ++ được viết bởi các lập trình viên Java nơi họ đã mong đợi insert () hoạt động giống như Map.put () trong Java nơi các giá trị được thay thế.


2

Một lưu ý là bạn cũng có thể sử dụng Boost.Assign :

using namespace std;
using namespace boost::assign; // bring 'map_list_of()' into scope

void something()
{
    map<int,int> my_map = map_list_of(1,2)(2,3)(3,4)(4,5)(5,6);
}

1

Đây là một ví dụ khác, cho thấy operator[] ghi đè giá trị cho khóa nếu nó tồn tại, nhưng .insert không ghi đè lên giá trị nếu nó tồn tại.

void mapTest()
{
  map<int,float> m;


  for( int i = 0 ; i  <=  2 ; i++ )
  {
    pair<map<int,float>::iterator,bool> result = m.insert( make_pair( 5, (float)i ) ) ;

    if( result.second )
      printf( "%d=>value %f successfully inserted as brand new value\n", result.first->first, result.first->second ) ;
    else
      printf( "! The map already contained %d=>value %f, nothing changed\n", result.first->first, result.first->second ) ;
  }

  puts( "All map values:" ) ;
  for( map<int,float>::iterator iter = m.begin() ; iter !=m.end() ; ++iter )
    printf( "%d=>%f\n", iter->first, iter->second ) ;

  /// now watch this.. 
  m[5]=900.f ; //using operator[] OVERWRITES map values
  puts( "All map values:" ) ;
  for( map<int,float>::iterator iter = m.begin() ; iter !=m.end() ; ++iter )
    printf( "%d=>%f\n", iter->first, iter->second ) ;

}

1

Đây là một trường hợp khá hạn chế, nhưng đánh giá từ các ý kiến ​​tôi đã nhận được tôi nghĩ rằng nó đáng chú ý.

Tôi đã thấy những người trong quá khứ sử dụng bản đồ ở dạng

map< const key, const val> Map;

để tránh các trường hợp ghi đè giá trị ngẫu nhiên, nhưng sau đó tiếp tục viết bằng một số đoạn mã khác:

const_cast< T >Map[]=val;

Lý do của họ để làm điều này khi tôi nhớ lại là vì họ chắc chắn rằng trong những đoạn mã nhất định này, họ sẽ không ghi đè lên các giá trị bản đồ; do đó, đi trước với phương pháp 'dễ đọc' hơn[] .

Tôi chưa bao giờ thực sự gặp rắc rối trực tiếp từ mã được viết bởi những người này, nhưng tôi cảm thấy mạnh mẽ cho đến ngày hôm nay rằng rủi ro - dù nhỏ - không nên được thực hiện khi họ có thể dễ dàng tránh được.

Trong trường hợp bạn đang xử lý các giá trị bản đồ tuyệt đối không được ghi đè, hãy sử dụng insert. Đừng tạo ra ngoại lệ chỉ để dễ đọc.


Nhiều người khác nhau đã viết điều đó? Chắc chắn sử dụng insert(không input), vì const_castsẽ làm cho bất kỳ giá trị nào trước đó bị ghi đè, rất không phải là const. Hoặc, không đánh dấu loại giá trị là const. (Loại điều đó thường là kết quả cuối cùng của const_cast, vì vậy hầu như luôn luôn là một lá cờ đỏ biểu thị lỗi ở một nơi khác.)
Potatoswatter

@Potatoswatter Bạn nói đúng. Tôi chỉ thấy rằng const_cast [] được một số người sử dụng với các giá trị bản đồ const khi họ chắc chắn rằng họ sẽ không thay thế một giá trị cũ trong một số đoạn mã nhất định; vì [] bản thân nó dễ đọc hơn. Như tôi đã đề cập trong phần cuối cùng của câu trả lời của mình, tôi khuyên bạn nên sử dụng inserttrong trường hợp bạn muốn ngăn các giá trị bị ghi đè. (Chỉ cần thay đổi inputthành insert- cảm ơn)
dk123

@Potatoswatter Nếu tôi nhớ lại một cách chính xác, những lý do chính mà mọi người dường như đang sử dụng const_cast<T>(map[key])là 1. [] dễ đọc hơn, 2. họ tin tưởng vào một số đoạn mã nhất định mà họ sẽ không ghi đè lên giá trị và 3. họ không muốn các bit khác của mã không biết ghi đè giá trị của chúng - do đó const value.
dk123

2
Tôi chưa bao giờ nghe nói về việc này được thực hiện; Nơi mà bạn đã nhìn thấy điều này? Viết const_castdường như nhiều hơn phủ nhận "khả năng đọc" thêm [], và sự tự tin đó gần như đủ cơ sở để sa thải nhà phát triển. Điều kiện thời gian chạy khó khăn được giải quyết bằng thiết kế chống đạn, không phải cảm xúc ruột.
Potatoswatter

@Potatoswatter Tôi nhớ đó là trong một trong những công việc trước đây của tôi phát triển các trò chơi giáo dục. Tôi không bao giờ có thể khiến những người viết mã thay đổi thói quen của họ. Bạn hoàn toàn đúng và tôi hoàn toàn đồng ý với bạn. Từ ý kiến ​​của bạn, tôi đã quyết định rằng điều này có lẽ đáng chú ý hơn câu trả lời ban đầu của tôi và do đó tôi đã cập nhật nó để phản ánh điều này. Cảm ơn!
dk123

1

Thực tế là hàm std :: map insert()không ghi đè giá trị được liên kết với khóa cho phép chúng ta viết mã liệt kê đối tượng như thế này:

string word;
map<string, size_t> dict;
while(getline(cin, word)) {
    dict.insert(make_pair(word, dict.size()));
}

Đây là một vấn đề khá phổ biến khi chúng ta cần ánh xạ các đối tượng không phải là duy nhất khác nhau sang một số id trong phạm vi 0..N. Những id đó có thể được sử dụng sau này, ví dụ, trong các thuật toán đồ thị. Thay thế với operator[]sẽ trông ít đọc hơn theo ý kiến ​​của tôi:

string word;
map<string, size_t> dict;
while(getline(cin, word)) {
    size_t sz = dict.size();
    if (!dict.count(word))
        dict[word] = sz; 
} 
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.