I'm trying to make floating point equality comparisons explicit for my custom classes (exact comparison vs approximate comparison). I can avoid overloading the ==
operator and force users to call functions like exactly_equal
or almost_equal
instead but this doesn't work with std
algorithms out of the box. I was wondering if there was a nice way to get the best of both worlds by forcing some explicit operator lookup at the call site (kind of like std::rel_ops
).
For example, let's say I have this CustomType
class and some operator==
overloads:
struct CustomType {
double value;
};
namespace exactly_equal {
auto operator==(CustomType const& lhs, CustomType const& rhs) -> bool {
return lhs.value == rhs.value;
}
} // namespace exactly_equal
namespace almost_equal {
auto operator==(CustomType const& lhs, CustomType const& rhs) -> bool {
return std::abs(lhs.value - rhs.value) < 1e-2; // This isn't the "right" way, just an example.
}
} // namespace almost_equal
Using this class I can do something like this, which compiles and runs fine:
auto main() -> int {
auto const a = CustomType{1.0/3.0};
auto const b = CustomType{0.3333};
{
using namespace exactly_equal;
if (a == b) {
std::cout << "Exact!" << std::endl;
}
}
{
using namespace almost_equal;
if (a == b) {
std::cout << "Inexact!" << std::endl;
}
}
return 0;
}
The thing that doesn't work is the argument-dependent lookup when using std
functions:
auto main() -> int {
auto const items = std::vector<CustomType>{{1.0/3.0}};
auto const value = CustomType{0.3333};
{
using namespace exactly_equal;
// error: no match for 'operator==' (operand types are 'const CustomType' and 'const CustomType')
if (std::find(items.begin(), items.end(), value) != items.end()) {
std::cout << "Exact!" << std::endl;
}
}
{
using namespace almost_equal;
// error: no match for 'operator==' (operand types are 'const CustomType' and 'const CustomType')
if (std::find(items.begin(), items.end(), value) != items.end()) {
std::cout << "Inexact!" << std::endl;
}
}
return 0;
}
Most suggestions for adding operators involve some sort of base class with operator overloads or a pattern similar to using namespace std::rel_ops
(which also fails argument-dependent lookup). I am not sure a base class would help for this problem and ideally I would want to use this solution on classes I don't own (and can't modify).
I could use explicit functions and types for my data structures and algorithms:
struct ExactlyEqualPredicate{
auto operator()(CustomType const& lhs, CustomType const& rhs) const -> bool {
return lhs.value == rhs.value;
}
};
struct AlmostEqualComparator{
CustomType value;
auto operator()(CustomType const& other) const -> bool {
return std::abs(value.value == other.value) < 1e-2;
}
};
auto main() -> int {
auto const items = std::vector<CustomType>{{1.0/3.0}};
auto const value = CustomType{0.3333};
if (std::find_if(items.begin(), items.end(), AlmostEqualComparator{value}) != items.end()) {
std::cout << "Inexact!" << std::endl;
}
auto custom_map = std::unordered_map<CustomType,
std::string,
std::hash<CustomType>,
ExactlyEqualPredicate>{
{CustomType{0.3333}, "Exact!"},
};
if (auto iter = custom_map.find(value); iter != custom_map.end()) {
std::cout << iter->second << std::endl;
}
return 0;
}
but this becomes quite verbose and breaks down when I have nested containers (std::vector<std::vector<CustomType>>
) or other complex structures. Template classes seem to have a couple extra rules for argument-dependent lookup but a nice way to accomplish this with templates was not immediately clear to me.
The goal is to keep the equality comparison explicit (exact comparison vs approximate comparison) at the call site without making usage of external functions and std
algorithms overly difficult and/or verbose. Is there a solution I'm missing or some black magic I haven't thought of that might make this possible? I currently use C++17 in my codebase.
Thanks!