Che differenza c'è tra l'uso di una struct e una std :: pair?

21

Sono un programmatore C ++ con esperienza limitata.

Supponendo di voler usare STL map per archiviare e manipolare alcuni dati, vorrei sapere se c'è qualche differenza significativa (anche nelle prestazioni) tra questi 2 approcci alla struttura 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 struct invece di un semplice pair ?

    
posta Marco Stramezzi 24.03.2017 - 12:18
fonte

3 risposte

32

La scelta 1 è ok per le cose piccole "usate solo una volta". In sostanza std::pair è ancora una struttura. Come indicato da questo commento la scelta 1 porterà a qualcosa di veramente brutto codice da qualche parte nella buca del coniglio come thing.second->first.second->second e 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 necessita improvvisamente di un altro flag). Le prestazioni non dovrebbero essere un problema qui.

    
risposta data 24.03.2017 - 12:43
fonte
14

Performance :

Dipende.

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

In un caso molto specifico (se stavi usando una struct vuota come uno dei membri dati) allora std::pair<> potrebbe potenzialmente utilizzare Empty Base Optimization (EBO) e avere un valore inferiore dimensione dell'equivalente di struct. E le 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 coppie regolari, tuttavia è generalmente utilizzata per le tuple.

La leggibilità :

Oltre alle micro-ottimizzazioni, tuttavia, una struttura con nome è più ergonomica.

Voglio dire, map[k].first non è poi così male mentre get<0>(map[k]) è a malapena comprensibile. Contrasto con map[k].name che indica immediatamente ciò che stiamo leggendo.

È tanto più importante quando i tipi sono convertibili l'uno con l'altro, dal momento che scambiarli inavvertitamente diventa una vera preoccupazione.

Si potrebbe anche voler leggere su Structural vs Nominal Typing. Ente è un tipo specifico che può essere utilizzato solo da cose che si aspettano Ente , tutto ciò che può operare su std::pair<std::string, bool> può operare su di essi ... anche quando std::string o bool non contiene cosa si aspettano, perché std::pair non ha una semantica associata ad essa.

Manutenzione :

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

tuple è migliore in questo senso, a patto che tu aggiunga al nuovo campo tutti i campi esistenti siano ancora accessibili dallo stesso indice. Che è imperscrutabile come prima ma almeno non è necessario andare e aggiornarli.

struct è il chiaro vincitore. Puoi aggiungere campi ovunque ti sembrino.

In conclusione:

  • pair è il peggiore di entrambi i mondi,
  • tuple può avere un leggero margine in un caso molto specifico (tipo vuoto),
  • utilizza struct .

Nota: se usi i getter, puoi usare il trucco base vuoto da solo senza che i client debbano sapere come in struct Thing: Empty { std::string name; } ; che è il motivo per cui Incapsulamento è l'argomento successivo di cui dovresti preoccuparti.

    
risposta data 24.03.2017 - 16:17
fonte
3
La coppia

brilla di più quando usata come tipo di ritorno di una funzione insieme all'assegnazione destrutturata usando il legame strutturato std :: tie e 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
}

Uso dell'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 una std :: pair (o tupla) sarebbe qualcosa del tipo:

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 viene memorizzato nell'indice di posizione 2. Ricorda la leggibilità e rendendo ovvio per il lettore che cosa sta facendo il codice è 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 molti altri) è che gli oggetti restituiti in una std :: tuple o std :: pair sono solo "correlati" nel contesto di effettuare una chiamata a una funzione che li restituisce o nel contesto della struttura dati che li collega insieme (ad esempio std :: map usa std :: pair per il suo tipo di archiviazione). Se la relazione esiste altrove nel tuo codice dovresti usare una struct.

Sezioni correlate delle linee guida principali:

risposta data 25.03.2017 - 01:01
fonte

Leggi altre domande sui tag