Después de TArray, el contenedor más usado en Unreal Engine es TMap. TMap se parece a TSet en que su estructura se basa en claves hash. Sin embargo, a diferencia de TSet, TMap almacena los datos como pares clave-valor (TPair<KeyType, ValueType>), usando claves solo para almacenamiento y recuperación.
Tipos de mapas en Unreal Engine
Hay dos tipos de mapas en Unreal Engine:
Resumen de TMap
En un TMap, los pares clave-valor se tratan como el tipo de elemento del mapa, como si cada par fuera un objeto individual. En este documento, «elemento» se refiere a un par clave-valor, mientras que los componentes individuales se denominan la clave del elemento o el valor del elemento.
El tipo de elemento es un
TPair<KeyType, ElementType>, aunque no debería ser habitual tener que hacer referencia al tipo TPair directamente.Las claves de TMap son únicas.
Al igual que TArray, TMap es un contenedor homogéneo, lo que significa que todos sus elementos son estrictamente del mismo tipo.
TMap es un tipo de valor y admite las operaciones habituales de copia, asignación y destructor, así como una propiedad sólida de sus elementos, que se destruyen cuando se destruye el mapa. La clave y el valor también deben ser tipos de valor.
TMap es un contenedor de hash, lo que significa que el tipo de clave debe ser compatible con la función GetTypeHash y proporcionar un
operator==para comparar las claves y determinar su igualdad.
TMap y TMultimap (al igual que muchos contenedores de Unreal Engine) asumen que el tipo de elemento es reubicable de forma trivial, lo que significa que los elementos se pueden con seguridad de una posición a otra en la memoria copiando directamente bytes sin procesar.
Resumen de TMultiMap
Admite el almacenamiento de varias claves idénticas.
Al añadir un nuevo par clave-valor a un TMap con una clave que coincida con un par existente, el nuevo par reemplazará al antiguo.
En un TMultiMap, el contenedor almacena tanto el par nuevo como el antiguo.
TMap puede usar un asignador opcional para controlar el comportamiento de la asignación de memoria. Sin embargo, a diferencia de TArray, estos son asignadores de conjuntos en lugar de los asignadores estándar de Unreal como FHeapAllocator y TInlineAllocator. Los asignadores de conjuntos (TSetAllocator) definen cuántos depósitos de hash debería usar el mapa y qué asignadores de UE estándar deberían usarse para el almacenamiento de hash y elementos.
El último parámetro de plantilla de TMap es KeyFuncs, que le dice al mapa cómo recuperar la clave del tipo de elemento, cómo comparar dos claves para determinar si son iguales y cómo aplicar el hash a la clave. Estos tienen valores por defecto que devuelven referencias a la clave, luego usan operator== para la igualdad, y llaman a la función GetTypeHash que no sea miembro para el hash. Si tu tipo de clave admite estas funciones, puedes usarla como clave de mapa sin tener que proporcionar un KeyFuncs personalizado.
A diferencia de TArray, el orden relativo de los elementos de TMap en la memoria no es fiable ni estable, y al iterar sobre los elementos es probable que se devuelvan en un orden distinto al que se añadieron. Es poco probable que los elementos se dispongan de forma contigua en la memoria.
La estructura de datos base de un mapa es una matriz dispersa, es decir, una matriz que admite de forma eficiente los espacios entre sus elementos. A medida que se vayan eliminando elementos del mapa, aparecerán espacios en la matriz dispersa. Esos espacios se pueden rellenar añadiendo nuevos elementos a la matriz. Sin embargo, aunque TMap no aleatoriza los elementos para rellenar los espacios, los punteros a los elementos del mapa pueden quedar invalidados, ya que todo el almacenamiento puede reasignarse cuando está lleno y se añaden nuevos elementos.
Cómo crear y rellenar un mapa
El siguiente código crea un TMap:
TMap<int32, FString> FruitMap;FruitMap ahora es un TMap vacío de cadenas que se identifican con claves enteras. No hemos especificado un asignador ni un KeyFuncs, por lo que el mapa realiza una asignación de montón estándar y compara la clave de tipo int32 con operator== y aplica la función hash a la clave con GetTypeHash. Aún no se ha asignado memoria.
Añadir
La forma estándar de propagar un mapa es llamar a Add con una clave y un valor:
FruitMap.Add(5, TEXT("Banana"));
FruitMap.Add(2, TEXT("Grapefruit"));
FruitMap.Add(7, TEXT("Pineapple"));
// FruitMap == [
// { Key: 5, Value: "Banana" },
// { Key: 2, Value: "Grapefruit" },
// { Key: 7, Value: "Pineapple" }
// ]Aunque los elementos se enumeran aquí en el orden en que se insertaron, no hay garantía de cuál será tu orden real en la memoria. Para un nuevo mapa, es probable que estén en orden de inserción, pero a medida que se producen más inserciones y eliminaciones, es cada vez menos probable que aparezcan nuevos elementos al final.
Esto no es un TMultiMap; por lo tanto, se garantiza que las claves sean únicas. El resultado de añadir una clave duplicada es el siguiente:
FruitMap.Add(2, TEXT("Pear"));
// FruitMap == [
// { Key: 5, Value: "Banana" },
// { Key: 2, Value: "Pear" },
// { Key: 7, Value: "Pineapple" }
// ]El mapa sigue conteniendo tres elementos, pero el valor anterior, Grapefruit, con una clave de 2, se ha sustituido por Pear.
La función Add puede aceptar una clave sin un valor. Cuando se llama a esta función Add sobrecargada, el valor se construirá por defecto:
FruitMap.Add(4);
// FruitMap == [
// { Key: 5, Value: "Banana" },
// { Key: 2, Value: "Pear" },
// { Key: 7, Value: "Pineapple" },
// { Key: 4, Value: "" }
// ]Emplace
Al igual que TArray, podemos usar Emplace en lugar de Add para evitar la creación de temporales al insertar en el mapa:
FruitMap.Emplace(3, TEXT("Orange"));
// FruitMap == [
// { Key: 5, Value: "Banana" },
// { Key: 2, Value: "Pear" },
// { Key: 7, Value: "Pineapple" },
// { Key: 4, Value: "" },
// { Key: 3, Value: "Orange" }
// ]Aquí, la clave y el valor se pasan directamente a sus respectivos constructores de tipos. Aunque esto no tiene sentido para la clave int32, evita la creación de un FString temporal para el valor. A diferencia de TArray, solo es posible insertar elementos en un mapa con constructores de un solo argumento.
Anexar
Puedes combinar dos mapas con la función Append, que mueve todos los elementos del mapa de argumentos al mapa de objetos que realiza la llamada:
TMap<int32, FString> FruitMap2;
FruitMap2.Emplace(4, TEXT("Kiwi"));
FruitMap2.Emplace(9, TEXT("Melon"));
FruitMap2.Emplace(5, TEXT("Mango"));
FruitMap.Append(FruitMap2);
// FruitMap == [
// { Key: 5, Value: "Mango" },
// { Key: 2, Value: "Pear" },
// { Key: 7, Value: "Pineapple" },
// { Key: 4, Value: "Kiwi" },
En el ejemplo anterior, el mapa resultante equivale a usar Add o Emplace para añadir cada elemento de FruitMap2 de forma individual, vaciando FruitMap2 cuando se complete el proceso. Esto significa que cualquier elemento de FruitMap2 que comparta su clave con un elemento que ya esté en FruitMap reemplazará a ese elemento.
Si marcas el TMap con la macro UPROPERTY y una de las palabras clave «editables» (EditAnywhere, EditDefaultsOnly o EditInstanceOnly), puedes añadir y editar elementos en el editor.
UPROPERTY(EditAnywhere, Category = MapsAndSets)
TMap<int32, FString> FruitMap;Iterar
La iteración sobre TMaps es similar a las TArrays. Puedes usar la función de rango de C++, recordando que el tipo de elemento es un TPair:
for (auto& Elem : FruitMap)
{
FPlatformMisc::LocalPrint(
*FString::Printf(
TEXT("(%d, \"%s\")\n"),
Elem.Key,
*Elem.Value
)
);
}
Puedes crear iteradores con las funciones CreateIterator y CreateConstIterator.
| Función | Descripción |
|---|---|
| Devuelve un iterador con acceso de lectura y escritura. |
| Devuelve un iterador de solo lectura. |
En cualquier caso, puedes usar las funciones Key y Value de estos iteradores para examinar los elementos. Imprimir el contenido de nuestro FruitMap de ejemplo usando iteradores quedaría así:
for (auto It = FruitMap.CreateConstIterator(); It; ++It)
{
FPlatformMisc::LocalPrint(
*FString::Printf(
TEXT("(%d, \"%s\")\n"),
It.Key(), // same as It->Key
*It.Value() // same as *It->Value
)
);
}Obtener valor
Si sabes que tu mapa contiene una clave determinada, puedes buscar el valor correspondiente con el operator[], usando la clave como índice. Hacer esto con un mapa no constante devuelve una referencia no constante, mientras que un mapa constante devuelve una referencia constante.
Siempre debes comprobar que el mapa contiene la clave antes de usar el operator[]. Si el mapa no contiene la clave, se afirmará.
FString Val7 = FruitMap[7];
// Val7 == "Pineapple"
FString Val8 = FruitMap[8];
// Assert!Consulta
Para determinar cuántos elementos hay actualmente en un TMap, llama a la función Num:
int32 Count = FruitMap.Num();
// Count == 6Para determinar si un mapa contiene o no una clave específica, llama a la función Contains:
bool bHas7 = FruitMap.Contains(7);
bool bHas8 = FruitMap.Contains(8);
// bHas7 == true
// bHas8 == falseSi no sabes con certeza si tu mapa contiene una clave o no, puedes comprobarlo con la función Contains y luego usar el operator[]. Sin embargo, esto no es lo mejor, ya que una recuperación correcta implica dos búsquedas en la misma clave.
La función Find combina estos comportamientos con una sola búsqueda. Find devuelve un puntero al valor del elemento si el mapa contiene la clave o un puntero nulo en caso contrario. Llamar a Find en un mapa constante devuelve un puntero constante.
FString* Ptr7 = FruitMap.Find(7);
FString* Ptr8 = FruitMap.Find(8);
// *Ptr7 == "Pineapple"
// Ptr8 == nullptrTambién puedes usar FindOrAdd o FindRef para garantizar que recibes un resultado válido de tu consulta:
| Función | Descripción |
|---|---|
| Devuelve una referencia al valor asociado a la clave que proporciones. Si la clave no está en el mapa,
|
| A pesar de su nombre, devuelve una copia del valor asociado a tu clave, o un valor construido por defecto si tu clave no se encuentra en el mapa. |
Dado que FindOrAdd y FindRef dan un resultado correcto aunque la clave no se encuentre en el mapa, puedes llamarlos sin tener que recurrir a los procedimientos de seguridad habituales, como comprobar de antemano Contains o comprobar si el valor de retorno es nulo.
FString& Ref7 = FruitMap.FindOrAdd(7);
// Ref7 == "Pineapple"
// FruitMap == [
// { Key: 5, Value: "Mango" },
// { Key: 2, Value: "Pear" },
// { Key: 7, Value: "Pineapple" },
// { Key: 4, Value: "Kiwi" },
// { Key: 3, Value: "Orange" },
// { Key: 9, Value: "Melon" }
// ]
Como FindOrAdd puede añadir nuevas entradas al mapa, como hace al inicializar Ref8 en nuestro ejemplo, los punteros o las referencias obtenidos previamente podrían dejar de ser válidos. Este es el resultado de la operación de adición que asigna memoria y mueve los datos existentes si el almacenamiento del backend del mapa necesita expandirse para contener el nuevo elemento. En el ejemplo anterior, es posible que Ref7 se invalide después de Ref8 después de la llamada a FindOrAdd(8).
La función FindKey realiza una búsqueda inversa, lo que significa que un valor proporcionado se asocia con una clave y devuelve un puntero a la primera clave que se empareja con el valor proporcionado. Buscar un valor que no esté presente en el mapa devuelve un puntero nulo.
const int32* KeyMangoPtr = FruitMap.FindKey(TEXT("Mango"));
const int32* KeyKumquatPtr = FruitMap.FindKey(TEXT("Kumquat"));
// *KeyMangoPtr == 5
// KeyKumquatPtr == nullptrLas búsquedas por valor son más lentas (en términos lineales) que las búsquedas por clave. Esto se debe a que el mapa se hashea por clave, no por valor. Además, si un mapa tiene varias claves con el mismo valor, FindKey puede devolver cualquiera de ellas.
Las funciones GenerateKeyArray y GenerateValueArray rellenan un TArray con una copia de todas las claves y valores respectivamente. En ambos casos, la matriz que se transmite se vacía antes del rellenado, por lo que el número de elementos resultante siempre será igual al número de elementos del mapa.
TArray<int32> FruitKeys;
TArray<FString> FruitValues;
FruitKeys.Add(999);
FruitKeys.Add(123);
FruitMap.GenerateKeyArray (FruitKeys);
FruitMap.GenerateValueArray(FruitValues);
// FruitKeys == [ 5,2,7,4,3,9,8 ]
// FruitValues == [ "Mango","Pear","Pineapple","Kiwi","Orange",
// "Melon","" ]Quitar
Puedes eliminar elementos de un mapa usando la función Remove y proporcionando la clave del elemento que quieras eliminar. El valor devuelto es la cantidad de elementos que se eliminaron y puede ser cero si el mapa no contiene ningún elemento que coincida con la clave.
FruitMap.Remove(8);
// FruitMap == [
// { Key: 5, Value: "Mango" },
// { Key: 2, Value: "Pear" },
// { Key: 7, Value: "Pineapple" },
// { Key: 4, Value: "Kiwi" },
// { Key: 3, Value: "Orange" },
// { Key: 9, Value: "Melon" }
// ]La eliminación de elementos puede dejar espacios en la estructura de datos, que puedes ver al visualizar el mapa en la ventana de inspección de Visual Studio, pero se han omitido aquí para mayor claridad.
La función FindAndRemoveChecked se puede usar para eliminar un elemento del mapa y devolver su valor. La parte «marcada» del nombre indica que las llamadas al mapa comprueban si la clave no existe.
FString Removed7 = FruitMap.FindAndRemoveChecked(7);
// Removed7 == "Pineapple"
// FruitMap == [
// { Key: 5, Value: "Mango" },
// { Key: 2, Value: "Pear" },
// { Key: 4, Value: "Kiwi" },
// { Key: 3, Value: "Orange" },
// { Key: 9, Value: "Melon" }
// ]
La función RemoveAndCopyValue es similar a Remove, pero copia el valor del elemento eliminado en un parámetro de referencia. Si la clave que has especificado no está presente en el mapa, el parámetro de salida no cambiará y la función devuelve false.
FString Removed;
bool bFound2 = FruitMap.RemoveAndCopyValue(2, Removed);
// bFound2 == true
// Removed == "Pear"
// FruitMap == [
// { Key: 5, Value: "Mango" },
// { Key: 4, Value: "Kiwi" },
// { Key: 3, Value: "Orange" },
// { Key: 9, Value: "Melon" }
// ]
Por último, puedes eliminar todos los elementos del mapa con las funciones Empty o Reset.
TMap<int32, FString> FruitMapCopy = FruitMap;
// FruitMapCopy == [
// { Key: 5, Value: "Mango" },
// { Key: 4, Value: "Kiwi" },
// { Key: 3, Value: "Orange" },
// { Key: 9, Value: "Melon" }
// ]
FruitMapCopy.Empty(); // You can also use Reset() here.
// FruitMapCopy == []Empty puede tomar un parámetro para indicar cuánta holgura dejar en el mapa, mientras que Reset siempre deja la mayor holgura posible.
Clasificar
Puedes ordenar un TMap por clave o por valor. Después de ordenar, la iteración sobre el mapa presenta los elementos ordenados, pero este comportamiento solo está garantizado hasta la próxima vez que modifiques el mapa. La clasificación es inestable, por lo que los elementos equivalentes en un TMultiMap pueden aparecer en cualquier orden.
Puedes ordenar por clave o por valor con las funciones KeySort o ValueSort, respectivamente. Ambas funciones utilizan un predicado binario que especifica el criterio de ordenación.
FruitMap.KeySort([](int32 A, int32 B) {
return A > B; // sort keys in reverse
});
// FruitMap == [
// { Key: 9, Value: "Melon" },
// { Key: 5, Value: "Mango" },
// { Key: 4, Value: "Kiwi" },
// { Key: 3, Value: "Orange" }
// ]
Operadores
Al igual que TArray, TMap es un tipo de valor normal y se puede copiar con el constructor de copia estándar o con el operador de asignación. Los mapas son propietarios estrictos de sus elementos, por lo que copiar un mapa es una tarea compleja; el nuevo mapa tendrá su propia copia de los elementos.
TMap<int32, FString> NewMap = FruitMap;
NewMap[5] = "Apple";
NewMap.Remove(3);
// FruitMap == [
// { Key: 4, Value: "Kiwi" },
// { Key: 5, Value: "Mango" },
// { Key: 9, Value: "Melon" },
// { Key: 3, Value: "Orange" }
// ]
// NewMap == [
Tmap es compatible con la semántica de movimientos, la cual puede invocarse con la función MoveTemp. Después de un movimiento, se garantiza que el mapa de origen estará vacío:
FruitMap = MoveTemp(NewMap);
// FruitMap == [
// { Key: 4, Value: "Kiwi" },
// { Key: 5, Value: "Apple" },
// { Key: 9, Value: "Melon" }
// ]
// NewMap == []Holgura
La holgura es memoria asignada que no contiene ningún elemento. Puedes asignar memoria sin añadir elementos haciendo una llamada a Reserve y puedes eliminar elementos sin desasignar la memoria que estaban usando haciendo una llamada a Reset o a Empty con un parámetro de holgura distinto de cero. La holgura optimiza el proceso de añadir nuevos elementos al mapa usando memoria preasignada en lugar de tener que asignar nueva memoria. También puede ayudar con la eliminación de elementos, ya que el sistema no necesita desasignar memoria. Esto es especialmente eficaz a la hora de vaciar un mapa que esperas repoblar inmediatamente con la misma cantidad de elementos o menos.
TMap no proporciona una forma de comprobar cuántos elementos están preasignados como lo hace la función Max en TArray.
En el siguiente código, la función Reserve asigna espacio para que el mapa contenga hasta diez elementos:
FruitMap.Reserve(10);
for (int32 i = 0; i < 10; ++i)
{
FruitMap.Add(i, FString::Printf(TEXT("Fruit%d"), i));
}
// FruitMap == [
// { Key: 9, Value: "Fruit9" },
// { Key: 8, Value: "Fruit8" },
// ...
// { Key: 1, Value: "Fruit1" },
Para eliminar todo la holgura de un TMap, usa las funciones Collpase y Shrink. Shrink elimina toda la holgura del final del contenedor, pero deja los elementos vacíos en el medio o al principio.
for (int32 i = 0; i < 10; i += 2)
{
FruitMap.Remove(i);
}
// FruitMap == [
// { Key: 9, Value: "Fruit9" },
// <invalid>,
// { Key: 7, Value: "Fruit7" },
// <invalid>,
// { Key: 5, Value: "Fruit5" },
Shrink solo eliminó un elemento no válido en el código anterior porque solo había un elemento vacío al final. Para eliminar toda la holgura, usa primero la función Compact para que los espacios vacíos se agrupen como preparación para Shrink.
FruitMap.Compact();
// FruitMap == [
// { Key: 9, Value: "Fruit9" },
// { Key: 7, Value: "Fruit7" },
// { Key: 5, Value: "Fruit5" },
// { Key: 3, Value: "Fruit3" },
// { Key: 1, Value: "Fruit1" },
// <invalid>,
// <invalid>,
// <invalid>,
KeyFuncs
Siempre que un tipo tenga un operator== y una sobrecarga GetTypeHash que no sea miembro, puedes usarlo como tipo de clave para un TMap sin ningún cambio. Sin embargo, es posible que quieras usar tipos como claves sin sobrecargar esas funciones. En estos casos, puedes proporcionar tu propio KeyFuncs personalizado. Para crear KeyFuncs para tu tipo de clave, debes definir dos definiciones de tipo y tres funciones estáticas, tal y como se indica a continuación:
| Definición de tipo | Descripción |
|---|---|
| Tipo usado para pasar claves. |
| Tipo usado para pasar elementos. |
| Función | Descripción |
|---|---|
| Devuelve la clave de un elemento. |
| Devuelve |
| Devuelve el valor hash de |
KeyInitType y ElementInitType son definiciones de tipo de la convención normal de paso del tipo de clave y el tipo de elemento. Por lo general, serán un valor para los tipos triviales y una referencia constante para los tipos no triviales. Recuerda que el tipo de elemento de un mapa es un TPair.
El siguiente fragmento de código es un ejemplo de KeyFuncs personalizadas:
MyCustomKeyFuncs.cpp
struct FMyStruct
{
// String which identifies our key
FString UniqueID;
// Some state which doesn't affect struct identity
float SomeFloat;
explicit FMyStruct(float InFloat)
: UniqueID (FGuid::NewGuid().ToString())
FMyStruct cuenta con un identificador único, así como con otros datos que no contribuyen a su identidad. GetTypeHash y operator== no serían apropiados aquí, porque operator== no debería ignorar ninguno de los datos del tipo para un uso general, pero tendría que hacerlo simultáneamente para ser coherente con el comportamiento de GetTypeHash, que solo mira el campo UniqueID.
Para crear un KeyFuncs personalizado para FMyStruct, sigue estos pasos:
Hereda de
BaseKeyFuncs, ya que define algunos tipos útiles, comoKeyInitTypeyElementInitType.BaseKeyFuncsutiliza dos parámetros de plantilla:El tipo de elemento del mapa.
Como ocurre con todos los mapas, el tipo de elemento es un
TPair, conFMyStructcomoKeyTypey el parámetro de plantilla deTMyStructMapKeyFuncscomoValueType. ElKeyFuncsde reemplazo es una plantilla, por lo que puedes especificarValueTypemapa por mapa, en lugar de tener que definir un nuevoKeyFuncscada vez que quieras crear un TMap con clave enFMyStruct.
El tipo de nuestra clave.
El segundo argumento
BaseKeyFuncses el tipo de la clave, que no debe confundirse con elKeyTypede TPair, que almacena el campo Key del elemento. Dado que este mapa debería usarUniqueID(deFMyStruct) como su clave, aquí se usaFString.
Define las tres funciones estáticas de
KeyFuncsnecesarias.La primera es GetSetKey, que devuelve la clave de un tipo de elemento dado. Nuestro tipo de elemento es
TPair, y nuestra clave esUniqueID, por lo que la función puede devolverUniqueIDdirectamente.La segunda función estática es Matches, que toma las claves de dos elementos recuperados por
GetSetKeyy las compara para ver si son equivalentes. ParaFString, la prueba de equivalencia estándar (operator==) no distingue entre mayúsculas y minúsculas; para sustituirlo por una búsqueda que distinga entre mayúsculas y minúsculas, usa la funciónCompare()con la opción de comparación de mayúsculas y minúsculas adecuada.La tercera función estática es
GetKeyHash, que toma una clave extraída y devuelve un valor hash para ella. Dado que la funciónMatchesdistingue entre mayúsculas y minúsculas,GetKeyHashtambién debe hacerlo. Una función FCrc que distingue entre mayúsculas y minúsculas calcula el valor hash a partir de la cadena de la clave.
Ahora que la estructura es compatible con los comportamientos que requiere TMap, puedes crear instancias.
C++TMap< FMyStruct, int32, FDefaultSetAllocator, TMyStructMapKeyFuncs<int32> > MyMapToInt32; // Add some elements MyMapToInt32.Add(FMyStruct(3.14f), 5); MyMapToInt32.Add(FMyStruct(1.23f), 2);En este ejemplo, se especifica el asignador de conjuntos predeterminado. Esto se debe a que el parámetro
KeyFuncses el último, y este tipoTMaplo requiere.
Cuando proporciones tu propio KeyFuncs, ten en cuenta que TMap asume que dos elementos que se comparan como iguales con Matches también devuelven el mismo valor de GetKeyHash. Además, modificar la clave de un elemento de mapa existente de forma que cambie los resultados de cualquiera de estas funciones se considera un comportamiento indefinido, ya que invalida el hash interno del mapa. Estas reglas también se aplican a las sobrecargas del operator== y GetKeyHash al usar KeyFuncs por defecto.
Varios
Las funciones CountBytes y GetAllocatedSize calculan cuánta memoria está utilizando actualmente la matriz interna. CountBytes usa un parámetro FArchive, mientras que GetAllocatedSize no. Estas funciones se suelen usar para generar informes de estadísticas.
La función Dump toma un FOutputDevice y escribe información de implementación sobre el contenido del mapa. Esta función se utiliza normalmente para la depuración.