如何实现适配实参值类别(拷贝/移动语义)的准确可调用性概念?
如何实现适配实参值类别(拷贝/移动语义)的准确可调用性概念?
你的困惑完全合理,std::invocable的行为确实容易让人误解——它的检查逻辑是“能否用std::declval<Args>()生成的实参调用目标”,而不是“能否用任意值类别的Args类型实参调用”。我们先拆解问题的根源,再给出符合你需求的解决方案。
为什么std::invocable会有这样的行为?
std::invocable<F, Args...>的核心逻辑依赖于std::declval<T>()生成实参,这个函数的返回规则是:
- 如果
T是对象类型(非引用),返回T&&(右值引用); - 如果
T是引用类型,直接返回T(左值/右值引用)。
对应你的例子来看:
std::invocable<void(std::unique_ptr<int>), std::unique_ptr<int>>为true,因为std::declval<std::unique_ptr<int>>()生成的是右值引用,函数参数是值类型,可以通过移动构造接收这个右值(unique_ptr支持移动);- 但你实际传左值
p1时,会尝试拷贝构造函数参数,而unique_ptr禁用了拷贝,所以编译失败——这就是你觉得“不符合预期”的原因:std::invocable默认检查的是右值实参的调用可行性,而非左值。
你的测试代码格式化后如下:
#include <concepts> #include <memory> // 你原本预期这行应该成立,但std::invocable返回true // static_assert(!std::invocable<void(std::unique_ptr<int>), std::unique_ptr<int>>); static_assert(!std::invocable<void(std::unique_ptr<int>), std::unique_ptr<int>&>); static_assert(std::invocable<void(std::unique_ptr<int>), std::unique_ptr<int>&&>); void foo(std::unique_ptr<int>) {} int main() { [[maybe_unused]] std::unique_ptr<int> p1 = std::unique_ptr<int>(new int); [[maybe_unused]] std::unique_ptr<int>& r1 = p1; // 以下两行编译失败,因为unique_ptr不可拷贝 // foo(p1); // foo(r1); foo(std::unique_ptr<int>(new int)); // 右值可以正常调用 }
如何实现适配拷贝/移动语义的可调用性概念?
你需要的是根据实参的实际值类别来检查调用可行性,而不是默认用右值。可以通过两种方式实现:
1. 直接复用std::invocable,显式指定实参的引用类型
不需要自定义概念,只要给std::invocable传递对应值类别的实参类型即可:
- 检查左值实参的调用可行性:用
T&作为实参类型
这会检查“能否用左值引用的static_assert(!std::invocable<void(std::unique_ptr<int>), std::unique_ptr<int>&>);unique_ptr调用函数”,结果为false,完全符合你的预期。 - 检查右值实参的调用可行性:用
T或T&&作为实参类型static_assert(std::invocable<void(std::unique_ptr<int>), std::unique_ptr<int>>); static_assert(std::invocable<void(std::unique_ptr<int>), std::unique_ptr<int>&&>); - 检查任意值类别的实参都能调用:同时验证左值和右值
比如对于template <typename F, typename... Args> concept invocable_with_any_value_category = std::invocable<F, Args&...> && std::invocable<F, Args&&...>;void(const std::unique_ptr<int>&),这个概念会返回true,因为左值和右值都能绑定到const左值引用。
2. 自定义语义更直观的概念
如果你想封装更易读的概念,比如专门检查左值/右值实参的调用可行性:
#include <concepts> #include <memory> #include <utility> // 检查:能否用Args类型的左值实参调用F template <typename F, typename... Args> concept invocable_with_lvalues = requires { std::invoke(std::declval<F>(), std::declval<Args&>()...); }; // 检查:能否用Args类型的右值实参调用F template <typename F, typename... Args> concept invocable_with_rvalues = requires { std::invoke(std::declval<F>(), std::declval<Args&&>()...); }; // 检查:能否用任意值类别的Args类型实参调用F template <typename F, typename... Args> concept invocable_with_any_value_category = invocable_with_lvalues<F, Args...> && invocable_with_rvalues<F, Args...>;
用你的foo函数验证:
void foo(std::unique_ptr<int>) {} // 符合预期:左值实参无法调用(不能拷贝) static_assert(!invocable_with_lvalues<decltype(foo), std::unique_ptr<int>>); // 符合预期:右值实参可以调用(可以移动) static_assert(invocable_with_rvalues<decltype(foo), std::unique_ptr<int>>); // 符合预期:不是所有值类别都能调用 static_assert(!invocable_with_any_value_category<decltype(foo), std::unique_ptr<int>>);
总结
std::invocable本身没有“错误”,只是它的默认行为是检查右值实参的调用可行性。要适配拷贝/移动语义,只需要:
- 明确你要检查的是左值还是右值实参;
- 要么给
std::invocable传递对应的引用类型实参,要么自定义封装好的概念。
这样就能准确反映拷贝/移动语义下的调用可行性了。
内容来源于stack exchange
相关产品推荐
相关产品推荐

