Чи краще на картах STL використовувати map :: insert, ніж []?


201

Деякий час тому я мав дискусію з колегою про те, як вставити значення в STL- карти . Я віддав перевагу map[key] = value; тому, що він відчуває себе природним і його чітко читати, тоді як він вважав за краще map.insert(std::make_pair(key, value))

Я просто запитав його, і ніхто з нас не може згадати причину, чому вставка краща, але я впевнений, що це було не просто вподобанням стилю, а є технічна причина, наприклад, ефективність. У посиланні SGI STL просто сказано: "Строго кажучи, ця функція члена є непотрібною: вона існує лише для зручності".

Хтось може мені сказати цю причину, або я просто мрію, що є така?


2
Дякую за всі чудові відгуки - вони були дуже корисні. Це чудова демонстрація переповнення стека в найкращих випадках. Мене розірвало, на що має бути прийнята відповідь: netjeff є більш явним щодо різної поведінки, Грег Роджерс згадав про проблеми з ефективністю. Бажаю, щоб я міг поставити обоє.
danio

6
Насправді, із C ++ 11 вам, мабуть, найкраще використовувати map :: emplace, який уникає подвійної конструкції
einpoklum

@einpoklum: Власне, Скотт Майєрс пропонує в іншому розмові "Еволюціонуючий пошук ефективного C ++".
Томас Едінг

3
@einpoklum: Це так під час використання в новобудованій пам'яті. Але через деякі вимоги до стандартів для карти, є технічні причини, чому замінити місце можна повільніше, ніж вставити. Розмова вільно доступна на YouTube, наприклад, за цим посиланням youtube.com/watch?v=smqT9Io_bKo @ ~ 38-40 хв позначки. Для посилання SO, ось stackoverflow.com/questions/26446352 / ...
Томас Eding

1
Насправді я б посперечався з деяким із того, що представив Меєрс, але це виходить за межі цього потоку коментарів, і все одно, я думаю, я повинен відкликати свій попередній коментар.
einpoklum

Відповіді:


240

Коли пишеш

map[key] = value;

немає ніякого способу дізнатися , якщо ви замінили на valueFOR key, або якщо ви створили новий keyз value.

map::insert() створить лише:

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

Для більшості моїх програм, як правило, мені все одно, створюю чи замінюю, тому я користуюся легшим для читання map[key] = value.


16
Слід зазначити, що map :: insert ніколи не замінює значення. І в загальному випадку я б сказав, що краще використовувати, (res.first)->secondа не valueв другому випадку.
dalle

1
Я оновив, щоб зрозуміти, що map :: insert ніколи не замінює. Я залишив це, elseтому що вважаю, що використання valueзрозуміліше, ніж ітератор. Тільки якби тип значення мав незвичайний копіюючий копію або op ==, це було б інакше, і цей тип викликав би інші проблеми, використовуючи контейнери STL, як map.
netjeff

1
map.insert(std::make_pair(key,value))повинно бути map.insert(MyMap::value_type(key,value)). Тип, повернутий з якого make_pair, не відповідає типу, прийнятому, insertі поточне рішення вимагає конверсії
David Rodríguez - dribeas

1
є спосіб сказати, чи ви вставили чи просто призначені operator[], просто порівняйте розмір до і після. Можливість дзвонити map::operator[]лише для типів, які можна сконструювати за замовчуванням, набагато важливіше.
idclev 463035818

@ DavidRodríguez-dribeas: Гарна пропозиція, я оновив код у своїй відповіді.
неттьєф

53

Вони мають різну семантику, коли мова йде про ключ, який вже існує на карті. Тож вони насправді не порівнянні безпосередньо.

Але версія [] для оператора [] вимагає побудувати значення за замовчуванням, а потім призначити, тож якщо це дорожче, то скопіюйте конструкцію, тоді це буде дорожче. Іноді побудова за замовчуванням не має сенсу, і тоді неможливо використовувати версію operator [].


1
make_pair може зажадати конструктора копій - це було б гірше за типовий. +1 у будь-якому випадку

1
Головне, як ви сказали, що вони мають різну семантику. Тож ні краще, ніж інше, просто використовуйте той, що робить те, що вам потрібно.
джельф

Чому конструктор копій буде гіршим за конструктор за замовчуванням з подальшим призначенням? Якщо це так, то людина, яка написала клас, щось пропустила, тому що, що б оператор = робив, вони повинні були зробити те саме в конструкторі копій.
Стів Джессоп

1
Іноді побудова за замовчуванням така ж дорога, як і сама призначення. Природно призначення та копіювання конструкції будуть рівнозначними.
Грег Роджерс

