I have a quad-/octree data structure. Im storing the children indexes/ptrs of a cell in an array. Each position in the array represents the location of a child with respect to its parent, e.g. in 2D:
// _____________
// | | |
// | 2 | 3 |
// |_____|_____|
// | | |
// | 0 | 1 |
// |_____|_____|
// for each cell, 4 children are always stored in row-major order
std::vector<std::array<Integer,4>> children;
I know that the max number of children is a subset of the values that an Integer
type can represent. Thus I can identify if a cell is missing a child by using a ''magic'' value like -1
for Integer = int
, or std::numeric_limits<unsigned>::max()
for Integer = unsigned
. This is something that std::optional<Integer>
cannot assume.
As far as I understood, this usage of magic values is one of the raison d'être of std::optional
. Still, I'm worried about the performance of std::vector<std::optional<int>>
in inner loops.
So,
Will the performance of std::vector<std::optional<int>>
be worse than that of std::vector<int>
? (I'm already doing the comparison for "non-existent" value).
Or, can the implementation of std::optional
be optimized to offer the same performance as a raw int
? And how?
Mixing std::optional
in the return type of my functions and magic values in my data structure sounds like a very bad idea. I prefer to be consistent and either use one or the other (at least within the same context). Although I could overload the function that performs the comparison with the magic number:
template<T> bool is_valid(const T& t) {
return /* comparison with magic value for t */;
}
for optional types.
As a conclusion, std::optional is as efficient as a custom type to represent an optional integer value. Don't implement your own type, simply use the standard type.
We use std::optional to make our code more expressive. std::optional contains the object within itself, depending on where it is stored (stack/data/heap) std::optional makes a copy of the contained object.
What's more, std::optional doesn't need to allocate any memory on the free store. std::optional is a part of C++ vocabulary types along with std::any , std::variant and std::string_view .
Thus, an optional object models an object, not a pointer, even though operator*() and operator->() are defined. When an object of type optional<T> is contextually converted to bool , the conversion returns true if the object contains a value and false if it does not contain a value.
std::optional
is going to require additional storage and fit fewer values into cache (it appears you already know the reason for this).
I don't think it's wrong to have a different value stored internally in your data structure from the one exposed by the public API, as long as the internal representation is completely hidden from users.
Furthermore, I suggest you isolate the magic number into a single pair of inline
conversion functions.
The compiler should help you remember to use the conversion functions consistently, by generating type errors if you forget. You might even use a thin struct wrapper for an int
in your internal data structure, to ensure that no implicit conversion exists (or define a user-defined conversion).
class CompressedOptionalUInt
{
static const unsigned SENTINEL_MISSING = std::numeric_limits<unsigned>::max();
unsigned value;
public:
CompressedOptionalUInt(std::optional<unsigned> val) : value(!val? SENTINEL_MISSING: *val) {}
operator std::optional<unsigned>() const { ... }
};
and then use std::array<CompressedOptionalUInt>
.
Making that into a template, with just the sentinel needing to be defined for each type, should be pretty straightforward.
No, it's not as efficient. As you can see from the reference implementation it has to store, update and check an extra value.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With