如何用 C++ concept 要求同时存在 const 和非 const 成员函数重载?
结论:用成员函数指针类型分别检查两个重载
对于示例中的 void foo() 和 void foo() const,可以在 requires 中分别检查能否取得对应类型的成员函数指针:
template <typename T>
concept HasNonConstFoo = requires {
static_cast<void (T::*)()>(&T::foo);
};
template <typename T>
concept HasConstFoo = requires {
static_cast<void (T::*)() const>(&T::foo);
};
template <typename T>
concept Both = HasNonConstFoo<T> && HasConstFoo<T>;
void (T::*)() 是非 const 成员函数指针类型,void (T::*)() const 是 const 成员函数指针类型。两项表达式都合法时,Both<T> 才为 true。
为什么原来的检查会接受 ConstOnly?
原来的约束检查的是调用表达式:
{ non_const_val.foo() } -> std::same_as<void>;
这个检查只能说明,通过该对象调用 foo() 是合法的,且返回类型为 void。非 const 对象也可以调用 const 成员函数,因此 ConstOnly 同样满足这一要求。
为 &T::foo 指定成员函数指针的目标类型后,编译器会按该类型选择匹配的重载。两个重载的 const 限定不同,不能相互替代;如果只有 const 重载,非 const 的检查就会失败。具体规则见 C++ 的重载集合取地址规则。
给原来的检查加上 !HasConstFoo<T> 也不可行:它会排除同时具有两个重载的 MyClass。
完整示例
示例使用 C++20 concepts,并用 static_assert 检查三个类型是否符合预期:
#include <cstdio>
template <typename T>
concept HasNonConstFoo = requires {
static_cast<void (T::*)()>(&T::foo);
};
template <typename T>
concept HasConstFoo = requires {
static_cast<void (T::*)() const>(&T::foo);
};
template <typename T>
concept Both = HasNonConstFoo<T> && HasConstFoo<T>;
struct MyClass {
void foo() {
std::puts("Non-const foo()");
}
void foo() const {
std::puts("Const foo()");
}
};
struct NonConstOnly {
void foo() {}
};
struct ConstOnly {
void foo() const {}
};
static_assert(Both<MyClass>);
static_assert(!Both<NonConstOnly>);
static_assert(!Both<ConstOnly>);
template <Both T>
void call_foo(T& obj) {
obj.foo();
const T& const_obj = obj;
const_obj.foo();
}
int main() {
MyClass mc;
call_foo(mc);
// NonConstOnly nco;
// call_foo(nco); // 不满足 Both
// ConstOnly co;
// call_foo(co); // 不满足 Both
}
这里用 <cstdio> 声明 std::puts。新约束没有使用 std::same_as,因此示例无需包含 <concepts>。
使用时需要明确的边界
这段约束检查的是返回 void、参数列表为空且没有 ref-qualifier 的普通成员函数。如果目标声明为 void foo() & 和 void foo() const &,成员函数指针类型也要相应调整:
static_cast<void (T::*)() &>(&T::foo);
static_cast<void (T::*)() const &>(&T::foo);
带默认实参的 void foo(int = 0) 可以通过 obj.foo() 调用,但它的参数列表并不为空,所以不符合这里的要求。成员函数还必须在约束所在位置可访问,且能够取地址。
这些约束并非对声明文本的严格检查。可匹配的成员函数模板特化也可能满足要求;目标指针类型没有写 noexcept,也可能接受 noexcept 成员函数。某些继承而来的成员函数同样可以通过检查,因此不能据此断言两个重载都由 T 自己声明。相关转换受成员指针转换规则限制。
备注:内容仅供参考。