@Arkadiy В оптимізованій збірці компілятор часто видаляє багато непотрібних викликів конструктора копій.
antred

35

Ще одна річ, яку слід зазначити std::map:

myMap[nonExistingKey];створить новий запис на карті, введений для nonExistingKeyініціалізації до значення за замовчуванням.

Це мене налякало пекло в перший раз, коли я його побачив (в той час як стукав головою об гнівний спадщину). Не очікував би цього. Для мене це виглядає як операція отримання, і я не очікував "побічного ефекту". Віддайте перевагу, map.find()коли ви отримуєте зі своєї карти.


3
Це гідне уявлення, хоча хеш-карти досить універсальні для цього формату. Це може бути одна з тих "дивацтв, які ніхто не вважає дивними" лише через те, наскільки широко вони використовують ті самі конвенції
Stephen J

19

Якщо хіт продуктивності конструктора за замовчуванням не є проблемою, будь ласка, для любові бога, перейдіть з більш читаною версією.

:)


5
По-друге! Треба відзначити це. Занадто багато людей торкаються тупості для наносекундних прискорень. Помилуй нас, бідні душі, які повинні підтримувати такі злодіяння!
Mr.Ree

6
Як писав Грег Роджерс: "Двоє мають різну семантику, коли мова йде про ключ, який вже існує на карті. Тому вони насправді не можуть бути безпосередньо порівнянні".
dalle

Це старе питання та відповідь. Але "більш читана версія" - це дурна причина. Бо те, що є найбільш читаним, залежить від людини.
валентін

14

insert краще з точки зору безпеки.

Вираз map[key] = value- це фактично дві операції:

  1. map[key] - створення елемента карти з значенням за замовчуванням.
  2. = value - копіювання значення в цей елемент.

Виняток може статися на другому кроці. В результаті операція буде виконана лише частково (новий елемент був доданий у карту, але цей елемент не був ініціалізований з value). Ситуація, коли операція не завершена, але стан системи модифікований, називається операцією з "побічним ефектом".

