Che differenza c'è tra usare una struct e una std :: pair?


26

Sono un programmatore C ++ con esperienza limitata.

Supponendo di voler utilizzare un STL mapper archiviare e manipolare alcuni dati, vorrei sapere se c'è qualche differenza significativa (anche nelle prestazioni) tra questi 2 approcci alla struttura dei dati:

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

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

In particolare, c'è un sovraccarico che utilizza un structanziché un semplice pair?


18
A std::pair è una struttura.
Caleth,

3
@gnat: domande generali come questa raramente sono target dupe adatti a domande specifiche come questa, specialmente se la risposta specifica non esiste sul target dupe (che è improbabile in questo caso).
Robert Harvey,

18
@Caleth - std::pairè un modello . std::pair<string, bool>è una struttura.
Pete Becker,

4
pairè completamente privo di semantica. Nessuno leggerà il tuo codice (incluso te in futuro) saprà che e.firstè il nome di qualcosa a meno che non lo indichi esplicitamente. Sono fermamente convinto che sia pairstata un'aggiunta molto povera e pigra std, e che quando è stato concepito nessuno ha pensato "ma un giorno, tutti lo useranno per tutto ciò che è due cose, e nessuno saprà cosa significa il codice di qualcuno ".
Jason C

2
@Snowman Oh, sicuramente. Tuttavia, è un mappeccato che gli iteratori non siano eccezioni valide. ("primo" = chiave e "secondo" = valore ... davvero std,? Davvero?)
Jason C

Risposte:


33

La scelta 1 va bene per le piccole cose "usate solo una volta". Essenzialmente std::pairè ancora una struttura. Come affermato da questo commento, la scelta 1 porterà a un codice davvero brutto da qualche parte nella tana del coniglio thing.second->first.second->seconde nessuno vuole davvero decifrarlo.

La scelta 2 è migliore per tutto il resto, perché è più facile leggere quale sia il significato delle cose nella mappa. È anche più flessibile se si desidera modificare i dati (ad esempio quando Ente ha bisogno improvvisamente di un'altra bandiera). Le prestazioni non dovrebbero essere un problema qui.


15

Prestazioni :

Dipende.

Nel tuo caso particolare non ci saranno differenze di prestazioni perché i due saranno similmente disposti in memoria.

In un caso molto specifico (se si utilizzava una struttura vuota come uno dei membri dei dati), si std::pair<>potrebbe potenzialmente utilizzare l'ottimizzazione della base vuota (EBO) e avere una dimensione inferiore rispetto all'equivalente della struttura. E dimensioni inferiori generalmente significano prestazioni più elevate:

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

Stampe: 32, 32, 40, 40 su ideone .

Nota: non sono a conoscenza di alcuna implementazione che utilizza effettivamente il trucco EBO per le coppie regolari, tuttavia viene generalmente utilizzato per le tuple.


Leggibilità :

A parte le micro-ottimizzazioni, tuttavia, una struttura denominata è più ergonomica.

Voglio dire, map[k].firstnon è poi così male mentre get<0>(map[k])è appena comprensibile. Contrasto con il map[k].namequale indica immediatamente da cosa stiamo leggendo.

È tanto più importante quando i tipi sono convertibili l'uno nell'altro, poiché scambiarli inavvertitamente diventa una vera preoccupazione.

Potresti anche leggere informazioni sulla digitazione strutturale e nominale. Enteè un tipo specifico che può essere operato solo dalle cose che si aspettano Ente, tutto ciò che può operare std::pair<std::string, bool>può operare su di esse ... anche quando la std::stringo boolnon contiene ciò che si aspettano, perché std::pairnon ha alcuna semantica associata.


Manutenzione :

In termini di manutenzione, pairè il peggiore. Non è possibile aggiungere un campo.

tuplefiere migliori a tale riguardo, fintanto che si aggiunge il nuovo campo a tutti i campi esistenti si accede ancora dallo stesso indice. Il che è imperscrutabile come prima ma almeno non è necessario andare ad aggiornarli.

structè il chiaro vincitore. Puoi aggiungere campi ovunque tu ne abbia voglia.


In conclusione:

  • pair è il peggiore di entrambi i mondi,
  • tuple potrebbe avere un leggero vantaggio in un caso molto specifico (tipo vuoto),
  • usarestruct .

Nota: se usi getter, puoi usare tu stesso il trucco base vuoto senza che i client debbano saperlo come in struct Thing: Empty { std::string name; }; ecco perché l' incapsulamento è il prossimo argomento di cui dovresti occuparti.


3
Non è possibile utilizzare EBO per le coppie, se si segue lo standard. Gli elementi della coppia sono memorizzati nei membri first e secondnon c'è spazio per l' ottimizzazione della base vuota .
Revolver_Ocelot

2
@Revolver_Ocelot: Beh, non puoi scrivere un C ++ pairche userebbe EBO, ma un compilatore potrebbe fornire un built-in. Dal momento che si suppone che siano membri, tuttavia, potrebbe essere osservabile (controllando i loro indirizzi, ad esempio) nel qual caso non sarebbe conforme.
Matthieu M.

1
Aggiunge C ++ 20 [[no_unique_address]], che consente l'equivalente di EBO per i membri.
underscore_d

3

La coppia brilla di più quando viene usata come tipo di ritorno di una funzione insieme all'assegnazione destrutturata usando std :: tie e l'associazione strutturata di C ++ 17. Utilizzando 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
}

Utilizzando l'associazione strutturata di 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 cattivo esempio di utilizzo di uno std :: pair (o tupla) sarebbe qualcosa del genere:

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

perché non è chiaro quando si chiama std :: get <2> (player_data) cosa è memorizzato nell'indice di posizione 2. Ricordare la leggibilità e rendere ovvio per il lettore ciò che il codice sta facendo è importante . Considera che questo è molto più leggibile:

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

In generale, dovresti pensare a std :: pair e std :: tuple come modi per restituire più di 1 oggetto da una funzione. La regola empirica che uso (e ne ho visti anche molti altri) è che gli oggetti restituiti in una coppia std :: tuple o std :: sono solo "correlati" nel contesto di effettuare una chiamata a una funzione che li restituisce o nel contesto della struttura di dati che li collega insieme (ad es. std :: map usa std :: pair per il suo tipo di archiviazione). Se la relazione esiste altrove nel codice, è necessario utilizzare una struttura.

Sezioni correlate delle Linee guida di base:

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.