Is there a hack to defer non-ADL name lookup to the point where a C++ concept is used?

Viewed 160

I'm trying to use a concept that applies to both built-in and user-defined types. In my setting, it is inconvenient to forward-declare certain functions (because the functions depend indirectly on the concepts causing my problem).

My problem is that concepts don't recognize functions that were not previously declared if those functions have arguments of built-in type. Here's a minimal non-working example:

struct S {};

// void func(bool);  // Works if this line uncommented

template<typename T> concept has_func =
  requires(T t) { func(t); };

void func(S) {}
static_assert(has_func<S>); // OK

void func(bool) {}
static_assert(has_func<bool>); // Error

My first question: Why exactly this is happening? I suspect it has to do with ADL, because even though everything is in the global namespace, the global namespace is still a namespace, and bool, being built-in, is not technically in that namespace. So somehow I need to express the concept in terms of some user-defined type that captures the behavior of bool.

My second question: What is a good workaround to defer name lookup on my function? My fallback (since I can't forward-declare in my actual code) is to redefine the concept as std::same_as<T, bool> || .... However, I'm wondering if there's a more general hack to defer name lookup to the point where the concept is used. I've tried things like using requires(std::type_identity_t<T> t), but it doesn't help.

update:

I don't have a hack to work around the problem, but I understand that it is being caused by the dependent name lookup rules. Specifically:

  • non-ADL lookup examines function declarations with external linkage that are visible from the template definition context
  • ADL examines function declarations with external linkage that are visible from either the template definition context or the template instantiation context

So the two things I've tried are:

  1. Calling some other function dofunc with an extra argument of a type that triggers ADL. That sort of works, except this function needs to call func, so the placement of the dofunc function now becomes tricky for func to be in scope. It doesn't really improve things.

  2. Using some kind of proxy type with an operator bool, but this doesn't work because it doesn't trigger ADL of the function I want to call.

It may be that what I want to do is impossible on purpose, to avoid situations where the concept means something different in different contexts. Of course, with ADL the concept still might be different in different contexts, depending on what is in scope at the time the concept is used, so I still don't really get the justification for this restriction.

1 Answers

Yes, static_assert(has_func<bool>) in your initial code does not work, because bool is a build-in type, so no argument-dependent lookup (ADL) is performed and func(bool) must be visible by the concept definition.

I think you can utilize a proxy type A<T> and ADL to make a similar concept accepting build-in types:

template<class T>
struct A{
    operator T() const;
};

template<typename T> concept has_func =
  requires { func(A<T>{}); };

// Some tests:
void func(bool&) {}
static_assert(has_func<bool&>);
static_assert(!has_func<int>); // func(1) is not valid

void func(bool) {}
static_assert(has_func<bool>);

struct S{};
void func(S) {}
static_assert(has_func<S>);

Demo: https://gcc.godbolt.org/z/bGY8zGes9

Related