insertоперація дає надійну гарантію, тобто не має побічних ефектів ( https://en.wikipedia.org/wiki/Exception_safety ). insertабо повністю зроблено, або залишає карту в незміненому стані.

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

Якщо потрібно вставити один елемент, в контейнері не буде змін у разі виключення (суттєва гарантія).


1
що ще важливіше, вставка не вимагає, щоб значення було констатуваним за замовчуванням.
UmNyobe

13

Якщо у вашій програмі важлива швидкість, я порадитимусь з використанням оператора [], оскільки він створює 3 копії оригінального об'єкта, з яких 2 - тимчасові об'єкти і рано чи пізно знищуються як.

Але у вставці () створено 4 копії оригінального об'єкта, з яких 3 - тимчасові об'єкти (не обов'язково "тимчасові") та знищені.

Що означає додатковий час для: 1. Виділення однієї пам'яті об'єктів 2. Один додатковий виклик конструктора 3. Один додатковий виклик деструктора 4. Делокація пам'яті об'єктів

Якщо ваші об'єкти великі, конструктори типові, деструктори роблять багато звільнення від ресурсів, вище точки нараховуються ще більше. Щодо читабельності, я думаю, що обидва досить справедливі.

Це ж запитання мені прийшло в голову, але не через читабельність, а швидкість. Ось зразок коду, за допомогою якого я дізнався про згаданий мною пункт.

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

Вихід, коли використовується insert () Вихід, коли використовується оператор []


4
Тепер запустіть цей тест ще раз із увімкненою повною оптимізацією.
antred

2
Також розглянемо, що насправді робить оператор []. Він спочатку шукає на карті запис, який відповідає вказаному ключу. Якщо він знайде його, він замінить значення цього запису на вказане. Якщо цього немає, він вставляє новий запис із вказаним ключем та значенням. Чим більша ваша карта, тим довше буде потрібно оператору [] для пошуку карти. У якийсь момент це буде більше, ніж компенсувати додатковий виклик c'tor (якщо це навіть залишається в кінцевій програмі після того, як компілятор здійснив свою магію оптимізації).
antred

1
@antred, insertповинен виконувати той самий пошук, тому різниці в цьому немає [](тому що клавіші карти унікальні).
Sz.

Хороша ілюстрація того, що відбувається з роздруківками - але що ці 2 біти коду насправді роблять? Чому взагалі потрібні 3 копії оригінального об’єкта?
rbennett485

1. повинен реалізувати оператор присвоєння, інакше ви отримаєте неправильні числа в деструкторі 2. використовуйте std :: move під час створення пари, щоб уникнути надмірного конструювання копій "map.insert (std :: make_pair <int, Sample> (1, std: : переміщення (зразок))); "
Fl0

10

Зараз у c ++ 11 я думаю, що найкращий спосіб вставити пару в карту STL:

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

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

В результаті вийде пара з:

  • Перший елемент (result.first) вказує на вставлену пару або вказує на пару за допомогою цього ключа, якщо ключ вже існує.

  • Другий елемент (result.second), вірний, якщо вставлення було правильним чи хибним, щось пішло не так.

PS: Якщо у вас немає справи щодо замовлення, ви можете використовувати std :: unordered_map;)

Дякую!


9

Готча з map :: insert () полягає в тому, що вона не замінить значення, якщо ключ вже існує на карті. Я бачив код C ++, написаний програмістами Java, де вони очікували, що вставка () поводитиметься так само, як Map.put () в Java, де замінюються значення.


2

Одна примітка полягає в тому, що ви також можете використовувати 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

Ось ще один приклад, показує, що operator[] перезаписує значення для ключа, якщо він існує, але .insert не замінює значення, якщо воно існує.

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

Це досить обмежений випадок, але, судячи з отриманих коментарів, я думаю, що це варто відзначити.

Я бачив, як люди в минулому використовують карти у вигляді

map< const key, const val> Map;

щоб уникнути випадків перезапису випадкового значення, але потім продовжуйте писати деякі інші біти коду:

const_cast< T >Map[]=val;

Як я пам'ятаю, їх причиною цього було те, що вони були впевнені, що в цих певних бітах коду вони не будуть перезаписувати значення карт; отже, продовження більш "читабельного" методу [].

Я ніколи насправді не мав жодних прямих проблем з коду, який написали ці люди, але я сильно відчуваю до сьогодні, що ризики - хоч і невеликі - не слід сприймати, коли їх можна легко уникнути.

У випадках, коли ви маєте справу зі значеннями карти, які абсолютно не повинні бути перезаписані, використовуйте insert. Не робіть винятків лише для читабельності.


Це написали кілька різних людей? Обов`язково використовуйте insert(не input), тому що const_castволя призведе до перезапису будь-якого попереднього значення, що є дуже неконструктивним. Або не позначайте тип значення як const. (Такі речі зазвичай є кінцевим результатом const_cast, тому майже завжди червоний прапор вказує на помилку десь в іншому місці.)
Potatoswatter

@Potatoswatter Ти маєш рацію. Я просто бачу, що const_cast [] деякими людьми використовується зі значеннями const map, коли вони впевнені, що вони не замінять старе значення в певних бітах коду; оскільки [] сама читає. Як я вже згадував в останньому фрагменті своєї відповіді, я рекомендую використовувати insertв тих випадках, коли ви хочете запобігти перезаписуванню значень. (Просто змінив inputна insert- дякую)
dk123

@Potatoswatter Якщо я добре пам’ятаю, основні причини, якими люди, як видається, користуються const_cast<T>(map[key]): 1. [] є більш читабельним, 2. вони впевнені в певних бітах коду, вони не будуть перезаписувати значення, і 3. вони не хочуть, щоб інші біти невідомого коду замінювали свої значення - звідси і const value.
dk123

2
Я ніколи не чув, щоб це було зроблено; де ти це бачив? Написання, const_castздається, більше ніж заперечує додаткову "читабельність" [], і така впевненість майже є достатньою підставою для звільнення розробника. Складні умови виконання вирішуються куленепробивними конструкціями, а не почуттями кишок.
Potatoswatter

@Potatoswatter Я пам’ятаю, що це було під час однієї з моїх минулих робіт з розробки навчальних ігор. Я ніколи не міг змусити людей, які пишуть код, змінити свої звички. Ви абсолютно праві, і я дуже з вами згоден. З ваших коментарів я вирішив, що це, мабуть, більше варто відзначити, ніж моя первісна відповідь, і тому я оновив його, щоб відобразити це. Дякую!
dk123

1

Той факт, що insert()функція std :: map не замінює значення, пов’язане з ключем, дозволяє нам записати код перерахунку об'єкта таким чином:

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

Це досить поширена проблема, коли нам потрібно зіставити різні не унікальні об'єкти на деякі ідентифікатори в діапазоні 0..N. Ці ідентифікатори можуть бути згодом використані, наприклад, в алгоритмах графіків. На operator[]мій погляд, альтернатива з виглядала б менш читаною:

string word;
map<string, size_t> dict;
while(getline(cin, word)) {
    size_t sz = dict.size();
    if (!dict.count(word))
        dict[word] = sz; 
} 
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.