Concept 是 C++20 对模板参数的命名约束。它解决了模板编程的核心痛点:错误信息晦涩、约束表达隐式、SFINAE 代码难以维护 。
核心语法 1 2 template <typename T>concept ConceptName = constraint_expression;
定义示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 template <typename T>concept Addable = requires (T a, T b) { a + b; };template <typename T>concept Numeric = std::integral<T> || std::floating_point<T>;template <typename T>concept Container = requires (T c) { typename T::value_type; c.begin (); c.end (); requires std::same_as<typename T::iterator, decltype (c.begin ())>; };
使用方式 四种等价写法 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 template <Addable T>T add (T a, T b) { return a + b; }template <typename T> requires Addable<T> T add (T a, T b) { return a + b; }template <typename T>T add (T a, T b) requires Addable<T> { return a + b; }Addable auto add (Addable auto a, Addable auto b) { return a + b; }
requires 表达式 requires 表达式有四种形式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 template <typename T>concept Example = requires (T t, T::value_type v) { t.size (); typename T::value_type; typename T::iterator; { t.begin () } -> std::same_as<typename T::iterator>; { t.empty () } -> std::convertible_to<bool >; { t.size () } noexcept -> std::same_as<size_t >; requires std::default_initializable<T>; requires std::copy_constructible<T>; };
复合要求语法 :{ expression } noexcept -> type-constraint;
noexcept 可选,要求表达式不抛异常-> type-constraint 要求表达式结果满足约束标准 Concept 分类 <concepts> 头文件提供四类约束:
类型关系 1 2 3 4 std::same_as<T, U> std::derived_from<D, B> std::convertible_to<From, To> std::common_with<T, U>
类型属性 1 2 3 4 std::integral<T> std::floating_point<T> std::is_pointer<T> std::is_array<T>
对象属性 1 2 3 std::regular<T> std::semiregular<T> std::trivially_copyable<T>
可调用 1 2 3 std::invocable<F, Args...> std::regular_invocable<F, Args...> std::predicate<F, Args...>
Subsumption(概念包含) Concept 支持重载决议时的包含关系:更严格的 concept 优先匹配。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 template <typename T>concept Integral = std::integral<T>;template <typename T>concept SignedIntegral = Integral<T> && std::is_signed_v<T>;template <Integral T>void process (T x) { std::print ("Integral\n" ); }template <SignedIntegral T>void process (T x) { std::print ("SignedIntegral\n" ); }process (42 ); process (42u );
规则 :当 concept A 的约束蕴含 concept B(A 更严格),则 A 包含 B。重载决议时,更严格的 concept 优先。
错误信息对比 SFINAE 时代的错误 (数百行模板展开):
1 2 3 4 5 error : no match for 'operator<<' ( operand types are 'std::ostream' { aka 'std::basic_ostream<char>' } and 'std::vector<int>' ) / usr/ include/ c++ / 13 / ostream: 108 : 7 : note: candidate: . . . / usr/ include/ c++ / 13 / ostream: 117 : 7 : note: candidate: . . . [ . . . 50 + lines of template instantiation backtrace . . . ]
Concept 时代的错误 :
1 2 3 4 error : cannot bind non- const lvalue reference of type 'int&' to an rvalue of type 'int' note: constraints not satisfied note: the required expression 't.size()' is invalid
编译器直接告诉你哪个约束失败,而不是展开整个模板实例化链。
与 SFINAE 对比 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 template <typename T, typename = std::enable_if_t <std::is_integral_v<T>>> T multiply (T a, T b) { return a * b; }template <std::integral T>T multiply (T a, T b) { return a * b; }template <typename T, typename = std::enable_if_t < std::is_integral_v<T> && std::is_copy_constructible_v<T>>>void func (T);template <std::integral T> requires std::copy_constructible<T> void func (T) ;
实际应用 约束迭代器类型 1 2 3 4 5 6 7 8 9 10 11 12 template <typename T>concept ForwardIterator = requires (T it) { { *it } -> std::same_as<typename T::reference>; { ++it } -> std::same_as<T&>; { it++ } -> std::same_as<T>; requires std::equality_comparable<T>; };template <ForwardIterator It>void advance (It& it, size_t n) { while (n--) ++it; }
约束可序列化类型 1 2 3 4 5 6 7 8 9 10 template <typename T>concept Serializable = requires (std::ostream& os, const T& obj) { { os << obj } -> std::same_as<std::ostream&>; };template <Serializable T>void save (const T& obj, const std::filesystem::path& file) { std::ofstream ofs (file) ; ofs << obj; }
约束数值计算 1 2 3 4 5 6 7 template <typename T>concept Arithmetic = std::integral<T> || std::floating_point<T>;template <Arithmetic T>constexpr T clamp (T value, T lo, T hi) { return value < lo ? lo : (value > hi ? hi : value); }
注意事项 1. Concept 是编译期约束
1 2 3 4 5 template <std::integral T>void func (T x) ;func (42 ); func (3.14 );
2. 约束不满足时的替代方案
1 2 3 4 5 6 7 8 template <typename T> requires std::integral<T> T abs (T x) { return x < 0 ? -x : x; }template <typename T> requires std::floating_point<T> T abs (T x) { return std::fabs (x); }
3. 避免过度约束
1 2 3 4 5 6 7 8 9 10 11 template <typename T>concept StrictContainer = requires (T c) { { c.size () } -> std::same_as<size_t >; };template <typename T>concept Container = requires (T c) { { c.size () } -> std::convertible_to<size_t >; };
4. Concept 与 auto 结合
1 2 3 4 5 6 void process (std::integral auto x) ;template <std::integral T>void process (T x) ;
与 Ranges 库结合 Ranges 库大量使用 concept 约束算法:
1 2 3 4 5 6 7 8 9 10 #include <ranges> #include <algorithm> std::vector<int > vec = {3 , 1 , 4 , 1 , 5 }; std::ranges::sort (vec); std::list<int > lst = {3 , 1 , 4 }; std::ranges::sort (std::views::take (vec, 3 ));
常用 Ranges concept :
Concept 要求 range有 begin() 和 end() sized_rangesize() 在常数时间返回random_access_range支持常数时间随机访问 view可移动、常数时间构造/析构
前置知识:typename 与嵌套类型 在模板中使用嵌套类型(如 T::iterator)时,必须使用 typename 关键字明确告诉编译器这是一个类型 而非静态成员。
为什么需要 typename 当编译器遇到 T::iterator 这样的语法时,无法确定 这是一个嵌套类型还是一个静态数据成员:
1 2 3 4 template <typename T>void process (T container) { T::iterator it; }
编译器默认假设这是静态数据成员。使用 typename 消除歧义:
1 2 3 4 template <typename T>void process (T container) { typename T::iterator it; }
必须使用 typename 的场景 场景 示例 声明依赖类型的嵌套类型变量 typename T::iterator it;类型别名声明 using ValueType = typename T::value_type;函数返回类型 typename T::value_type get();模板参数传递 std::same_as<typename T::iterator, It>Concept 的复合要求 { c.begin() } -> std::same_as<typename T::iterator>;
什么是嵌套类型 嵌套类型(Nested Type) 是指在类(或结构体)内部定义的类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 class Container {public : using value_type = int ; typedef int * pointer; struct iterator { }; enum class Status { Ok, Error }; }; Container::value_type x; Container::iterator it; Container::Status s;
与成员变量的区别 :
1 2 3 4 5 6 7 8 9 class Container {public : using value_type = int ; static int count; int size; }; Container::value_type x; Container::count = 10 ;
Concept 中的两种 typename 用法 在 requires 表达式中,typename T::name 有两种含义:
1. 类型要求(检查嵌套类型存在)
1 2 3 4 template <typename T>concept HasIterator = requires { typename T::iterator; };
2. 类型声明(使用嵌套类型)
1 2 3 4 5 template <typename T>concept Container = requires (T c) { { c.begin () } -> std::same_as<typename T::iterator>; };
不需要 typename 的情况 非依赖类型 (类型已知,不依赖模板参数):1 2 struct MyClass { using Inner = int ; }; MyClass::Inner x;
使用 auto 推导 (C++11 起):1 2 3 4 template <typename Container>void process (const Container& c) { auto it = c.begin (); }
典型编译错误 如果不使用 typename:
1 2 3 4 template <typename T>void bad (T& c) { T::iterator it = c.begin (); }
错误信息:
1 error: need 'typename' before 'T::iterator' because 'T' is a dependent scope
总结 特性 SFINAE Concept 语法 晦涩 清晰 错误信息 冗长 明确 组合性 困难 自然 重载决议 手动 自动(subsumption)
Concept 不是语法糖,而是模板元编程范式的转变:从"类型替换失败不是错误"到"类型约束显式表达"。当你写模板时,先问自己:这个类型需要满足什么条件?然后用 concept 表达它。