Search code examples
c++c++20function-pointersc++-conceptsoverload-resolution

Why aren't concept constraints considered contextual type information?


Consider:

void f(int);        // (1)
void f(int, int);   // (2)


auto pf1 = static_cast< void (*)(int) >(f);      // Ok, address of (1)
auto pf2 = static_cast< void (*)(int, int) >(f); // Ok, address of (2)


static_assert(std::invocable<decltype(pf1), int>);      // passed
static_assert(std::invocable<decltype(pf2), int, int>); // passed
static_assert(!std::invocable<decltype(pf2), int>);     // passed

Now, instead of explicit cast, I would like to use concepts to help the compiler doing overload resolution:

std::invocable<int> auto pf1 = f;  // Only (1) satisfies, there is no ambiguity

Except it doesn't compile. My question is why?

If the example was to work, we would be able to write:

std::string s;
std::ranges::transform(s, s.begin(), std::toupper);

Solution

  • The only effect that a type constraint on a variable declaration currently has is to implicitly static_assert that the type, deduced as if there was no type constraint, satisfies the constraints. It isn't specified to affect overload resolution or type deduction in any way.

    It wouldn't help for standard library functions like toupper either way, because most of them are not designated as addressable, meaning that you don't know what overload structure is used to implement them and so it is not guaranteed that you are able to take a pointer/reference to them in any situation. For example std::toupper might be implemented as a single function template instead of multiple overloads. Then simply checking the constraint is not enough to make what you want work. You would need to somehow make template argument deduction choose the set of template arguments that happen to make the function with an int. In general that is an undecidable problem.

    The only reliable way to pass toupper to a function is by wrapping it in a lambda. An explicit cast or ::toupper is also not guaranteed to work. Additionally, you want that lambda anyway, because as mentioned in the question comments, just calling toupper directly will cause undefined behavior for certain inputs.

    So, I have doubts that you would be able to come up with reasonable rules that could be added to the language to actually achieve the behavior that you want.

    I think it would be possible to add something that says that overloads of f are dropped in std::invocable<int> auto pf1 = f; if the overload (after template argument deduction) doesn't satisfy the type constraint. But that wouldn't help if f is implemented as template instead of multiple overloads and it also won't work for function parameters of the same form because type constraints on a function template are not associated with a single function parameter, but the whole function template.