Jaka jest różnica między użyciem struct i std :: pair?

26

Jestem programistą C ++ z ograniczonym doświadczeniem.

Przypuśćmy, że chcę użyć STL mapdo przechowywania i manipulowania niektórymi danymi, chciałbym wiedzieć, czy istnieje jakaś znacząca różnica (także w wydajności) między tymi dwoma podejściami do struktury danych:

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

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

W szczególności, czy jest jakiś narzut przy użyciu structzamiast zwykłego pair?

Marco Stramezzi
źródło
18
A std::pair jest strukturą.
Caleth
3
@gnat: Ogólne pytania tego typu rzadko są odpowiednimi celami duplikatów dla konkretnych pytań takich jak ten, szczególnie jeśli konkretna odpowiedź nie istnieje w celu duplikacji (co w tym przypadku jest mało prawdopodobne).
Robert Harvey
18
@Caleth - std::pairto szablon . std::pair<string, bool>jest strukturą.
Pete Becker
4
pairjest całkowicie pozbawiony semantyki. Nikt nie czyta twojego kodu (w tym ciebie w przyszłości) nie będzie wiedział, że e.firstjest to nazwa czegoś, chyba że wyraźnie to zaznaczysz. Jestem głęboko przekonany, że pairbył to bardzo biedny i leniwy dodatek do tego std, i kiedy to zostało wymyślone, nikt nie pomyślał „ale pewnego dnia wszyscy wykorzystają to do wszystkiego , co jest dwiema rzeczami, i nikt nie będzie wiedział, co oznacza kod każdego „.
Jason C
2
@ Snowman Och, zdecydowanie. Mimo to szkoda, że mapiteratory nie są ważnymi wyjątkami. („pierwszy” = klucz i „drugi” = wartość ... naprawdę std? Naprawdę?)
Jason C

Odpowiedzi:

33

Wybór 1 jest odpowiedni dla małych „używanych tylko raz” rzeczy. Zasadniczo std::pairjest nadal strukturą. Jak stwierdzono w tym komentarzu, wybór 1 doprowadzi do naprawdę brzydkiego kodu gdzieś w dole króliczej nory, thing.second->first.second->secondi nikt tak naprawdę nie chce tego rozszyfrować.

Wybór 2 jest lepszy do wszystkiego innego, ponieważ łatwiej jest odczytać, jakie są znaczenie rzeczy na mapie. Jest również bardziej elastyczny, jeśli chcesz zmienić dane (na przykład, gdy Ente nagle potrzebuje innej flagi). Wydajność nie powinna być tutaj problemem.

rośnie Ciemność
źródło
15

Wydajność :

To zależy.

W twoim konkretnym przypadku nie będzie różnicy w wydajności, ponieważ oba będą podobnie ułożone w pamięci.

W bardzo szczególnym przypadku (jeśli używałeś pustej struktury jako jednego z elementów danych), wówczas std::pair<>potencjalnie mogliby skorzystać z pustej optymalizacji bazy (EBO) i mieć mniejszy rozmiar niż odpowiednik struktury. A mniejszy rozmiar ogólnie oznacza wyższą wydajność:

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

Drukuje: 32, 32, 40, 40 na ideone .

Uwaga: nie znam żadnej implementacji, która faktycznie używałaby sztuczki EBO dla zwykłych par, jednak ogólnie jest ona stosowana do krotek.


Czytelność :

Jednak oprócz mikrooptymalizacji nazwana struktura jest bardziej ergonomiczna.

Mam na myśli, map[k].firstże nie jest tak źle, podczas gdy get<0>(map[k])jest ledwo zrozumiałe. Kontrast z map[k].namektórym natychmiast wskazuje, z czego czytamy.

Jest to tym bardziej ważne, gdy typy są konwertowalne na siebie, ponieważ ich przypadkowa wymiana staje się prawdziwym problemem.

Możesz także przeczytać o typowaniu strukturalnym a typowym. EnteJest to typ specyficzny, który może być obsługiwany tylko przez rzeczy, które oczekują Ente, że wszystko może działać na std::pair<std::string, bool>może działać na nich ... nawet gdy std::stringlub boolnie zawiera tego, co się spodziewać, ponieważ std::pairnie ma semantykę z nim związane.


Konserwacja :

Pod względem konserwacji pairjest najgorszy. Nie możesz dodać pola.

tupletargi są lepsze pod tym względem, o ile dodajesz nowe pole, wszystkie istniejące pola są nadal dostępne dla tego samego indeksu. Co jest tak nieprzeniknione jak wcześniej, ale przynajmniej nie musisz iść i je aktualizować.

structjest wyraźnym zwycięzcą. Możesz dodawać pola w dowolnym miejscu.


Podsumowując:

  • pair jest najgorszy z obu światów,
  • tuple może mieć niewielką krawędź w bardzo szczególnym przypadku (pusty typ),
  • użyćstruct .

Uwaga: jeśli używasz programów pobierających, możesz samodzielnie skorzystać z pustej sztuczki bazowej bez potrzeby, aby klienci wiedzieli o tym jak w struct Thing: Empty { std::string name; }; dlatego Encapsulation to kolejny temat, którym powinieneś się zająć.

Matthieu M.
źródło
3
Nie możesz używać EBO dla par, jeśli postępujesz zgodnie ze standardem. Elementy pary są przechowywane w elementach first i secondnie ma miejsca na rozpoczęcie Optymalizacji Pustej Bazy .
Revolver_Ocelot
2
@Revolver_Ocelot: Cóż, nie możesz napisać C ++, pairktóry używałby EBO, ale kompilator może zapewnić wbudowane. Ponieważ mają to być członkowie, może być jednak obserwowalne (na przykład sprawdzanie ich adresów), w którym to przypadku byłoby niezgodne.
Matthieu M.
1
C ++ 20 dodaje [[no_unique_address]], co umożliwia ekwiwalent EBO dla członków.
underscore_d
3

Para świeci najbardziej, gdy jest używana jako typ zwracany funkcji wraz ze zniszczonym przypisaniem przy użyciu std :: tie i strukturalnego wiązania C ++ 17. Używając 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
}

Korzystanie z powiązania strukturalnego C ++ 17:

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

Zły przykład użycia std :: pair (lub krotki) może wyglądać następująco:

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

ponieważ nie jest jasne, kiedy wywołujemy std :: get <2> (data_gracza), co jest przechowywane w indeksie pozycji 2. Pamiętaj o czytelności i uświadomienie czytelnikowi, co robi kod, jest ważne . Weź pod uwagę, że jest to o wiele bardziej czytelne:

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

Ogólnie powinieneś pomyśleć o std :: pair i std :: tuple jako sposobach na zwrócenie więcej niż 1 obiektu z funkcji. Zasadą, której używam (i widziałem także wielu innych), jest to, że obiekty zwracane w std :: tuple lub std :: pair są „powiązane” tylko w kontekście wywołania funkcji, która je zwraca lub w kontekście struktury danych, która łączy je ze sobą (np. std :: map używa std :: pair dla swojego typu pamięci). Jeśli relacja istnieje gdzie indziej w kodzie, należy użyć struktury.

Powiązane sekcje podstawowych wytycznych:

Damian Jarek
źródło