跳转到内容

什么是概念(concepts)?怎么用?

SFINAE范式过于复杂且不优雅,C++20中引入了足以完全取代 SFINAE 的新语言特性:概念(concepts)。

概念用于为模板指定模板参数的约束条件。

引入

例如,我们想为“数字”类型(int、double、float等)编写一个计算两个数字的和的函数模板sum()。代码如下:

#include <iostream>
template <typename T>
T sum(const T& a, const T& b) {
return a + b;
}
int main() {
std::cout << sum(1, 2) << std::endl;
std::cout << sum(1.8, 2.7) << std::endl;
}

看起来似乎没有任何问题。但如果现在我将两个std::string字符串作为参数传给sum()会怎么样呢?

std::string a {"hello"};
std::string b {"world"};
std::cout << sum(a, b) << std::endl;

虽然没有考虑到参数为std::string字符串的情况,稍稍有点出乎意料,但程序仍然运行良好。

但假如我现在传给sum()的不是std::string字符串,而是const char*字符串,会怎么样呢?

const char* a {"hello"};
const char* b {"world"};
std::cout << sum(a, b) << std::endl;

程序编译出错了,并且出错信息不够直观,并不会直接告诉我们编译出错是因为我们传入了错误的类型,而是间接地说,const char* const不是运算符+的合法的操作数。

main.cpp: In instantiation of ‘T sum(const T&, const T&) [with T = const char*]’:
main.cpp:11:19: required from here
11 | std::cout << sum(a, b) << std::endl;
| ~~~^~~~~~
main.cpp:5:12: error: invalid operands of types ‘const char* constandconst char* const’ to binary ‘operator+
5 | return a + b;
| ~~^~~

因为这个程序很短,所以程序的出错原因还能比较快的找出来,但如果程序更大一些,就没那么容易了。

假如现在程序中有一个类Custom,我将类Custom的实例传给函数模板sum()又会发生什么呢?

#include <iostream>
class Custom {};
template <typename T>
T sum(const T& a, const T& b) {
return a + b;
}
int main() {
Custom a;
Custom b;
std::cout << sum(a, b) << std::endl;
}

程序编译出错,并且这次的错误信息足足有 265 行,要上下滚动窗口好久才能完整浏览一遍。因为很长,我不直接贴在文章中,可以打开这里查看。

虽然错误信息很长,但也能比较轻松的看出,错误信息是说,编译出错是因为类Custom没有重载运算符+。而不会直接说函数模板sum()不接受类型为Custom的参数。

从上面的例子可以看出,往函数模板sum()里传什么类型的参数都行,对类型没有任何限制,即便传入预料之外的类型的参数,程序也能接受,不过有可能可以编译成功,也有可能编译失败。并且编译出错信息很不直观。

有没有一种办法,可以让程序只能接受指定的一些类型的参数,如果传的参数是别的类型,直接失败,并且能够直接指出编译失败的原因就是传入了错误类型的参数?

这就是概念(concepts)的用途。

对于上面的例子,我们的设计针对的是数字类型,也就是整数(int、long、short等)或浮点数(float、double等)。可以像下面这样为函数模板sum()添加约束。

template <typename T>
requires std::integral<T> || std::floating_point<T>
T sum(const T& a, const T& b) {
return a + b;
}

上面使用的reqires关键字是概念(concepts)的语法的一种。新添加的requires表达式的意思是T需要满足“是整数”或“是浮点数”,否则就是不满足概念的条件约束。std::integralstd::floating_point是标准库<concepts>中的预定义概念,所有的预定义概念可以看这里这里

对于数字类型的参数,程序正常编译;如果传入了不支持的类型(比如自定义类Custom),将同样会编译出错,但错误信息短了不少,并且能直接知道错误原因是参数类型不满足条件约束:

