How can I define a C++ constraint that involves a namespace-specified function? Example:
template<typename T>
concept C = requires(T t) {
{ N::foo(t) } -> std::same_as<void>;
};
struct S {};
namespace N {
void foo(S) {}
}
static_assert(C<S>);
The requirements are that S
must be in the global namespace, whereas foo
must be within N
.
Conceptually (pun intended), S
does fulfil concept C
. But the code is not compiling because N::
does not make sense to the compiler when the concept is defined. And of course, fwd-declaring void foo(S)
in the namespace N
before the concept definition is not an option, because I can't know type S
in advance. What am I missing here?
EDIT: note that concept C
can belong to whatever namespace. I've tried to define it within N
, but that doesn't help.
In C++, name lookup must succeed at the point of definition even inside a requires
expression or when defining a concept. This means if you write:
template<typename T>
concept IsValid = requires(T t) {
{ N::foo(t) } -> std::same_as<void>;
};
then the name N
must be visible before the concept is defined. Otherwise, the compiler will emit an error like “N
is not declared.”
This isn't a limitation specific to concepts, it's a fundamental rule of how C++ performs name lookup during template definition. Unlike SFINAE (Substitution Failure Is Not An Error), concepts and requires
expressions perform immediate (eager) name lookup, not deferred instantiation-time lookup.
Below are several valid approaches to work around this limitation, depending on the specific constraints of your use case, which are :
S
is declared in the global namespace
foo
is defined inside namespace N
The concept must be defined before namespace N
is introduced
Each solution illustrates a different technique to work within these constraints.
1. Concepts + ADL (Argument-Dependent Lookup)
#include <concepts>
#include <utility>
template<typename T>
concept C = std::same_as<decltype(foo(std::declval<T>())), void>;
struct S {};
namespace N {
void foo(S) {} // Must return void
}
using N::foo;
static_assert(C<S>);
Note: We use using N::foo;
before the static_assert
to bring foo
into the same namespace as S
. This enables ADL to find foo
correctly since S
is in the global namespace and foo
is in N
.
2. Concepts + Traits + Explicit Qualification
#include <type_traits>
#include <concepts>
template<typename T>
struct foo_traits {};
template<typename T>
concept C = std::same_as<typename foo_traits<T>::result_type, void>;
struct S {};
namespace N {
void foo(S) {}
}
template<>
struct foo_traits<S> {
using result_type = decltype(N::foo(S{}));
};
static_assert(C<S>);
Note: The trait is specialized after N::foo
is defined, and the concept only uses the trait. This avoids the name lookup issue at concept definition time.
3. Traits + SFINAE + ADL
#include <type_traits>
#include <utility>
template<typename, typename = void>
struct foo_traits : std::false_type {};
template<typename T>
struct foo_traits<T, std::enable_if_t<std::is_same_v<decltype(foo(std::declval<T>())), void>>> : std::true_type {};
template<typename T>
inline constexpr bool C = foo_traits<T>::value;
struct S {};
namespace N {
void foo(S) {}
}
using N::foo;
static_assert(C<S>);
Note: This version uses SFINAE to delay substitution, allowing name lookup to succeed only if foo(t)
is valid. As in example 1, using N::foo;
ensures foo
is visible to unqualified lookup via ADL.
Using a Deleted foo
Declaration
A common workaround is to declare foo
as a deleted function before defining the concept, then rely on overload resolution to select a valid foo
later. However, this technique can have pitfalls due to unexpected candidate selection and surprising name lookup behavior. See this related question for more details:
Function selection finding unexpected candidate when using namespaces