Chris Usher Chris Usher - 4 days ago 5
C++ Question

How do I template overload an operator for a group of related classes without conflicting with standard library operators?

This seems like a fairly straight forward idea: I have a group of classes for which I should be able to write an operator, let's say subtraction, using basically the exact same code.

When trying to do this the "obvious" way, i.e:

template <typename T>
T operator-(T a, const T& b) {
return a -= b;
}


Then, on certain compilers, this seems to conflict with a subtraction operator for iterators (specifically it breaks on gcc 3.4.6 and Apple LLVM, while seemingly working fine on gcc version 4 or newer), and I get the following error:

error: use of overloaded operator '-' is ambiguous (with operand types 'std::__1::__wrap_iter<const double *>' and 'const_iterator'
(aka '__wrap_iter<const_pointer>'))


I also considered template overloading for just a base class from which all of the classes in the group would be derived from, but then since the first parameter is passed by value, I think information specific to the subclass would be lost during the copy.

Am I missing something obvious? Is there a way to do this that keeps all these compilers happy?

Edit: the solution should ideally work in C++03.

Answer

Use a base class, adl koenig operators, and sfinae.

namespace ops{
  struct subtract_support{
    template<class T,
      std::enable_if_t<std::is_base_of<subtract_support, T>{}, int> =0
    >
    friend T operator-( T lhs, T const& rhs ){
      lhs-=rhs;
      return lhs;
    }
  };
}

Now inheriting from ops::subtract_support causes - to work for your type. (Note the two-line body: that ensures the lhs is moved out of -, unlike your version in the OP).

Merely namespace restricting results in your - being found any template produced type where one of tge arguments comes from your namespace, and other unintended cases: adl operates on types whose template members come from a specific namespace.1

This trick lets you mark each type as using this technique at the point of declaration.

There are almost zero binary layout implications of this technique. But if you need something obscure like layout compatibility of prefixes a traits class may be required instead.


Now, this is a C++11 solution. Apparently the OP needs C++03. Well, the best way forward is to implement it as much like C++11 so code can be deleted when you upgrade your compiler.

enable_if can easily be written in C++03. is_base_of takes a few more lines:

namespace notstd{
  namespace details{
    template<class T, class U>
    struct base_test{
      typedef char no; // sizeof(1)
      struct yes { no unused[2]; }; // sizeof(2) or greater
      static yes test(T*); // overload if arg is convertible-to-T*
      static no test(...); // only picked if first overload fails
      // pass a `U*` to `test`.  If the result is `yes`, T
      // is a base of U.  Note that inaccessible bases might fail here(?)
      // but we are notstd, good enough.
      enum {value= (
        sizeof(yes)==sizeof(test((U*)0))
      )};
    };
  }
  template<class Base, class T>
  struct is_base_of{
    enum{value=details::base_test<Base,T>::value};
  };
  template<bool b, class T=void>
  struct enable_if {};
  template<class T>
  struct enable_if<true, T> {
    typedef T type;
  };
}

We also need to tweak the SFINAE in the template ADL operator for C++03 compliance:

namespace ops{
  struct subtract_support{
    template<class T>
    friend
    typename notstd::enable_if<notstd::is_base_of<subtract_support, T>::value, T>::type
    operator-( T lhs, T const& rhs ){
      lhs-=rhs;
      return lhs;
    }
  };
}

Live example.


1 As an example, if Foo is a type in the namespace with a greedy - template operator, then decltype(v0-v1) where v0 and v1 are vector<Foo> will be vector<Foo>. That is a false positive (it will not compile). But a vec3<Foo> (vector space of 3 Foo) with its own - would result in the same ambiguity.

Comments