I have a struct within a struct of std::optionals, and I am trying to find the minimum and maximum value within the struct. However, using std::minmax_element seems to be inaccurate, and I've had to split the comparison function and use std::min_element and std::max_element instead. Is there a way to approach this that will allow me to still use std::minmax_element and be cleaner?
#include <iostream>
#include <vector>
#include <optional>
#include <algorithm>
struct AE
{
double A;
double E;
};
struct HM
{
std::optional<AE> H;
std::optional<AE> M;
};
bool CompareHM(const HM& a_, const HM& b_)
{
// If both H fields have a value, compare them by A
if (a_.H.has_value() && b_.H.has_value())
{
return a_.H->A < b_.H->A;
}
// If only one H field has a value, set to false
else
{
return false;
}
}
bool CompareHMMax(const HM& a_, const HM& b_)
{
// If both H fields have a value, compare them by A
if (a_.H.has_value() && b_.H.has_value())
{
return a_.H->A < b_.H->A;
}
// If only one H field has a value, consider it larger
else if (a_.H.has_value())
{
return false;
}
// If both H fields are empty, or if just b has a value
else {
return false;
}
}
bool CompareHMMin(const HM& a_, const HM& b_)
{
// If both H fields have a value, compare them by A
if (a_.H.has_value() && b_.H.has_value())
{
return a_.H->A < b_.H->A;
}
// If only one H field has a value, consider it smaller
else if (a_.H.has_value())
{
return true;
}
// If both H fields are empty, or if just b has a value
else
{
return false;
}
}
int main()
{
// Create a vector of HM structs with some data
std::vector<HM> vec = {
HM{AE{1.8, 4.5}, AE{7.2, 4.1}},
HM{AE{2.3, 3.4}, std::nullopt},
HM{std::nullopt, AE{6.7, 8.9}},
HM{AE{0.9, 2.1}, std::nullopt},
HM{AE{1.8, 4.2}, AE{7.6, 9.0}},
HM{std::nullopt, AE{0.7, 13.5}},
HM{std::nullopt, AE{0.5, 19.3}},
HM{AE{2.1, 4.2}, std::nullopt},
};
auto minmax = std::minmax_element(vec.begin(), vec.end(), CompareHM);
auto min = std::min_element(vec.begin(), vec.end(), CompareHMMin);
auto max = std::max_element(vec.begin(), vec.end(), CompareHMMax);
// Print the results
std::cout << "Correct Min A value: " << min->H->A << "\n";
std::cout << "Correct Max A value: " << max->H->A << "\n";
std::cout << "Incorrect Min A value using minmax_element: " << minmax.first->H->A << "\n";
std::cout << "Incorrect Max A value using minmax_element: " << minmax.second->H->A << "\n";
return 0;
}
Output:
Correct Min A value: 0.9
Correct Max A value: 2.3
Incorrect Min A value using minmax_element: 1.8
Incorrect Max A value using minmax_element: 2.1
CompareHM
does not induce a strict weak ordering, which is why std::minmax_element
gives you nonsense results.
A strict weak ordering needs to be transitive, and incomparable elements break this:
a < std::nullopt && std::nullopt < b => a < b
This implication needs to be true for a strict weak ordering, but clearly isn't when std::nullopt
is incomparable.
Incomparability needs to be a transitive relationship, i.e.
a incomparable_to std::nullopt && b incomparable_to => a incomparable_to b
But this is clearly not the case if a
is comparable to b
.
On a side note this is also why you cannot sort ranges of float
s that contain NaN (see Is NaN a valid key value for associative containers?).
Unlike in your Min
and Max
relations, we cannot simply treat std::nullopt
as a positive/negative infinity value.
In older C++ standards, you could eliminate std::nullopt
elements in-place using std::remove_if
in the remove-erase idiom:
vec.erase(std::remove_if([](const HM& h) {
return !h.has_value();
}, vec.end());
auto minmax = std::minmax_element(vec);
std::cout << minmax.first->H->A << ' ' << minmax.second->H->A << '\n';
Actually, it would be sufficient to use just std::remove_if
if your goal is only to find the minimum and maximum element.
Obviously, there's always the option of using std::min_element
and std::max_element
separately.
We could use std::ranges::filter_view
to get a view of our range without any std::nullopt
elements inside, allowing std::ranges::minmax_element
to work correctly.
auto filter = std::ranges::filter_view(vec, [](const HM& h) {
return h.H.has_value();
});
auto minmax = std::ranges::minmax_element(filter, CompareHM);
std::cout << minmax.min->H->A << ' ' << minmax.max->H->A << '\n';