C++20 Concepts: How to Require a Static Member Function in C++
Understanding the Issue: Why Concept Checks Can Fail Silently
When working with C++20 concepts, checking for the existence of a static member function with a specific name and signature is a common requirement. However, developers often encounter a subtle gotcha where their concept checks evaluate to true regardless of whether the class actually contains the static function.
The Cause of the Always-True Behavior
Consider the following snippet from the original question:
export template<typename T, typename... Args>
concept require_static_function_name = requires(Args... args) {
{ T::something(args...) };
};
bool has_static = requires { require_static_function_name<MyClass>; }; // Always returns true!
The primary bug isn't inside the concept definition itself, but in how it is tested. The expression requires { require_static_function_name<MyClass>; } uses an anonymous requires expression that tests whether require_static_function_name<MyClass> is a syntactically valid expression.
Because require_static_function_name<MyClass> is a valid boolean concept-id (which evaluates to false at compile time), the syntax check itself succeeds! Thus, the nested requires block evaluates to true because evaluating the concept is a valid operation, even though the concept's result is false.
How to Properly Evaluate a Concept
To check the actual boolean result of a concept, evaluate it directly as a boolean value without wrapping it in an unconstrained requires block:
// Correct way to evaluate a concept value:
constexpr bool has_static = require_static_function_name<MyClass>;
If you want to use a nested requires expression inside another constraint, use the requires requires syntax (a nested requirement clause):
// Inside another requires block or template constraint:
requires require_static_function_name<MyClass>
Refining the Static Function Concept Definition
To make your concept robust, flexible, and capable of handling perfect forwarding, rewrite the concept as follows:
#include <utility>
template<typename T, typename... Args>
concept HasStaticSomething = requires(Args&&... args) {
{ T::something(std::forward<Args>(args)...) };
};
Complete Working Example
#include <iostream>
#include <utility>
template<typename T, typename... Args>
concept HasStaticSomething = requires(Args&&... args) {
{ T::something(std::forward<Args>(args)...) };
};
struct ValidClass {
static void something(int x) {
std::cout << "Called with: " << x << '\n';
}
};
struct InvalidClass {
// No 'something' function defined
};
int main() {
static_assert(HasStaticSomething<ValidClass, int>, "ValidClass should satisfy the concept");
static_assert(!HasStaticSomething<InvalidClass, int>, "InvalidClass should NOT satisfy the concept");
std::cout << "Concept assertions passed successfully!\n";
return 0;
}
Summary & Best Practices
- Avoid syntax-checking concepts: Do not wrap concept-ids inside a plain
requires { ... }block; evaluate the concept directly. - Use Perfect Forwarding: Use rvalue references
Args&&...andstd::forwardinside concept expressions to properly match argument types. - Use static_assert for Verification: Verify your concepts using
static_assertduring compile-time testing rather than runtime variables.