¿Qué diferencia hay entre usar una estructura y un par std ::?

26

Soy un programador de C ++ con experiencia limitada.

Suponiendo que quiero usar un STL mappara almacenar y manipular algunos datos, me gustaría saber si hay alguna diferencia significativa (también en el rendimiento) entre esos 2 enfoques de estructura de datos:

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

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

Específicamente, ¿hay alguna sobrecarga usando un en structlugar de un simple pair?

Marco Stramezzi
fuente
18
A std::pair es una estructura.
Caleth
3
@gnat: Preguntas generales como esa rara vez son objetivos de engaño adecuados para preguntas específicas como esta, especialmente si la respuesta específica no existe en el objetivo de engaño (lo cual es poco probable en este caso).
Robert Harvey
18
@Caleth: std::paires una plantilla . std::pair<string, bool>es una estructura
Pete Becker
44
pairestá completamente desprovisto de semántica. Nadie que lea su código (incluido usted en el futuro) sabrá que ese e.firstes el nombre de algo a menos que lo indique explícitamente. Soy un firme creyente de que pairfue una adición muy pobre y perezosa std, y que cuando se concibió nadie pensó "pero algún día, todos van a usar esto para todo lo que son dos cosas, y nadie sabrá lo que significa el código de nadie ".
Jason C
2
@Snowman Oh, definitivamente. Aún así, es una lástima que los mapiteradores no sean excepciones válidas. ("first" = key y "second" = value ... realmente std,? Really?)
Jason C

Respuestas:

33

La opción 1 está bien para cosas pequeñas "usadas solo una vez". Esencialmente std::pairsigue siendo una estructura. Como se indica en este comentario, la opción 1 conducirá a un código realmente feo en algún lugar del agujero del conejo comothing.second->first.second->second y nadie realmente quiere descifrar eso.

La opción 2 es mejor para todo lo demás, porque es más fácil leer cuál es el significado de las cosas en el mapa. También es más flexible si desea cambiar los datos (por ejemplo, cuando Ente repentinamente necesita otra bandera). El rendimiento no debería ser un problema aquí.

creciente
fuente
15

Rendimiento :

Depende.

En su caso particular, no habrá diferencia de rendimiento porque los dos se presentarán de manera similar en la memoria.

En un caso muy específico (si estaba utilizando una estructura vacía como uno de los miembros de datos), std::pair<>podría utilizar la Optimización de base vacía (EBO) y tener un tamaño inferior al equivalente de la estructura. Y un tamaño más bajo generalmente significa un mayor rendimiento:

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

Impresiones: 32, 32, 40, 40 en ideona .

Nota: No conozco ninguna implementación que realmente use el truco EBO para pares regulares, sin embargo, generalmente se usa para tuplas.


Legibilidad :

Además de las microoptimizaciones, una estructura con nombre es más ergonómica.

Quiero decir, map[k].firstno es tan malo mientras get<0>(map[k])apenas es inteligible. Contrastar conmap[k].name que inmediatamente indica de qué estamos leyendo.

Es aún más importante cuando los tipos son convertibles entre sí, ya que intercambiarlos sin darse cuenta se convierte en una verdadera preocupación.

Es posible que también desee leer sobre Mecanografía estructural vs nominal. Entees un tipo específico que sólo puede ser operado por las cosas que esperan Ente, cualquier cosa que pueda operar sobre std::pair<std::string, bool>puede operar sobre ellos ... incluso cuando el std::stringo boolno contiene lo que esperan, porque std::pairno tiene la semántica asociados a ella.


Mantenimiento :

En términos de mantenimiento, paires lo peor. No puedes agregar un campo.

tupleferias mejor en ese sentido, siempre y cuando agregue el nuevo campo todos los campos existentes todavía se accede por el mismo índice. Lo cual es tan inescrutable como antes, pero al menos no necesita ir a actualizarlos.

structEs el claro ganador. Puede agregar campos donde lo desee.


En conclusión:

  • pair es lo peor de ambos mundos
  • tuple puede tener una ligera ventaja en un caso muy específico (tipo vacío),
  • usostruct .

Nota: si usa getters, puede usar el truco de la base vacía usted mismo sin que los clientes tengan que saberlo como en struct Thing: Empty { std::string name; }; Es por eso que Encapsulación es el siguiente tema con el que debe preocuparse.

Matthieu M.
fuente
3
No puede usar EBO para pares, si está siguiendo el Estándar. Elementos de par se almacenan en los miembros first y second, no hay lugar para el vacío Base optimización a patada en.
Revolver_Ocelot
2
@Revolver_Ocelot: Bueno, no puedes escribir un C ++ pairque usaría EBO, pero un compilador podría proporcionar un incorporado. Sin embargo, dado que se supone que son miembros, puede ser observable (verificando sus direcciones, por ejemplo) en cuyo caso no sería conforme.
Matthieu M.
1
C ++ 20 agrega [[no_unique_address]], lo que habilita el equivalente de EBO para los miembros.
underscore_d
3

El par brilla más cuando se usa como el tipo de retorno de una función junto con la asignación desestructurada usando std :: tie y el enlace estructurado de 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 el enlace estructurado de 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 mal ejemplo de uso de un std :: pair (o tupla) sería algo como esto:

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

porque no está claro al llamar a std :: get <2> (player_data) lo que está almacenado en el índice de posición 2. Recuerde que la legibilidad y que sea obvio para el lector lo que hace el código es importante . Considere que esto es mucho más legible:

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

En general, debe pensar en std :: pair y std :: tuple como formas de devolver más de 1 objeto de una función. La regla general que uso (y he visto usar muchos otros también) es que los objetos devueltos en un par std :: tuple o std :: solo están "relacionados" dentro del contexto de hacer una llamada a una función que los devuelve o en el contexto de la estructura de datos que los une (por ejemplo, std :: map usa std :: pair para su tipo de almacenamiento). Si la relación existe en otra parte de su código, debe usar una estructura.

Secciones relacionadas de las Directrices básicas:

Damian Jarek
fuente