Какая разница между использованием структуры и std :: pair?

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
std::pair Это на структуру.
Caleth
3
@gnat: Общие вопросы, подобные этим, редко являются подходящими мишенями для двойных целей для конкретных вопросов, подобных этому, особенно если конкретного ответа на целевую мишень не существует (что в данном случае маловероятно).
Роберт Харви
18
@Caleth - std::pairэто шаблон . std::pair<string, bool>это структура.
Пит Беккер,
4
pairполностью лишен семантики. Никто, читающий ваш код (включая вас в будущем), не узнает, что e.firstэто имя чего-либо, если вы явно не укажете это. Я твердо верю в то, что это pairбыло очень плохое и ленивое дополнение std, и что, когда оно было задумано, никто не думал, «но однажды все будут использовать это для всего, что является двумя вещами, и никто не будет знать, что означает чей-либо код ».
Джейсон С
2
@ Снеговик О, определенно. Тем не менее, слишком плохо, что mapитераторы не являются допустимыми исключениями. ("first" = ключ и "second" = значение ... действительно, stdправда?)
Джейсон С

Ответы:

33

Вариант 1 подходит для небольших «используемых только один раз» вещей. По сути std::pairвсе еще структура. Как указано в этом комментарии, выбор 1 приведет к действительно ужасному коду где-то внизу кроличьей норы, thing.second->first.second->secondи никто не хочет его расшифровать.

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

risingDarkness
источник
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 для членов.
underscore_d
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 :: pair (или tuple) будет выглядеть примерно так:

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 :: pair, только «связаны» в контексте вызова функции, которая их возвращает. или в контексте структуры данных, которая связывает их вместе (например, std :: map использует std :: pair для своего типа хранения). Если отношения существуют где-то еще в вашем коде, вы должны использовать структуру.

Соответствующие разделы Основных руководящих принципов:

Дамиан Ярек
источник