Search code examples
c++language-lawyercomparison-operators

Could removing "using namespace std::rel_ops" change behavior?


I recently found that a large project has a "using namespace std::rel_ops;" in a frequently included header file, and in the global namespace. Ouch.

Specifically, it caused an issue because these two function template declarations are ambiguous:

namespace std::rel_ops {
  template <class T>
  bool operator!=(const T&, const T&);
}
namespace std {
  template <class... TTypes, class... UTypes>
  constexpr bool operator!=(const tuple<TTypes...>&, const tuple<UTypes...>&);
}

so my attempt to use an expression similar to std::tie(a.m1, a.m2, a.m3) != std::tie(b.m1, b.m2, b.m3) was ill-formed.

So the plan is to delete the using namespace std::rel_ops;, then fix the compiler errors which result, probably by defining more specific comparison operator functions. But I want to also go through the exercise of evaluating whether it would be possible this change could change the meaning of some code hidden somewhere else in this large project, without causing a compiler error.

In what conditions, if any, could two C++ programs with and without a directive using namespace std::rel_ops; differ in behavior, given that neither is ill-formed with a required diagnostic?

I suspect it would require another comparison operator function template which is less specialized than one in std::rel_ops, and which has different effective behavior from the std::rel_ops definition. Both conditions seem quite unlikely in a real project, and even less likely considered together.


Solution

  • Given a pair of programs with and without using namespace std::rel_ops; which don't violate a rule requiring a diagnostic, a difference in behavior can only be due to overload resolution between a member of rel_ops and another function or function template which is a worse overload in some context with both declarations viable. The overload resolution context could be:

    • a binary comparison expression, like E1 != E2
    • an explicit call using an operator-function-id as function name, like operator!=(E1, E2)
    • a use of operator-function-id or &operator-function-id as an initializer for a pointer to function or reference to function, in one of the contexts listed in [over.over]/1, like static_cast<bool(*)(const std::string&, const std::string&)>

    It's actually not that hard for another function or function template specialization to be a worse overload than a member of std::rel_ops. Examples include:

    The other function or template specialization requires a user-defined conversion.

    class A {};
    bool operator==(const A&, const A&);
    
    class B {
    public:
        B(A);
    };
    bool operator==(const B&, const B&);
    bool operator!=(const B&, const B&);
    
    void test1() {
        // With using-directive, selects std::rel_ops::operator!=<A>(const A&, const A&).
        // Without, selects operator!=(const B&, const B&).
        A{} != A{};
    }
    
    class C {
       operator int() const;
    };
    bool operator==(const C&, const C&);
    
    void test2() {
        // With using-directive, selects std::rel_ops::operator!=<C>.
        // Without, selects the built-in != via converting both arguments to int.
        C{} != C{};
    

    The other function or template specialization requires a derived-to-base "Conversion" ([over.best.ics]/6).

    class D {};
    bool operator==(const D&, const D&);
    bool operator!=(const D&, const D&);
    
    class E : public D {};
    bool operator==(const E&, const E&);
    
    void test3() {
        // With using-directive, selects std::rel_ops::operator!=<E>.
        // Without, selects operator!=(const D&, const D&).
        E{} != E{};
    }
    

    The other function or template specialization has an rvalue reference parameter type.

    class F {};
    bool operator==(F&&, F&&);
    
    void test4() {
        // With using-directive, selects std::rel_ops::operator!=<F>.
        // Without, selects operator!=(F&&, F&&).
        F{} != F{};
    }
    

    The other function is a specialization of a less-specialized function template.

    namespace N1 {
    
    class A{};
    bool operator==(const A&, const A&);
    
    template <typename T1, typename T2>
    bool operator!=(const T1&, const T2&);
    
    }
    
    void test5() {
        // With using-directive, selects std::rel_ops::operator!=<N1::A>.
        // Without, selects N1::operator!=<N1::A,N1::A>.
        N1::A{} != N1::A{};
    }
    
    namespace N2 {
    
    class B{};
    bool operator==(const B&, const B&);
    
    template <typename T>
    bool operator!=(T&, T&);
    
    }
    
    void test6() {
        // With using-directive, selects std::rel_ops::operator!=<N2::B>.
        // Without, selects operator!=<const N2::B>.
        const N2::B b1;
        const N2::B b2;
        b1 != b2;
    }
    

    Other categories and examples are possible, but that more than makes the point.

    As far as practical concerns, it's unlikely any of the comparison operator names declared in std::rel_ops would be implemented to give results very different from the rel_ops definition for the same type, given that the related operator< or operator== is also defined. It might make a difference if "invalid" or special values have special treatment, like how for floating point types a <= b is not equivalent to !(b < a) when at least one operand is a NaN value. But the cases involving an implicit conversion to a different type could fairly easily result in different behavior. After a derived-to-base Conversion, any information in derived data members will very likely be ignored. After a converting constructor or conversion function, the comparison is on values of an entirely different type, which supposedly somehow represent the original arguments but might not represent their full functional identities.

    (So for the original motivation, I decided it's worth using a static analysis tool to find all the places in the existing code naming a member of std::rel_ops, to help check for unintended changes in meaning not caught by just compiling.)