main.cpp: In function ‘int main()’:
main.cpp:15:19: error: no matching function for call to ‘sum(Custom&, Custom&)’
15 | std::cout << sum(a, b) << std::endl;
| ~~~^~~~~~
main.cpp:8:3: note: candidate: ‘template<class T> requires (integral<T>) || (floating_point<T>) T sum(const T&, const T&)’
8 | T sum(const T& a, const T& b) {
| ^~~
main.cpp:8:3: note: template argument deduction/substitution failed:
main.cpp:8:3: note: constraints not satisfied
main.cpp: In substitution of ‘template<class T> requires (integral<T>) || (floating_point<T>) T sum(const T&, const T&) [with T = Custom]’:
main.cpp:15:19: required from here
main.cpp:8:3: note: 15 | std::cout << sum(a, b) << std::endl;
main.cpp:8:3: note: | ~~~^~~~~~
main.cpp:8:3: required by the constraints of ‘template<class T> requires (integral<T>) || (floating_point<T>) T sum(const T&, const T&)’
main.cpp:7:27: note: no operand of the disjunction is satisfied
7 | requires std::integral<T> || std::floating_point<T>
| ~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~
cc1plus: note: set ‘-fconcepts-diagnostics-depth=’ to at least 2 for more detail

概念(concepts)与类型特征(type traits)有什么区别和联系?

区别:C++规范允许概念与模板交互(而类型特征显然不能)。

如可以用概念代替模板声明语法中的typename关键字。

又如上面出现过的requires子句,可以出现在模板声明之后。

联系:类型特征可以作为requires子句的一部分。

观察概念的实现可以知道,概念的底层是通过类型特征实现的。如概念std::same_as的实现:

namespace detail
{
template< class T, class U >
concept SameHelper = std::is_same_v<T, U>;
}
template< class T, class U >
concept same_as = detail::SameHelper<T, U> && detail::SameHelper<U, T>;

使用概念(concepts)的方式

约束模板参数(constrained template parameters)

在模板参数列表中,可以用概念名称替换模板声明template <typename T>中的typename关键字。这表示,类型T必须满足概念中的约束条件。

例子 两个整数相加(无论是intshort还是long

#include <concepts>
#include <iostream>
template <std::integral T>
T sum(const T& a, const T& b) {
return a + b;
}
int main() {
int a{5};
int b{7};
std::cout << sum(a, b) << std::endl;
}

requires 子句

例子 两个整数相加(无论是intshort还是long),使用 requires 子句

#include <concepts>
#include <iostream>
// template <std::integral T>
// T sum(const T& a, const T& b) {
// return a + b;
// }
template <typename T>
requires std::integral<T>
T sum(const T& a, const T& b) {
return a + b;
}
int main() {
int a{5};
int b{7};
std::cout << sum(a, b) << std::endl;
}

文章开头的例子使用了 requires 子句,其中的约束条件是两个概念的组合(使用||,两个概念满足其中之一即可),这里把代码再贴一遍:

#include <concepts>
#include <iostream>
template <typename T>
requires std::integral<T> || std::floating_point<T>
T sum(const T& a, const T& b) {
return a + b;
}
int main() {
int a{5};
int b{7};
std::cout << sum(a, b) << std::endl;
}

例子 只接受指定类的子类,使用类型特征(type traits)

#include <type_traits>
class Player {};
class Monster {};
class Goblin : public Monster {};
class Rock {};
template <typename T>
requires std::is_base_of_v<Player, T> || std::is_base_of_v<Monster, T>
void func(T Character) {}
int main() {
// fine
func(Player{});
func(Monster{});
func(Goblin{});
// error
// func(Rock{});
}

传入函数func()的参数的类型,必须派生自类Player或类Monster。这个例子中使用了类型特征std::is_base_of_v,可以用概念std::derived_from替代。代码如下(注意T和基类的位置颠倒了):

template <typename T>
requires std::derived_from<T, Player> || std::derived_from<T, Monster>
void func(T Character) {}

许多用于确定类型是否满足要求的标准库类型特征,都可以用相应的概念替代,大多数具有布尔成员value的类型特征都属于这一类。

尾随 requires 子句(Trailing Requires Clause)

requires子句可以放在函数签名和函数体之间。

template <typename T>
T sum(const T& a, const T& b)
requires std::integral<T>
{
return a + b;
}

这种语法的一种常见用法是,基于模板类型参数禁用类模板的某个成员函数。

例子 对某类型禁用类模板的某个成员函数

#include <concepts>
#include <iostream>
#include <string>
template <typename T>
class Container {
public:
std::string execute()
requires std::integral<T>
{
return "execute()";
}
std::string run() { return "run()"; }
};
int main() {
Container<int> containerInt;
Container<double> containerDouble;
std::cout << containerInt.execute() << std::endl;
std::cout << containerInt.run() << std::endl;
// std::cout << containerDouble.execute() << std::endl;
std::cout << containerDouble.run() << std::endl;
}

类模板Container的实例Container<int>有两个成员函数execute()run(),而实例Container<double>只有一个成员函数run(),没有execute()

概念在缩写函数模板(abbreviated function template)中的使用

使用缩写函数模板语法可以很方便地创建模板函数,只需要将一个或多个函数参数的类型设置为auto即可。如下:

#include <iostream>
auto average(auto x, auto y) {
return (x + y) / 2;
}
int main() {
std::cout << average(1, 2) << std::endl;
std::cout << average(1.0, 2.0) << std::endl;
}

若要使用概念约束函数参数,只需将概念放在auto前即可,如下:

#include <concepts>
#include <iostream>
auto average(std::integral auto x, std::integral auto y) {
return (x + y) / 2;
}
int main() {
std::cout << average(1, 2) << std::endl;
// std::cout << average(1.0, 2.0) << std::endl; // error
}

上面这段代码约束函数average()的参数xy必须是整数。

组合多种使用方式

多种概念(concepts)的使用方式可以组合使用。如下:

#include <concepts>
#include <iostream>
template <typename T>
requires std::integral<T> || std::floating_point<T>
auto average(std::integral auto x, T y)
requires std::signed_integral<T>
{
return (x + y) / 2;
}
int main() {
std::cout << average(1, 2) << std::endl;
unsigned int a{3};
std::cout << average(1, a) << std::endl; // error
}

函数模板average()使用了requires 子句、尾随 requires 子句,并且是缩写函数模板。

自定义概念

例子 在函数模板中使用自定义概念

#include <concepts>
#include <iostream>
template <typename T>
concept Integer = std::integral<T>;
template <Integer T>
void constrainedTemplate(T x) {
std::cout << "That's an integer.\n";
}
int main() {
constrainedTemplate(1);
}

这段代码没有实用性,只是为了说明自定义概念的语法。Integer就相当于是标准库预定义概念std::integral的别名。

例子 在if constexpr中使用自定义概念

#include <concepts>
#include <iostream>
template <typename T>
concept Integer = std::integral<T>;
template <typename T>
void unconstrainedTemplate(T x) {
if constexpr (Integer<T>) {
std::cout << "That's an integer, too.\n";
}
}
int main() {
unconstrainedTemplate(2);
}

例子 结合两个标准库概念

#include <concepts>
#include <iostream>
template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
template <Numeric T>
T sum(const T& a, const T& b) {
return a + b;
}
int main() {
std::cout << sum(1, 2) << std::endl;
}

std::integralstd::floating_point满足其中之一,自定义概念Numeric就成立。

例子 在 requires {} 中组合多个 requires 语句

#include <concepts>
#include <iostream>
#include <string>
template <typename T>
concept ConvertibleToFromInt = requires {
requires std::convertible_to<int, T>;
requires std::convertible_to<T, int>;
};
template <ConvertibleToFromInt T> void func(int x) {
std::cout << "ordinary: " << x << std::endl;
std::cout << "T{x}: " << T{x} << std::endl;
std::cout << "int{T{x}}: " << int{T{x}} << std::endl;
}
template <typename T> void func(int x) {
std::cout << typeid(T).name() << " is not satisfied with the concept."
<< std::endl;
}
class CustomType {
public:
CustomType(int x) : m_value{x} {}
operator int() { return m_value; }
private:
int m_value;
};
int main() {
func<CustomType>(42);
func<std::string>(88);
return 0;
}

requires {} 的大括号{}中是多个独立的 requires 语句。

在上面这个例子中,如果类型T能够和int互转,则打印一些文字;如果不能,则打印另一些文字。

例子 无效的约束

requires {}{}中的语句是 valid 的(有效的,合法的)即可,并不要求计算结果为true。使用时可能在这个地方出错,如下:

#include <concepts>
#include <iostream>
template <typename T>
concept Integer = requires { std::integral<T>; };
int main() {
if (Integer<std::string>) { std::cout << "The concept is satisfied.\n"; }
return 0;
}

虽然编译时编译器给出了一个警告,但仍编译成功了,并且在控制台打印了The concept is satisfied.。为什么?不是应该什么都不打印吗?明明std::string不是整数呀,明明 std::integral<T>的结果是false

对于任意的T,无论 std::integral<T>; 的结果是false还是true,它都是一个合法的(有效的,valid)语句。requires {} 仅仅只会检查大括号{}内的语句是否有效,如果里面的语句都有效,就视为满足这个概念。

requires {} 中,如果不仅想要语句是有效的(valid),并且结果还要是true,应该在语句前面添加requires关键字,如下:

template <typename T>
concept Integer = requires { requires std::integral<T>; };

例子 组合requires语句及其否定

requires表达式中可以使用布尔表达式||, &&, !

下面的概念定义,要求类型T满足:可以从int转换为T,但是不能从T转换为int,如下:

template <typename T>
concept SomeConcept = requires {
requires std::convertible_to<int, T>;
requires !std::convertible_to<T, int>;
};

下面的概念定义,要求类型T满足:T不能是可以同时从int转换为和转换到的类型,(可以在一个方向上转换,或两个方向都不能转换),如下:

template <typename T>
concept SomeConcept = !requires {
requires std::convertible_to<int, T>;
requires std::convertible_to<T, int>;
};

例子 “函数式requires”语法

类型T必须实现加法+运算符,如下:

template <typename T>
concept MyConcept = requires(T x) {
x + x;
};

类型T必须实现运算符+, -, *, /,如下:

template <typename T>
concept Arithmetic = requires(T x) {
x + x;
x - x;
x * x;
x / x;
};

类型T必须实现加法+运算符,并且必须实现/运算符,并且/的右操作数必须是一个int,例如2。如下:

template <typename T>
concept Averagable = requires(T x, T y) {
(x + y) / 2;
};

一个完整的例子:

#include <concepts>
#include <iostream>
template <typename T>
concept Averagable = requires(T x, T y) { (x + y) / 2; };
template <Averagable T> T average(T a, T b) { return (a + b) / 2; }
int main() {
// fine
std::cout << average(3, 5) << std::endl;
std::string A{"hello"};
std::string B{"world"};
// error
std::cout << average(A, B) << std::endl;
return 0;
}

例子 具有多个模板参数的概念

要求类型T1T2可以进行加法+运算,如下:

#include <iostream>
template <typename T1, typename T2>
concept Addable = requires(T1 x, T2 y) {
x + y;
};
int main() {
if (Addable<int, float>) {
std::cout << "int is addable to float.\n";
}
if (!Addable<int, std::string>) {
std::cout << "int is not addable to std::string.\n";
}
return 0;
}

当概念用于约束模板参数时,会自动为第一个参数提供值。也就是说,如果概念只需要一个参数,则就不用为概念提供参数;如果概念需要多个参数,则就不用提供第一个参数,只需提供后续的参数。

例如,将std::integral用于布尔表达式时需要显式提供模板参数,如std::integral<int>,需要手动提供模板参数int;而当将其用于约束模板时,参数在替换过程中会自动填充,如template <std::integral T>,不需要手动提供模板参数,直接用std::integral就可以。

例如,自定义的概念AddableTo需要两个模板类型参数,用于布尔表达式时需要提供两个模板参数,用于约束模板时只需要提供一个模板参数,如下:

#include <iostream>
template <typename T1, typename T2>
concept AddableTo = requires(T1 x, T2 y) {
x + y;
};
// provide one argument.
template <AddableTo<float> T>
void func(T x) {
std::cout << "type T is addable to float.\n";
}
int main() {
// provide two arguments.
if (AddableTo<int, float>) {
std::cout << "int is addable to float.\n";
}
func(42);
return 0;
}

将概念和类一起使用

例子 使用 requires 表达式检查类(class)是否具有特定的成员变量或函数

关键代码:

template <typename T>
concept Sized = requires { T::size; };

完整代码:

#include <iostream>
template <typename T>
concept Sized = requires { T::size; };
struct Rock {
int size;
};
int main() {
if constexpr (Sized<Rock>) {
std::cout << "Rock has a size.\n";
}
return 0;
}

另一种做法,使用“函数式 requires 表达式”,如下:

// template <typename T>
// concept Sized = requires { T::size; };
template <typename T>
concept Sized = requires(T x) {
T::size;
// 或者
// x.size;
};

例子 类型要么有一个 size 成员,要么同时具有 width 和 height 成员

template <typename T>
concept Sized = requires { T::size; } || requires {
T::width;
T::height;
};

例子 约束成员变量的类型,成员size的类型限定为整数

template <typename T>
concept IntegerSized = std::integral<decltype(T::size)>;

或者:

template <typename T>
concept IntegerSized = requires {
requires std::integral<decltype(T::size)>;
};

例子 约束成员变量的类型为确切的类型,检查成员size的类型是否确定为int

template <typename T>
concept IntegerSized = std::same_as<int, std::remove_cvref_t<decltype(T::size)>>;

或者:

template <typename T>
concept IntegerSized = requires {
requires std::same_as<int, std::remove_cvref_t<decltype(T::size)>>;
};

意思是,类T的数据成员size去除const和引用限定符后,应当与int类型相同。也就是,size的类型可以是intconst intint&, const int&。如果不加std::remove_cvref_t,就太严苛了些,size的类型只能是int,带const, &都不行。

例子 约束类必须具有特定成员函数,要求类具有一个不带参数的可调用的render()成员函数

较为自然的方法是,使用函数式requires表达式,然后提供使用这些函数成员的语句。

#include <iostream>
template <typename T>
concept Renderable = requires(T object) {
object.render();
};
struct Rock {
void render(){}
};
int main() {
if constexpr (Renderable<Rock>) {
std::cout << "Rock is renderable with no arguments.\n";
}
}

例子 约束类型是可调用的(函数指针)

在C++中,函数也有类型,就像任何其他类型一样,我们可以使用取地址运算符 & 来创建指向它们的指针。

#include <iostream>
template <typename T>
void call(T function) {
function();
}
void greet() {
std::cout << "hello\n";
}
int main() {
call(&greet);
}

在上面的例子中,call() 函数接收一个函数指针作为参数,然后调用指向的函数。

可以使用概念std::invocalble来约束它:

template <std::invocable T>
void call(T function) {
function();
}

上面将 std::invocalble 用于约束模板类型参数,它所需的模板参数值自动填充。如果将 std::invocalble 用作独立的表达式,它所需的模板参数值需要手动填写,如:

#include <concepts>
#include <iostream>
void greet() {
std::cout << "hello\n";
}
int main() {
if (std::invocable<decltype(&greet)>) {
std::cout << "Greet is invocable.\n";
}
}

例子 约束类型是可调用的(类的成员函数)

如果要测试的函数是类的成员函数,需要使用范围解析运算符::限定其确切位置,如SomeType::SomeMethod;还需要为概念std::invocable提供第二个模板参数,该参数是指向该成员函数所属的类的指针类型。

#include <concepts>
#include <iostream>
class SomeType {
public:
void greet() { std::cout << "hello\n"; }
};
int main() {
if (std::invocable<decltype(&SomeType::greet), SomeType*>) {
std::cout << "Greet is invocable.\n";
}
}

或者:

#include <concepts>
#include <iostream>
template <typename T>
concept Greetable = std::invocable<decltype(&T::greet), T*>;
class SomeType {
public:
void greet() { std::cout << "hello\n"; }
};
int main() {
if constexpr (Greetable<SomeType>) {
std::cout << "Greet is invocable.\n";
}
}

例子 约束类的成员函数只能使用特定类型的参数来调用

可以简单地通过在语句中传递示例值做到。

完整代码:

#include <iostream>
template <typename T>
concept Renderable = requires(T object) {
object.render(1, 2);
};
struct Rock {
void render(int x, int y) {}
};
int main() {
if constexpr (Renderable<Rock>) {
std::cout << "Rock is renderable with two integer arguments.\n";
}
return 0;
}

关键代码:

template <typename T>
concept Renderable = requires(T object) {
object.render(1, 2);
};

或者,将假设参数(上面是12)添加到函数式requires语句的参数列表中,这样就可以显式指定参数类型,并可以为变量命名:

template <typename T>
concept Renderable = requires(T object, int width, int height) {
object.render(width, height);
};

或者,使用std::declval()创建假设值,如下:

template <typename T>
concept Renderable = requires(T object) {
object.render(std::declval<int>(), std::declval<int>());
};

概念的模板参数列表并不局限于只能使用单个参数,因此可以像下面这样定义和使用概念:

#include <iostream>
template <typename T, typename R1, typename R2>
concept Renderable = requires(T object, R1 x, R2 y) {
object.render(x, y);
};
struct Rock {
void render(int x, int y) {}
};
int main() {
if constexpr (Renderable<Rock, int, float>) {
std::cout << "Rock is renderable with two integer arguments.\n";
}
return 0;
}

这也可以通过使用带参数的std::invocable做到,如下:

#include <iostream>
struct Rock {
void render(std::string x, int y) {}
};
int main() {
if constexpr (std::invocable<decltype(&Rock::render), Rock*, std::string, int>) {
std::cout << "Rock is renderable with two integer arguments.\n";
}
return 0;
}

或者:

#include <iostream>
template <typename T, typename R1, typename R2>
concept Renderable = std::invocable<decltype(&T::render), T*, R1, R2>;
struct Rock {
void render(std::string x, int y) {}
};
int main() {
if constexpr (Renderable<Rock, std::string, int>) {
std::cout << "Rock is renderable with two integer arguments.\n";
}
return 0;
}

关于std::invocable<>尖括号里面的T*指针类型,可以看前面 例子 约束类型是可调用的(类的成员函数)。

例子 约束普通函数只能使用特定类型的参数来调用

#include <iostream>
template <std::invocable<std::string, int> T>
void call(T function) {
function("hello", 42);
}
void greet(std::string greeting, int num) {
std::cout << greeting << ", Num: " << num;
}
int main() {
call(&greet);
return 0;
}

当使用概念约束模板类型参数时,概念的第一个模板参数是自动提供的,因此只写成template <std::invocable<std::string, int> T>,而不用写成template <std::invocable<decltype(&T), std::string, int> T>

例子 约束成员函数具有特定的返回类型

要求对象有一个render()成员函数,该函数不接受参数,函数的返回值满足概念std::integral,如下:

template<typename T>
concept Renderable = requires(T object) {
{ object.render() } -> std::integral;
};

注意,C++规范要求,在->后面需要是一个概念,而不能是类型。如果想要约束函数返回某种特定类型(比如int),可以使用标准库概念std::same_asstd::convertible_to,如下:

template<typename T>
concept Renderable = requires(T object) {
{ object.render() } -> std::same_as<int>;
};

下面的例子,要求render()成员函数接收类型为模板参数类型A的参数,返回值类型为可转换另一个模板参数类型R的类型,如下:

template<typename T, typename A, typename R>
concept Renderable = requires(T object, A x) {
{ object.render(x) } -> std::convertible_to<R>;
};

例子 测试类型是否实现特定运算符,如相等比较运算符,例如 == 和 !=

运算符本质上是函数(或成员函数),所以可以用测试是否具有符合要求的普通函数(或成员函数)的做法来测试是否具有符合要求的运算符。

要求类型实现==运算符,如下:

template<typename T>
concept Comparable = requires(T x, T y) {
x == y;
};

要求类型实现==运算符,并且返回值可转换为布尔值,如下:

template<typename T>
concept Comparable = requires(T x, T y) {
{ x == y }->std::convertible_to<bool>;
};

为概念另外添加一个模板类型参数,让使用者可以指定要对象与哪个类型进行比较,如下:

#include <iostream>
template<typename T1, typename T2>
concept ComparableTo = requires(T1 x, T2 y) {
{ x == y }->std::convertible_to<bool>;
{ y == x }->std::convertible_to<bool>;
{ x != y }->std::convertible_to<bool>;
{ y != x }->std::convertible_to<bool>;
};
struct Container { int value; };
bool operator==(const Container& x, int y) {return x.value == y;}
bool operator==(int x, const Container& y) {return y == x;}
bool operator!=(const Container& x, int y) {return !(x == y);}
bool operator!=(int x, const Container& y) {return !(x == y);}
int main() {
if constexpr (ComparableTo<Container, int>) {
std::cout << "Container is comparable to int";
}
return 0;
}

含义是,Containerint 两个类型之间可以进行比较,重载了相关的 ==!= 运算符,并且比较的结果可以转换为bool值。

标准库提供了与上面我们自定义的概念类似的概念,如下:

std::equality_comparable<Container>
std::equality_comparable_to<Container, int>

参考