PAYATHON 2026

如何用 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 自己声明。相关转换受成员指针转换规则限制。

备注:内容仅供参考。