在较早的博客文章中,我展示了 Go 编程语言允许您在定义接口后编写通用函数。 Java 有一个非常相似的同名概念(接口)。我给出了以下示例:
类型IntIterable接口{ 下一个( )布尔 下一个( ) uint32 重置( ) } func Count ( i IntIterable ) ( count int ) { 计数= 0 我。重置( ) 对于我。下一个( ) { 我。下一个( ) 计数+ + } 返回 }
从这段代码中,您所要做的就是提供一个支持该接口的类型(具有具有正确签名的方法 HasNext、Next 和 Reset),然后您就可以使用函数 Count。
C++呢?在传统的 C++ 中,您可以像这样编写一个 Count 模板:
template < class T > size_t count ( T & t ) { 吨。重置( ) ; size_t计数= 0 ; while ( t . has_next ( ) ) { 吨。下一个( ) ; 计数+ + ; } 返回计数; }
这在适度使用时很好,但如果我是一名程序员并且我需要使用我不熟悉的模板函数,我可能必须阅读代码以找出我的类型需要实现什么才能与函数模板兼容.当然,它也限制了我用来编程的工具:它们对我将在 count 函数中实际使用的类型了解不多。
值得庆幸的是,C++ 现在具有相当于 Go 或 Java 接口的功能,它被称为概念(它需要支持 C++20 的最新编译器)。你会这样实现它……
模板<类型名T > 概念 is_iterable = requires ( T v ) { {诉。 has_next ( ) } - > std :: convertible_to < bool > ; {诉。 next ( ) } - > std :: same_as < uint32_t > ; {诉。重置( ) } ; } ; template < is_iterable T > size_t count ( T & t ) { 吨。重置( ) ; size_t计数= 0 ; while ( t . has_next ( ) ) { 吨。下一个( ) ; 计数+ + ; } 返回计数; }
它甚至比等效的 Go 更好,因为正如我的示例所示,我不必严格要求has_next函数返回布尔值,我可以只要求它返回可以转换为布尔值的东西。在这个特定的示例中,我要求next方法返回一个特定类型 ( uint32_t ),但我可能需要一个整数 ( std::is_integral<T>::value ) 或一个数字 ( std:: is_arithmetic<T>::value )。
在 Go 中,我发现使用接口并不是免费的:它会使代码变慢。在 C++ 中,如果您实现以下类型并对其调用计数,您会发现优化编译器能够弄清楚它们需要返回内部向量的大小。换句话说:概念/模板的使用没有运行时成本。但是,您需要预先支付更长的编译时间。
结构iterable_array { std :: vector <uint32_t>数组{ } ; _ size_t索引= 0 ; void reset ( ) {索引= 0 ; } bool has_next ( ) {返回索引<数组。尺寸( ) ; } uint32_t next ( ) {索引+ + ;返回数组[索引- 1 ] ; } } ; size_t f ( iterable_array & a ) { 返回计数(一) ; }
在 C++ 中,您甚至可以通过强制编译时计算使运行时成本绝对为零。诀窍是确保您的类型可以实例化为编译时常量,然后将其传递给计数函数。它现在可以与最近的 GCC 一起使用,但最终应该会得到广泛支持。在下面的代码中,函数只返回整数 10。
模板< is_iterable T > constexpr size_t count ( T & & t ) { 返回计数( t ) ; } 结构iterable_array { constexpr iterable_array ( size_t s ) :数组( s ) { } std :: vector <uint32_t>数组{ } ; _ size_t索引= 0 ; constexpr void reset ( ) { index = 0 ; } constexpr bool has_next ( ) {返回索引<数组。尺寸( ) ; } constexpr uint32_t next ( ) {索引+ + ;返回数组[索引- 1 ] ; } } ; consteval size_t f ( ) { 返回计数( iterable_array ( 10 ) ) ; }
那么概念有什么用呢?我认为这主要是关于记录您的代码。例如,在 simdjson 库中,我们有get<T>()类型的模板方法,其中T是几种选择类型之一( int64_t 、 double 、 std::string_view等)。一些用户总是希望有一些神奇的东西,只是做get<mytypefromanotherlibrary>()希望 simdjson 会以某种方式有足够的过载。然后他们会收到一条令人讨厌的错误消息。通过使用概念,我们可能会限制这些编程错误。事实上,IDE 和 C++ 编辑器可能会立即捕捉到它。
原文: https://lemire.me/blog/2023/04/18/defining-interfaces-in-c-with-concepts-c20/