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

26

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

En supposant que je souhaite utiliser un STL mappour 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 utilisant un structau lieu d'un simple pair?

Marco Stramezzi
la source
18
A std::pair est une structure.
Caleth
3
@gnat: Des questions générales comme celles-ci sont rarement des cibles de dupe appropriées pour des questions spécifiques comme celle-ci, surtout si la réponse spécifique n'existe pas sur la cible de dupe (ce qui est peu probable dans ce cas).
Robert Harvey
18
@Caleth - std::pairest un modèle . std::pair<string, bool>est une struct.
Pete Becker
4
pairest entièrement dépourvu de sémantique. Personne ne lisant votre code (y compris vous à l'avenir) ne saura que e.firstc'est le nom de quelque chose à moins que vous ne le signaliez explicitement. Je suis fermement convaincu que pairc'était un ajout très pauvre et paresseux std, et que quand il a été conçu, personne ne pensait "mais un jour, tout le monde va l'utiliser pour tout ce qui est deux choses, et personne ne saura ce que signifie le code de quiconque ".
Jason C
2
@Snowman Oh, certainement. Pourtant, c'est dommage que les mapitérateurs ne soient pas des exceptions valides. ("first" = clé et "second" = valeur ... vraiment, stdvraiment?)
Jason C

Réponses:

33

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

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.

montanteObscurité
la source
15

Performance :

Ç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 une structure vide comme l'un des membres de données), alors le std::pair<>pourrait potentiellement utiliser l'optimisation de base vide (EBO) et avoir une taille inférieure à l'équivalent de la structure. 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 idéone .

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].firstn'est pas si mal alors qu'il get<0>(map[k])est à peine intelligible. Contraste avec map[k].namelequel 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 réelle préoccupation.

Vous voudrez peut-être également en savoir plus sur le typage structurel vs nominal. Enteest un type spécifique qui ne peut être opéré que par des choses qui s’attendent Ente, tout ce qui peut opérer std::pair<std::string, bool>peut les opérer ... même lorsque le std::stringou boolne contient pas ce qu’il attend, car il std::pairn’a pas de sémantique associée.


Entretien :

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

tupleconvient 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.

structest 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 avantage dans un cas très spécifique (type vide),
  • utiliserstruct .

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 que vous devriez vous préoccuper.

Matthieu M.
la source
3
Vous ne pouvez pas utiliser EBO pour les paires, si vous suivez la norme. Les éléments de la paire sont stockés dans les membres first et second, il n'y a pas de place pour l' optimisation de la base vide .
Revolver_Ocelot
2
@Revolver_Ocelot: Eh bien, vous ne pouvez pas écrire un C ++ pairqui utiliserait EBO, mais un compilateur pourrait fournir une fonction intégrée. Étant donné que ceux-ci sont censés être membres, cependant, il peut être observable (vérification de leurs adresses, par exemple) auquel cas ce ne serait pas conforme.
Matthieu M.
1
C ++ 20 ajoute [[no_unique_address]], ce qui active l'équivalent d'EBO pour les membres.
underscore_d
3

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:

Damian Jarek
la source