2

I want to write a proxy class, that takes a template value and can be compared to any class that the template can be compared to.

template <class T>
class Proxy {
public:
  Proxy(T value) : _value(value) {}

  template <class U> // this should exist only if the T == U operator is defined
  bool operator==(U const& other) const  { return _value == other; }

  template <class U> // this should exist only if U == T is defined
  friend bool operator==(U const& first, Proxy<T> const& second) const  { return first == second._value; }

private:
  T _value;
};

for example, since this is legal code:

bool compare(std::string first, std::string_view second) {
  return first == second;
}

I want this to be legal, too:

bool compare(std::string first, Proxy<std::string_view> second) {
  return first == second;
}

but just to clarify, this should work for any classes that can be compared, or can be implicitly converted in order to be compared. Can I define a template conditional that will check for either case?

nihohit
  • 498
  • 3
  • 23
  • [`std::is_same`](https://en.cppreference.com/w/cpp/types/is_same) should be helpful, in combination with [`std::enable_if`](https://en.cppreference.com/w/cpp/types/enable_if). – πάντα ῥεῖ Aug 14 '19 at 07:43
  • Just to be clear - the answer that this duplicates doesn't seem to be for a generic case, but specifically for 'char'. Can it be expanded by just adding another template argument, instead of char? – nihohit Aug 14 '19 at 07:57
  • @Max OK, I reopened the question, feel free to write an answer. – πάντα ῥεῖ Aug 14 '19 at 08:03

1 Answers1

5

Since your criteria is essentially whether or not an expression like _value == other is well-formed, you can just rely on expression SFINAE to test it.

  template <class U> // this should exist only if the T == U operator is defined
  auto operator==(U const& other) const -> decltype(_value == other)
  { return _value == other; }

  template <class U, std::enable_if_t<!std::is_same<U, Proxy>::value, int> = 0> // this should exist only if U == T is defined
  friend auto operator==(U const& first, Proxy const& second) -> decltype(first == second._value)
  { return first == second._value; }

It may not be very DRY, since we need to repeat the expression twice, but it's a fairly simple way to do SFINAE, which is a major plus.

The other thing to note is that we do not want the second overload to be considered recursively, which may happen upon comparison of two proxies. So we need another SFINAE condition, spelled out the old fashioned way with enable_if, to discard the overload when U is a Proxy. This relies on the C++14 feature whereby substitutions are checked in declaration order. Putting it first prevents the recursion in first == second._value.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
  • Any comments on [the order of `_value` declaration` and the operator mattering](https://godbolt.org/z/2r284E) ([compare](https://godbolt.org/z/59KeLZ))? That took me by surprise at least... I could ask a new question though. – Max Langhof Aug 14 '19 at 08:16
  • 1
    Also, the code still has an error: You take `T` as the second argument and then do `second._value`. Shouldn't you take `Proxy` as the second argument? – Max Langhof Aug 14 '19 at 08:19
  • @MaxLanghof - I'm called into a meeting. I have every intention of fixing the OP's example in a bit. Sorry for the radio silence. – StoryTeller - Unslander Monica Aug 14 '19 at 08:19
  • BTW, I see that in c++17 the decltype is unnecessary, and this is sufficient: template auto operator==(U const& other) const { return _value == other; } – nihohit Aug 14 '19 at 08:25
  • 1
    @nihohit - You won't get SFINAE if you dispense with the decltype, it will be hard error – StoryTeller - Unslander Monica Aug 14 '19 at 08:33
  • 1
    @nihohit It should still be necessary, because you don't want the `operator==` to be considered available if its instantiation leads to a hard error. That may be fine in the immediate moment ("you tried to compare a `Proxy` to something it couldn't be compared to and got an error, that's good"), but you shouldn't do that. Your operator should itself be SFINAE-friendly (and I believe there are other contexts where this operator would be implicitly instantiated where you will then get an error even though you shouldn't). – Max Langhof Aug 14 '19 at 08:34
  • @MaxLanghof - Fleshed it out. It's not as short and sweet as one would like anymore, but at least it works. – StoryTeller - Unslander Monica Aug 14 '19 at 08:43