Is it possible to specify different C++ `operator==` functionality depending on the context at the call site?

Viewed 72

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!

1 Answers

You have already enumerated the plausible, flawed approaches with the exception of wrapper types that define their own comparisons. They might store pointers or references to the underlying objects to avoid copying, although even that would not allow algorithms to interpret containers differently. For that, the modern solution is to use Ranges (perhaps as included in C++20) with its composable transformations and projections (much like in your last approach).

Related