相关推荐recommended
C++20:从0到1学懂concept
作者:mmseoamin日期:2024-02-03

目录

  • 1.concept语法

    • 1.1 替换typename

    • 1.2 requires关键字

    • 1.4 concept与auto

  • 2.编译器支持

  • 3.总结

    C++20引入了concept(概念),是对模板参数(编译时评估)的一组约束。你可以将它们用于类模板和函数模板来控制函数重载和特化。一些优点包括:

    • 对模版参数强制类型约束

    • 提高代码可读性(替换了较长的SFINAE代码)

    • 提供更友好的报错信息

    • 通过限制可以使用的类型来防止意外的模板实例化

      往期C++20的系列文章:

      1.C++那些事之C++20协程开篇

      2.盘点C++20模块那些事


      注:本篇所有代码已更新于星球。


      下面进入正文,以一个比较简单加法为例。

      #include 
      struct Foo {};
      template 
      T Add(T a, T b) {
        return a + b;
      }
      int main() {
        std::cout << Add(1, 2) << std::endl;
        Foo f1, f2;
        std::cout << Add(f1, f2) << std::endl;
        return 0;
      }

      对于Foo来说,是不支持加法的,于此同时也是不可以直接std::cout << ,因此在编译时报一大堆错误,包含operator<<与operator+,但这并不是我们期望的错误信息,我们比较期望的是编译器给我们最直观的错误信息,即:这个结构体能不能相加。

      add.cc: In function 'int main()':
      add.cc:13:13: error: no match for 'operator<<' (operand types are 'std::ostream' {aka 'std::basic_ostream'} and 'Foo')
         13 |   std::cout << Add(f1, f2) << std::endl;
            |   ~~~~~~~~~ ^~ ~~~~~~~~~~~
            |        |          |
            |        |          Foo
            |        std::ostream {aka std::basic_ostream}
      In file included from /usr/local/Cellar/gcc/13.2.0/include/c++/13/iostream:41,
                       from add.cc:1:
      /usr/local/Cellar/gcc/13.2.0/include/c++/13/ostream:110:7: note: candidate: 'std::basic_ostream<_CharT, _Traits>::__ostream_type& std::basic_ostream<_CharT, _Traits>::operator<<(__ostream_type& (*)(__ostream_type&)) [with _CharT = char; _Traits = std::char_traits; __ostream_type = std::basic_ostream]'
        110 |       operator<<(__ostream_type& (*__pf)(__ostream_type&))
        
      /usr/local/Cellar/gcc/13.2.0/include/c++/13/ostream:801:5: note:   template argument deduction/substitution failed:
      /usr/local/Cellar/gcc/13.2.0/include/c++/13/ostream: In substitution of 'template _Ostream&& std::operator<<(_Ostream&&, const _Tp&) [with _Ostream = std::basic_ostream&; _Tp = Foo]':
      add.cc:13:26:   required from here
      /usr/local/Cellar/gcc/13.2.0/include/c++/13/ostream:801:5: error: no type named 'type' in 'struct std::enable_if'
      add.cc: In instantiation of 'T Add(T, T) [with T = Foo]':
      add.cc:13:19:   required from here
      add.cc:7:12: error: no match for 'operator+' (operand types are 'Foo' and 'Foo')
          7 |   return a + b;
            |          ~~^~~

      当我们使用concept实现之后:

      template 
      concept Addable = requires(T a, T b) { a + b; };
      template 
        requires Addable
      T Add(T a, T b) {
        return a + b;
      }

      便可以得到我们关心的编译错误:

      add_concept.cc: In function 'int main()':
      add_concept.cc:17:19: error: no matching function for call to 'Add(Foo&, Foo&)'
         17 |   std::cout << Add(f1, f2) << std::endl;
            |                ~~~^~~~~~~~
      add_concept.cc:10:3: note: candidate: 'template  requires  Addable T Add(T, T)'
         10 | T Add(T a, T b) {
            |   ^~~
      add_concept.cc:10:3: note:   template argument deduction/substitution failed:
      add_concept.cc:10:3: note: constraints not satisfied
      add_concept.cc: In substitution of 'template  requires  Addable T Add(T, T) [with T = Foo]':
      add_concept.cc:17:19:   required from here
      add_concept.cc:6:9:   required for the satisfaction of 'Addable' [with T = Foo]
      add_concept.cc:6:19:   in requirements with 'T a', 'T b' [with T = Foo]
      add_concept.cc:6:42: note: the required expression '(a + b)' is invalid
          6 | concept Addable = requires(T a, T b) { a + b; };
            |                                        ~~^~~

      下面,我们来针对上面这个例子深入学习concept语法。

      1.concept语法

      语法:

      template 
      concept concept-name = constraint-expression;

      我们来对比一下实际的例子,Addable是concept-name,constraint-expression是requires(T a, T b) { a + b; }。

      template 
      concept Addable = requires(T a, T b) { a + b; };

      使用方式为:

      #include 

      这个concept可以放在多个地方,如下:

      • typename的位置

      • requires后面

      • auto前面

        1.1 替换typename

        约束模版参数,替换typename。

        // template   typename->Addable
        template 
        T Add(T a, T b) {
          return a + b;
        }

        1.2 requires关键字

        我们在函数模板中使用 requires 关键字。它可以访问我们的模板T是否是可以相加的,如果模板可以处理相加,它将返回 true。

        requires可以放在模版中,也可以放在函数之后,但是不可以放在类之后。于此同时它有两个写法:

        • requires 条件

          例如:

          template 
            requires Addable
          T Add(T a, T b) { ... }
          • requires 表达式

            例如:

            template 
            concept Addable = requires(T a, T b) { a + b; };
            1.2.1 放在模版中

            函数:

            template 
              requires Addable
            T Add(T a, T b) {
              return a + b;
            }

            类:

            template 
             requires Addable 
            class Bar {
             public:
              T Add(T a, T b) { return a + b; }
            };
            1.2.2 函数尾随 Requires 子句

            函数:

            template 
            T Add(T a, T b)
              requires Addable
            {
              return a + b;
            }

            对于类则不支持这种写法,会报错:error: expected unqualified-id before 'requires'  28 |   requires Addable

            template 
            class Bar requires Addable 
            {
             public:
              T Add(T a, T b) { return a + b; }
            };
            1.2.3 requires基本示例

            以数据库当中的类型为例,数据库中有不同类型,我们将其划分为:null、binary、number等,我们想要对传递的类型执行打印操作,于是有了下面的示例:

            #include 
            class NumberType {};
            class BaseBinaryType {};
            class NullType {};
            class FloatingPointType : public NumberType {};
            class IntegerType : public NumberType {};
            class BinaryType: public BaseBinaryType {};
            template 
              requires std::is_base_of_v || std::is_base_of_v
            void PrintValue(T v) {}
            int main() {
              PrintValue(FloatingPointType{});
              PrintValue(NullType{});
              return 0;
            }

            对于requires我们可以使用||,上面示例中出现了NullType,它不满足requires,因此会编译出现:

            concept_requires.cc:16:13: error: no matching function for call to 'PrintValue(NullType)'
               16 |   PrintValue(NullType{});
                  |   ~~~~~~~~~~^~~~~~~~~~~~
            concept_requires.cc:12:6: note: candidate: 'template  requires (is_base_of_v) || (is_base_of_v) void PrintValue(T)'
               12 | void PrintValue(T v) {}
                  |      ^~~~~~~~~~
            concept_requires.cc:12:6: note:   template argument deduction/substitution failed:
            concept_requires.cc:12:6: note: constraints not satisfied
            concept_requires.cc: In substitution of 'template  requires (is_base_of_v) || (is_base_of_v) void PrintValue(T) [with T = NullType]':
            concept_requires.cc:16:13:   required from here
            concept_requires.cc:12:6:   required by the constraints of 'template  requires (is_base_of_v) || (is_base_of_v) void PrintValue(T)'
            concept_requires.cc:11:45: note: no operand of the disjunction is satisfied
               11 |   requires std::is_base_of_v || std::is_base_of_v
                  |            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

            1.4 concept与auto

            当auto出现时,我们可以将其与concept一起使用,例如:

            auto add(auto x, auto y) {
              return x + y;
            }

            我们可以变为:

            template 
            concept Addable = requires(T a, T b) { a + b; };
            auto add2(Addable auto x, Addable auto y) {
              return x + y;
            }

            编译时会出现:

            concept_auto.cc:17:20: error: no matching function for call to 'add2(Foo&, Foo&)'
               17 |   std::cout << add2(f1, f2) << std::endl;
                  |                ~~~~^~~~~~~~
            concept_auto.cc:10:6: note: candidate: 'template  requires (Addable) && (Addable) auto add2(auto:18, auto:19)'
               10 | auto add2(Addable auto x, Addable auto y) {
                  |      ^~~~
            concept_auto.cc:10:6: note:   template argument deduction/substitution failed:
            concept_auto.cc:10:6: note: constraints not satisfied
            concept_auto.cc: In substitution of 'template  requires (Addable) && (Addable) auto add2(auto:18, auto:19) [with auto:18 = Foo; auto:19 = Foo]':
            concept_auto.cc:17:20:   required from here
            concept_auto.cc:8:9:   required for the satisfaction of 'Addable' [with auto:18 = Foo]
            concept_auto.cc:8:19:   in requirements with 'T a', 'T b' [with T = Foo]
            concept_auto.cc:8:42: note: the required expression '(a + b)' is invalid
                8 | concept Addable = requires(T a, T b) { a + b; };

            2.编译器支持

            需要GCC(10.0+),Clang(10.0+),编译选项:-std=c++20/-std=c++2a

            https://en.cppreference.com/w/cpp/compiler_support

            3.总结

            自C++20提供的concept之后,我们不再需要enable_if/SFINAE的机制、函数重载来做一些模版约束检查了,使用concept可以帮你搞定这个操作,它提供了一种更清晰和强大的模板参数约束机制,使得模板代码更易于编写、理解和维护。通过在编译时进行类型检查,它有助于提高代码的稳健性和可读性。

            源码获取👇:

            C++20:从0到1学懂concept,9c5377a2b5662eb3bfb8fc1c9ce6f2fb.jpeg,第1张

            C++20:从0到1学懂concept,00fc8c29733319dd7dbffe92771d1f6a.jpeg,第2张


            往期回顾:

            热度更新,手把手实现工业级线程池

            C++ 多值返回:从版本1到版本6秒杀