web-dev-qa-db-fra.com

Quelle différence y a-t-il entre l'utilisation d'une structure et d'une paire std ::?

Je suis un programmeur C++ avec une expérience limitée.

Supposons que je veuille utiliser un STL map pour stocker et manipuler certaines données, je voudrais savoir s'il existe une différence significative (également en termes de performances) entre ces 2 approches de structure de données:

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

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

Plus précisément, y a-t-il des frais généraux en utilisant un struct au lieu d'un simple pair?

26
Marco Stramezzi

Le choix 1 est correct pour les petites choses "utilisées une seule fois". Essentiellement std::pair est toujours une structure. Comme indiqué par ce commentaire le choix 1 conduira à un code vraiment moche quelque part dans le trou du lapin comme thing.second->first.second->second et personne ne veut vraiment déchiffrer cela.

Le choix 2 est meilleur pour tout le reste, car il est plus facile de lire la signification des éléments de la carte. Il est également plus flexible si vous souhaitez modifier les données (par exemple, lorsque Ente a soudainement besoin d'un autre indicateur). Les performances ne devraient pas être un problème ici.

33
risingDarkness

Performances :

Ça dépend.

Dans votre cas particulier, il n'y aura pas de différence de performances car les deux seront également disposés en mémoire.

Dans un cas très spécifique (si vous utilisiez un structure vide comme l'un des membres de données), le std::pair<> Pourrait potentiellement utiliser l'optimisation de base vide (EBO) et avoir une taille inférieure à l'équivalent struct. Et une taille plus petite signifie généralement des performances plus élevées:

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

Impressions: 32, 32, 40, 40 sur ideone .

Remarque: je ne connais aucune implémentation qui utilise réellement l'astuce EBO pour les paires régulières, mais elle est généralement utilisée pour les tuples.


Lisibilité :

Hormis les micro-optimisations, cependant, une structure nommée est plus ergonomique.

Je veux dire, map[k].first N'est pas si mal alors que get<0>(map[k]) est à peine intelligible. Comparez avec map[k].name Qui indique immédiatement ce que nous lisons.

C'est d'autant plus important lorsque les types sont convertibles entre eux, car les échanger par inadvertance devient une véritable préoccupation.

Vous voudrez peut-être également en savoir plus sur le typage structurel vs nominal. Ente est un type spécifique qui ne peut être opéré que par des choses qui attendent Ente, tout ce qui peut fonctionner sur std::pair<std::string, bool> peut fonctionner sur eux ... même lorsque le std::string Ou bool ne contient pas ce qu'ils attendent, car std::pair N'a pas de sémantique associée.


Maintenance :

En termes de maintenance, pair est le pire. Vous ne pouvez pas ajouter de champ.

Tuple convient mieux à cet égard, tant que vous ajoutez le nouveau champ, tous les champs existants sont toujours accessibles par le même index. Ce qui est aussi impénétrable qu'auparavant, mais au moins vous n'avez pas besoin d'aller les mettre à jour.

struct est clairement le gagnant. Vous pouvez ajouter des champs où vous en avez envie.


En conclusion:

  • pair est le pire des deux mondes,
  • Tuple peut avoir un léger Edge dans un cas très spécifique (type vide),
  • utilisez struct.

Remarque: si vous utilisez des getters, vous pouvez utiliser vous-même l'astuce de base vide sans que les clients n'aient à le savoir comme dans struct Thing: Empty { std::string name; }; c'est pourquoi L'encapsulation est le prochain sujet dont vous devriez vous préoccuper.

15
Matthieu M.

La paire brille le plus lorsqu'elle est utilisée comme type de retour d'une fonction avec une affectation déstructurée à l'aide de std :: tie et de la liaison structurée de C++ 17. Utilisation de 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
}

Utilisation de la liaison structurée de C++ 17:

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

Un mauvais exemple d'utilisation d'une paire std :: (ou Tuple) serait quelque chose comme ceci:

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

car il n'est pas clair lors de l'appel de std :: get <2> (player_data) ce qui est stocké à l'index de position 2. Souvenez-vous de la lisibilité et de rendre évident pour le lecteur ce que fait le code est important . Considérez que ceci est beaucoup plus lisible:

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

En général, vous devriez penser à std :: pair et std :: Tuple comme des moyens de renvoyer plus d'un objet à partir d'une fonction. La règle de base que j'utilise (et que beaucoup d'autres ont également utilisée) est que les objets retournés dans une paire std :: Tuple ou std :: ne sont "liés" que dans le contexte d'un appel à une fonction qui les renvoie ou dans le contexte d'une structure de données qui les relie (par exemple, std :: map utilise std :: pair pour son type de stockage). Si la relation existe ailleurs dans votre code, vous devez utiliser une structure.

Sections connexes des Principes directeurs:

3
Damian Jarek