Tamás Szelei
Tamás Szelei

Reputation: 23921

Should concepts fail to compile when an invalid expression is used?

I'm trying to implement a compact way of detecting if a free function is available at compile time (I'm using std::max as an example). I came up with this:

#include <stdio.h>  
#include <algorithm>           // (1)

namespace std { struct max; }  // (2)

template<typename A>
concept bool have_std_max = requires(A const& a1, A const& a2) {
  { std::max(a1, a2) }
};

template <typename A> 
constexpr A const &my_max(A const &a1, A const &a2) {
  if constexpr(have_std_max<A>) {
    return std::max(a1, a2);
  }
  else {
    return (a1 > a2) ? a1 : a2;
  }
}

int main() {
  int x = 5, y = 6;
  return my_max(x, y);
}

If I comment out (1), the detection works and my code uses the constexpr else branch (see in Compiler Explorer). However, if I comment out both (1) and (2), this code will fail to compile, because the name std::max is unknown to the compiler. Shouldn't the concept simply return false in this case? Is there a way to implement something similar without having to declare a dummy max?

Upvotes: 3

Views: 315

Answers (1)

Luc Danton
Luc Danton

Reputation: 35449

The template system of C++ is never an excuse to write incorrect code. Templates only offer leeway to so-called dependent types and expressions. (A long topic of its own which is studied in more depth in this answer.) For our purposes, a qualified name of the form std::max does not involve anything dependent so it must be correct where it appears. In turn, that means the look-up must succeed.

You were on the right track by trying to add a max declaration. By doing this, the non-dependent qualified name always successfully finds a declaration. Meanwhile, the overall expression remains dependent (by virtue of involving a0 and a1). All that remains is to avoid polluting the std namespace, which we aren't allowed to do:

#include <algorithm>

namespace dirty_tricks {

namespace max_fallback {

// non-deducible on purpose
template<typename Dummy>
void max() = delete;

} // max_fallback

namespace lookup {

// order of directives is not significant
using namespace std;
using namespace max_fallback;

} // lookup

} // dirty_tricks

template<typename Val>
concept bool have_std_max = requires(Val arg) {
    // N.B. qualified call to avoid ADL
    dirty_tricks::lookup::max(arg, arg);
};

(When testing the code by removing the <algorithm> include make sure the std namespace is still declared, or the using directive might fail. As seen in this Coliru demo.)

Now dirty_tricks::lookup::max either finds both std::max and dirty_tricks::max_fallback::max or the latter alone, but it can't fail. We also make sure that our own max overload cannot result in a valid expression by deleting it (and otherwise valid calls would look very different anyway).

Upvotes: 2

Related Questions