Que diferença existe entre o uso de uma struct e um std :: pair?


26

Eu sou um programador de C ++ com experiência limitada.

Supondo que eu queira usar um STL mappara armazenar e manipular alguns dados, gostaria de saber se há alguma diferença significativa (também no desempenho) entre essas duas abordagens da estrutura de dados:

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

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

Especificamente, existe alguma sobrecarga usando um em structvez de um simples pair?


18
A std::pair é uma estrutura.
Caleth

3
@gnat: Perguntas gerais como essa raramente são alvos idiotas adequados para perguntas específicas como esta, especialmente se a resposta específica não existir no alvo idiota (o que é improvável neste caso).
Robert Harvey

18
@Caleth - std::pairé um modelo . std::pair<string, bool>é uma estrutura.
Pete Becker

4
pairé totalmente desprovido de semântica. Ninguém lendo seu código (incluindo você no futuro) saberá que esse e.firsté o nome de algo, a menos que você o indique explicitamente. Acredito firmemente que isso pairfoi uma adição muito pobre e preguiçosa std, e que, quando foi concebido, ninguém pensou "mas algum dia, todo mundo vai usar isso para tudo o que é duas coisas, e ninguém saberá o que o código de alguém significa" "
Jason C

2
@ Snowman Oh, definitivamente. Ainda assim, é uma pena que os mapiteradores não sejam exceções válidas. ( "primeiro" = chave e "segundo" = valor ... realmente, stdrealmente?)
Jason C

Respostas:


33

A opção 1 é válida para pequenas coisas "usadas apenas uma vez". Essencialmente std::pairainda é uma estrutura. Como afirmado por esse comentário, a escolha 1 levará a um código realmente feio em algum lugar abaixo da toca do coelho thing.second->first.second->seconde ninguém realmente quer decifrar isso.

A opção 2 é melhor para todo o resto, porque é mais fácil ler qual é o significado das coisas no mapa. Também é mais flexível se você deseja alterar os dados (por exemplo, quando o Ente precisa de outro sinalizador). O desempenho não deve ser um problema aqui.


15

Desempenho :

Depende.

No seu caso particular, não haverá diferença de desempenho, pois os dois serão dispostos da mesma forma na memória.

Em um caso muito específico (se você estivesse usando uma estrutura vazia como um dos membros dos dados), o std::pair<>potencial poderia fazer uso da otimização da base vazia (EBO) e ter um tamanho menor que o equivalente à estrutura. E tamanho menor geralmente significa maior desempenho:

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

Impressões: 32, 32, 40, 40 em ideone .

Nota: Não conheço nenhuma implementação que realmente use o truque EBO para pares regulares, no entanto, geralmente é usado para tuplas.


Legibilidade :

Além das micro-otimizações, no entanto, uma estrutura nomeada é mais ergonômica.

Quero dizer, map[k].firstnão é tão ruim assim tão get<0>(map[k])pouco inteligível. Contraste com o map[k].namequal indica imediatamente o que estamos lendo.

É ainda mais importante quando os tipos são conversíveis entre si, pois trocá-los inadvertidamente se torna uma preocupação real.

Você também pode querer ler sobre Estrutural versus Tipagem Nominal. Enteé um tipo específico que só pode ser operado por coisas que esperamos Ente, qualquer coisa que possa operar std::pair<std::string, bool>pode operar com elas ... mesmo quando o contém std::stringou boolnão o que eles esperam, porque std::pairnão possui semântica associada a ele.


Manutenção :

Em termos de manutenção, pairé o pior. Você não pode adicionar um campo.

tuplefeiras melhor nesse sentido, desde que você acrescente o novo campo, todos os campos existentes ainda serão acessados ​​pelo mesmo índice. O que é tão inescrutável quanto antes, mas pelo menos você não precisa atualizá-los.

structé o vencedor claro. Você pode adicionar campos onde quiser.


Em conclusão:

  • pair é o pior dos dois mundos,
  • tuple pode ter uma ligeira aresta em um caso muito específico (tipo vazio),
  • usestruct .

Nota: se você usa getters, pode usar o truque de base vazia sem que os clientes tenham que saber sobre isso como em struct Thing: Empty { std::string name; }; é por isso que o encapsulamento é o próximo tópico com o qual você deve se preocupar.


3
Você não pode usar o EBO para pares, se estiver seguindo o padrão. Os elementos do par são armazenados nos membros first e second, não há lugar para a Otimização da Base Vazia entrar em ação.
Revolver_Ocelot

2
@Revolver_Ocelot: Bem, você não pode escrever um C ++ pairque usaria EBO, mas um compilador poderia fornecer um built-in. Como esses membros devem ser membros, no entanto, pode ser observável (verificando seus endereços, por exemplo), caso em que não estaria em conformidade.
Matthieu M. 24/03

11
C ++ 20 adiciona [[no_unique_address]], o que permite o equivalente a EBO para membros.
underscore_d

3

O par brilha mais quando usado como o tipo de retorno de uma função, juntamente com a atribuição desestruturada usando std :: tie e a ligação estruturada do C ++ 17. Usando 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
}

Usando a ligação estruturada do C ++ 17:

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

Um mau exemplo de uso de um std :: pair (ou tupla) seria algo como isto:

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

porque não está claro ao chamar std :: get <2> (player_data) o que está armazenado no índice de posição 2. Lembre-se da legibilidade e deixe óbvio para o leitor o que o código está fazendo é importante . Considere que isso é muito mais legível:

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

Em geral, você deve pensar em std :: pair e std :: tuple como formas de retornar mais de 1 objeto de uma função. A regra geral que eu uso (e já vi muitos outros também) é que os objetos retornados em um std :: tuple ou std :: pair são apenas "relacionados" no contexto de fazer uma chamada para uma função que os retorna ou no contexto da estrutura de dados que os vincula (por exemplo, std :: map usa std :: pair para seu tipo de armazenamento). Se o relacionamento existir em outro lugar no seu código, você deve usar uma estrutura.

Seções relacionadas das Diretrizes Principais:

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.