C++20 Concepts:是否有更易用的函数参数约束定义方式?
我开始用C++20 Concepts定义模板类“接口”,它能生成友好的编译错误,但定义时很难平衡易用性和可读性。比如我要定义一个IFoo Concept,静态检查类是否有三个输入/返回类型完全匹配的函数,示例类如下:
struct Foo { void bar(uint32_t a, uint16_t b, uint8_t c) {} int32_t baz(int32_t d, int16_t e, uint8_t f) {return 0;} void bax(char g, uint8_t *h, uint32_t *i) {} };
正确定义后static_assert(IFoo<Foo>);要能编译通过。目前我有两种实现方式:
方式一:单Concept声明所有参数
满足需求但可读性差,得手动关联参数类型和函数输入:
template<typename T> concept IFoo = requires(T ifoo, uint32_t a, uint16_t b, uint8_t c, int32_t d, int16_t e, uint8_t f, char g, uint8_t *h, uint32_t *i) { {ifoo.bar(a, b, c)} -> std::same_as<void>; {ifoo.baz(d, e, f)} -> std::same_as<int32_t>; {ifoo.bax(g, h, i)} -> std::same_as<void>; };
方式二:拆分单个函数Concept再组合
可读性提升但冗余代码多:
template<typename T> concept IFoo_bar = requires(T ifoo, uint32_t a, uint16_t b, uint8_t c) { {ifoo.bar(a, b, c)} -> std::same_as<void>; }; template<typename T> concept IFoo_baz = requires(T ifoo, int32_t d, int16_t e, uint8_t f) { {ifoo.baz(d, e, f)} -> std::same_as<int32_t>; }; template<typename T> concept IFoo_bax = requires(T ifoo, char g, uint8_t *h, uint32_t *i) { {ifoo.bax(g, h, i)} -> std::same_as<void>; }; template<typename T> concept IFoo = requires { requires IFoo_bar<T>; requires IFoo_baz<T>; requires IFoo_bax<T>; };
我理想的写法是嵌套require块,但这种写法无法编译:
template<typename T> concept IFoo = requires(T ifoo) { requires(uint32_t a, uint16_t b, uint8_t c) { {ifoo.bar(a, b, c)} -> std::same_as<void>; }; requires(int32_t d, int16_t e, uint8_t f) { {ifoo.baz(d, e, f)} -> std::same_as<int32_t>; }; requires(char g, uint8_t *h, uint32_t *i) { {ifoo.bax(g, h, i)} -> std::same_as<void>; }; };
想问:我是不是漏了C++20 Concepts的特性?还是只能用现有两种方式?有没有更简便的实现方法?
首先明确:C++20不支持你写的那种嵌套requires块语法,你的理想写法不符合标准,所以无法编译。不过有几种更简洁的方案,能兼顾可读性和代码精简:
方案1:双重requires表达式内联检查
不需要拆分多个Concept,而是把每个函数的检查用独立的requires(...) { ... }表达式嵌套在主Concept里,这是标准支持的写法,既保留了方式二的逻辑隔离性,又避免了额外定义多个Concept的冗余:
#include <concepts> #include <cstdint> template<typename T> concept IFoo = requires(T ifoo) { // 单独检查bar函数 requires requires(uint32_t a, uint16_t b, uint8_t c) { {ifoo.bar(a, b, c)} -> std::same_as<void>; }; // 单独检查baz函数 requires requires(int32_t d, int16_t e, uint8_t f) { {ifoo.baz(d, e, f)} -> std::same_as<int32_t>; }; // 单独检查bax函数 requires requires(char g, uint8_t *h, uint32_t *i) { {ifoo.bax(g, h, i)} -> std::same_as<void>; }; };
这里的语法是:外层requires用来引入一个布尔表达式(即内层的requires表达式),内层requires(参数列表) {约束}是标准的requires表达式,专门用来检查函数的可用性和类型匹配。这种写法把每个函数的检查逻辑清晰分隔,可读性接近方式二,但代码更紧凑。
方案2:用std::declval简化参数声明
如果只需要验证函数参数/返回类型的匹配性,不需要具体的参数变量名,可以用std::declval直接生成对应类型的右值,省略参数声明步骤,进一步精简代码:
#include <concepts> #include <cstdint> #include <utility> // 包含std::declval template<typename T> concept IFoo = requires(T ifoo) { {ifoo.bar(std::declval<uint32_t>(), std::declval<uint16_t>(), std::declval<uint8_t>())} -> std::same_as<void>; {ifoo.baz(std::declval<int32_t>(), std::declval<int16_t>(), std::declval<uint8_t>())} -> std::same_as<int32_t>; {ifoo.bax(std::declval<char>(), std::declval<uint8_t*>(), std::declval<uint32_t*>())} -> std::same_as<void>; };
这种写法代码量最少,适合简单场景。如果函数参数是左值引用,只需调整std::declval的模板参数(比如std::declval<uint8_t&>())即可适配。
方案3:模板变量辅助逻辑复用
如果需要在多个地方复用单个函数的检查逻辑,可以用constexpr模板变量代替独立Concept,语法比方式二更简洁:
#include <concepts> #include <cstdint> #include <utility> template<typename T> constexpr bool has_bar = requires(T t) { requires requires(uint32_t a, uint16_t b, uint8_t c) { {t.bar(a, b, c)} -> std::same_as<void>; }; }; template<typename T> constexpr bool has_baz = requires(T t) { requires requires(int32_t d, int16_t e, uint8_t f) { {t.baz(d, e, f)} -> std::same_as<int32_t>; }; }; template<typename T> constexpr bool has_bax = requires(T t) { requires requires(char g, uint8_t *h, uint32_t *i) { {t.bax(g, h, i)} -> std::same_as<void>; }; }; template<typename T> concept IFoo = has_bar<T> && has_baz<T> && has_bax<T>;
这种写法同样实现了逻辑隔离,且模板变量的复用性更强,适合复杂场景下的代码复用。
总结
- C++20不支持你期望的嵌套requires块语法,该写法不符合标准。
- 优先推荐方案1:双重requires表达式,兼顾可读性和代码紧凑性,是最接近你理想写法的标准实现。
- 若追求极致精简,可选用方案2:结合
std::declval省略参数声明。
内容的提问来源于stack exchange,提问作者haydenridd

