Яка різниця між використанням struktur та std :: пари?


26

Я програміст на C ++ з обмеженим досвідом.

Припустимо, що я хочу використовувати STL mapдля зберігання та маніпулювання деякими даними, я хотів би знати, чи є якась суттєва різниця (також у продуктивності) між цими двома підходами до структури даних:

Choice 1:
    map<int, pair<string, bool> >

Choice 2:
    struct Ente {
        string name;
        bool flag;
    }
    map<int, Ente>

Зокрема, чи є накладні витрати structзамість простого pair?


18
A std::pair - структура.
Калет

3
@gnat: Загальні подібні запитання рідко підходять для мішень для конкретних питань, таких як цей, особливо якщо конкретна відповідь не існує на цілі дупи (що в цьому випадку малоймовірно).
Роберт Харві

18
@Caleth - std::pairце шаблон . std::pair<string, bool>є структура.
Піт Бекер

4
pairповністю позбавлений семантики. Ніхто не читає ваш код (включаючи вас у майбутньому) не дізнається, що e.firstце ім’я чогось, якщо ви прямо не вказали на нього. Я твердо вірю в те, що це pairбуло дуже бідним і ледачим доповненням std, і що, коли це було задумано, ніхто не думав, "але одного дня всі будуть використовувати це для всього, що є двома речами, і ніхто не буде знати, що чийсь код означає" ".
Джейсон С

2
@Snowman О, безумовно. Тим не менш, це дуже погані речі, такі як mapітератори - не допустимі винятки. ("перший" = ключ і "другий" = значення ... дійсно, stdсправді?)
Jason C

Відповіді:


33

Для невеликих речей, які "використовуються лише один раз", вибір 1 нормальний. По суті std::pairце все-таки структура. Як зазначено в цьому коментарі, вибір 1 призведе до дійсно некрасивого коду десь у кролячій норі, thing.second->first.second->secondі ніхто не хоче цього розшифровувати.

Вибір 2 краще для всього іншого, тому що легше читати, яке значення мають речі на карті. Це також більш гнучко, якщо ви хочете змінити дані (наприклад, коли Ente раптом потрібен інший прапор). Продуктивність не повинна бути проблемою тут.


15

Продуктивність :

Це залежить.

У вашому конкретному випадку різниці в продуктивності не буде, оскільки вони будуть однаково закладені в пам'яті.

У дуже конкретному випадку (якщо ви використовували порожню структуру як один із членів даних), std::pair<>потенційно потенційно можна використовувати оптимізацію порожньої бази (EBO) та мати менший розмір, ніж структурний еквівалент. А менший розмір, як правило, означає більш високу продуктивність:

struct Empty {};
struct Thing { std::string name; Empty e; };

int main() {
    std::cout << sizeof(std::string) << "\n";
    std::cout << sizeof(std::tuple<std::string, Empty>) << "\n";
    std::cout << sizeof(std::pair<std::string, Empty>) << "\n";
    std::cout << sizeof(Thing) << "\n";
}

Принти: 32, 32, 40, 40 на ideone .

Примітка. Мені невідома жодна реалізація, яка фактично використовує трюк EBO для звичайних пар, проте, як правило, він використовується для кортежів.


Читання :

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

Я маю на увазі, map[k].firstчи не так вже й погано, але get<0>(map[k])ледве можна зрозуміти. Контраст, з map[k].nameяким одразу вказується те, з чого ми читаємо.

Тим важливіше, коли типи можуть бути конвертовані один в одного, оскільки їх обміняння ненавмисно стає справжнім клопотом.

Ви також можете прочитати про "Структурне проти номінального набору тексту". Enteце специфічний тип, яким можуть керувати лише ті речі, які очікують Ente, все, що може діяти, std::pair<std::string, bool>може діяти на них ... навіть коли те, що вони очікують, std::stringабо boolне містить його, оскільки std::pairне має з ним пов'язаної семантики .


Технічне обслуговування :

Щодо технічного обслуговування, pairце найгірше. Ви не можете додати поле.

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

struct- явний переможець. Ви можете додавати поля де завгодно.


На закінчення:

  • pair є найгіршим з обох світів,
  • tuple може мати незначний край у дуже конкретному випадку (порожній тип),
  • використанняstruct .

Примітка: якщо ви користуєтеся геттерами, ви можете самостійно використовувати порожню базову хитрість, без того, щоб клієнти знали про це як у struct Thing: Empty { std::string name; }; саме тому інкапсуляція - це наступна тема, якою слід займатися.


3
Ви не можете використовувати EBO для пар, якщо ви дотримуєтесь стандарту. Елементи пари зберігаються в членах, first і secondтам немає місця для пустої оптимізації бази .
Revolver_Ocelot

2
@Revolver_Ocelot: Ну, ви не можете написати C ++, pairякий би використовував EBO, але компілятор міг би забезпечити вбудований. Однак, оскільки вони повинні бути членами, це може бути помітним (наприклад, перевіряючи їх адреси), і в цьому випадку це не відповідає.
Маттьє М.

1
C ++ 20 додає [[no_unique_address]], що дозволяє еквівалент EBO для членів.
підкреслюй_

3

Пара найбільше світить, коли використовується як тип повернення функції разом із деструктурованим призначенням, використовуючи std :: tie та C ++ 17-и структуровані прив’язки. Використання std :: tie:

struct Ente {/*...*/};
std::map<int, Ente> map;
auto inserted_position = map.end();
auto was_inserted = false;
std::tie(inserted_position, was_inserted) = map.emplace(1, Ente{});
if (!was_inserted) {
    //handle insertion error
}

Використання структурованого зв'язування C ++ 17:

struct Ente {/*...*/};
std::map<int, Ente> map;
auto [inserted_position, was_inserted] = map.emplace(1, Ente{});
if (!was_inserted) {
    //handle insertion error
}

Поганий приклад використання std :: пари (або кортежу) може бути приблизно таким:

using player_data = std::tuple<std::string, uint64_t, double>;
player_data player{};
/* ... */
auto health = std::get<2>(player);
/* ... */

тому що під час виклику std :: get <2> (player_data) незрозуміло, що зберігається в індексі позиції 2. Запам'ятайте читабельність і зробіть зрозумілим для читача, що робить код важливим . Вважайте, що це набагато читабельніше:

struct player_data
{
    std::string name;
    uint64_t player_id;
    double current_health;
};
player_data player{};
/* ... */
auto health = player.current_health;
/* ... */

Взагалі слід подумати про std :: pair та std :: tuple як про способи повернути більше ніж 1 об’єкт з функції. Основне правило, яке я використовую (і я також бачив багато інших), - це те, що об'єкти, повернуті в парі std :: tuple або std ::, є лише "пов'язаними" в контексті здійснення виклику функції, яка повертає їх або в контексті структури даних, яка пов'язує їх між собою (наприклад, std :: map використовує std :: пара для свого типу зберігання). Якщо взаємозв'язок існує в іншому місці коду, слід використовувати структуру.

Пов'язані розділи Основних рекомендацій:

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.