Even though that's far from ideal (because it causes additional gas cost), this is how I do it now:
pragma solidity ^0.4.24;
contract MyContract {
struct Person {
uint age;
uint size;
}
// Index of a person is its ID.
Person[] persons;
// The following holds a list of currently existing persons (without holes between the indexes)
uint256[] personIds;
// Holds the mapping from personID to its index in the above personIds array.
mapping (uint256 => uint256) personIdToIndex;
modifier personExists(uint256 _id) {
uint256 index = personIdToIndex[_id]; // is 0 if not explicitly set
require(personIds[index] == _id, "Person does not exist.");
_;
}
event PersonAdded(uint indexed id, uint age, uint size);
function addPerson(uint _age, uint _size) public {
Person memory person = Person(_age, _size);
id = persons.push(person) - 1;
uint256 key = personIds.push(id) - 1;
personIdToIndex[id] = key;
emit PersonAdded(id, _age, _size)
}
function removePerson(uint _id) personExists(_id) public {
delete persons[_id]
// Reorganize index array and map
uint256 key = personIdToIndex[_id];
// Fill gap with last element of personIds
personIds[key] = personIds[personIds.length - 1];
// Update personIdToIndex
personIdToIndex[personIds[key]] = key;
// Decrease length of personIds array by 1
personIds.length--;
}
}
I won't mark this as the correct answer for now, because I still hope there's a more elegant solution.