Was ist der Unterschied zwischen einem struct und einem std :: pair?

26

Ich bin ein C ++ - Programmierer mit begrenzter Erfahrung.

Angenommen, ich möchte ein STL mapzum Speichern und Bearbeiten einiger Daten verwenden, dann würde ich gerne wissen, ob zwischen diesen beiden Datenstrukturansätzen ein bedeutender Unterschied (auch in Bezug auf die Leistung) besteht:

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

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

Gibt es einen Overhead, der eine structanstelle einer einfachen verwendet pair?

Marco Stramezzi
quelle
18
A std::pair ist eine Struktur.
Caleth
3
@gnat: Allgemeine Fragen wie diese sind selten für bestimmte Fragen wie diese geeignet, insbesondere wenn die spezifische Antwort auf dem Dupe-Ziel nicht vorhanden ist (was in diesem Fall unwahrscheinlich ist).
Robert Harvey
18
@Caleth - std::pairist eine Vorlage . std::pair<string, bool>ist eine Struktur.
Pete Becker
4
pairist völlig ohne Semantik. Niemand, der Ihren Code liest (auch Sie in Zukunft), wird wissen, dass dies e.firstder Name von etwas ist, es sei denn, Sie weisen ausdrücklich darauf hin. Ich bin fest davon überzeugt, dass dies paireine sehr schlechte und faule Ergänzung war std, und dass, als es konzipiert wurde, niemand dachte, "aber eines Tages wird jeder dies für alles verwenden , was zwei Dinge sind, und niemand wird wissen, was jemandes Code bedeutet ".
Jason C
2
@Schneemann Oh, auf jeden Fall. Trotzdem ist es schade, dass mapIteratoren keine gültigen Ausnahmen sind. ("erste" = Schlüssel und "zweite" = Wert ... wirklich std? Wirklich?)
Jason C

Antworten:

33

Wahl 1 ist in Ordnung für kleine "nur einmal verwendete" Dinge. Im Wesentlichen std::pairist immer noch eine Struktur. Wie aus diesem Kommentar hervorgeht, führt Auswahl 1 zu wirklich hässlichem Code irgendwo im Kaninchenbau, thing.second->first.second->secondund niemand möchte das wirklich entschlüsseln.

Wahl 2 ist besser für alles andere, weil es einfacher ist, die Bedeutung der Dinge auf der Karte zu erkennen. Es ist auch flexibler, wenn Sie die Daten ändern möchten (zum Beispiel, wenn Ente plötzlich ein anderes Flag benötigt). Leistung sollte hier kein Thema sein.

steigende Dunkelheit
quelle
15

Leistung :

Es hängt davon ab, ob.

In Ihrem speziellen Fall wird es keinen Leistungsunterschied geben, da die beiden in ähnlicher Weise im Speicher angeordnet werden.

In einem ganz bestimmten Fall (wenn Sie eine leere Struktur als eines der Datenelemente verwenden) kann die std::pair<>EBO-Funktion (Empty Base Optimization) möglicherweise verwendet werden und eine geringere Größe als die entsprechende Struktur aufweisen. Und eine geringere Größe bedeutet im Allgemeinen eine höhere Leistung:

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

Drucke: 32, 32, 40, 40 auf Ideone .

Hinweis: Mir ist keine Implementierung bekannt, die den EBO-Trick für reguläre Paare verwendet, er wird jedoch im Allgemeinen für Tupel verwendet.


Lesbarkeit :

Abgesehen von Mikrooptimierungen ist eine benannte Struktur jedoch ergonomischer.

Ich meine, map[k].firstist zwar nicht so schlimm, get<0>(map[k])ist aber kaum verständlich. Ein Kontrast, map[k].nameder sofort anzeigt, woraus wir lesen.

Umso wichtiger ist es, wenn die Typen untereinander konvertierbar sind, da ein versehentliches Vertauschen zu einem echten Problem wird.

Vielleicht möchten Sie auch mehr über strukturelle oder nominelle Typisierung lesen. Enteist ein spezifischer Typ, der nur von Dingen bearbeitet Entewerden kann, die es erwarten , und alles, was es bearbeitet, std::pair<std::string, bool>kann es bearbeiten ... selbst wenn das std::stringoder boolnicht das enthält, was es erwartet, weil std::pairkeine Semantik damit verbunden ist.


Wartung :

In Bezug auf die Wartung pairist das schlimmste. Sie können kein Feld hinzufügen.

tupleDiesbezüglich ist es besser, wenn Sie das neue Feld anhängen und auf alle vorhandenen Felder mit demselben Index zugegriffen wird. Das ist so unergründlich wie zuvor, aber zumindest müssen Sie sie nicht aktualisieren.

structist der klare Gewinner. Sie können Felder hinzufügen, wo immer Sie möchten.


Abschließend:

  • pair ist das Schlimmste von beiden Welten,
  • tuple kann in einem ganz bestimmten Fall eine leichte Kante haben (leerer Typ),
  • verwendenstruct .

Hinweis: Wenn Sie Getter verwenden, können Sie den Trick mit der leeren Basis selbst anwenden, ohne dass die Clients darüber Bescheid wissen müssen struct Thing: Empty { std::string name; }. Aus diesem Grund ist Encapsulation das nächste Thema, mit dem Sie sich befassen sollten.

Matthieu M.
quelle
3
Sie können EBO nicht für Paare verwenden, wenn Sie dem Standard folgen. Elemente eines Paares sind in Mitgliedern gespeichert, firstund secondes gibt keinen Platz für die Optimierung der leeren Basis, um einzutreten.
Revolver_Ocelot
2
@Revolver_Ocelot: Nun, Sie können kein C ++ schreibenpair , das EBO verwenden würde, aber ein Compiler könnte ein eingebautes bereitstellen. Da diese jedoch Mitglieder sein sollen, kann es beobachtbar sein (zum Beispiel ihre Adressen überprüfen), in welchem ​​Fall es nicht konform wäre.
Matthieu M.
1
C ++ 20 fügt hinzu [[no_unique_address]], wodurch das Äquivalent von EBO für Mitglieder aktiviert wird.
Underscore_d
3

Pair scheint am besten, wenn es als Rückgabetyp einer Funktion zusammen mit einer destrukturierten Zuweisung unter Verwendung von std :: tie und der strukturierten Bindung von C ++ 17 verwendet wird. Mit 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
}

Verwenden der strukturierten Bindung von C ++ 17:

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

Ein schlechtes Beispiel für die Verwendung eines std :: pair (oder Tupels) wäre etwa so:

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

weil nicht klar ist , wenn std :: get <2> (player_data) aufrufen , was an der Position Index gespeichert 2. Lesbarkeit Denken Sie daran , und es offensichtlich für den Leser zu machen , was der Code tut wichtig . Beachten Sie, dass dies viel besser lesbar ist:

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

Im Allgemeinen sollten Sie über std :: pair und std :: tuple nachdenken, um mehr als ein Objekt von einer Funktion zurückzugeben. Die Faustregel, die ich verwende (und die auch von vielen anderen verwendet wurde), lautet, dass Objekte, die in einem std :: tuple oder std :: pair zurückgegeben werden, nur im Kontext eines Aufrufs einer Funktion "verwandt" sind, die sie zurückgibt oder im Kontext einer Datenstruktur, die sie miteinander verbindet (z. B. verwendet std :: map std :: pair als Speichertyp). Wenn die Beziehung an anderer Stelle in Ihrem Code vorhanden ist, sollten Sie eine Struktur verwenden.

Verwandte Abschnitte der Kernrichtlinien:

Damian Jarek
quelle