• 闭关之 C++ 函数式编程笔记(四):monad 和 模板元编程


    第十章 monad

    注意

    • 本节所述的内容都是基于 range-v3, 而不是 C++20 的 ranges
    • C++20 的 ranges 与 range-v3 有很大差别,而且有很多 range-v3 的特性没有被支持
    • 如果要学 C++20 的 ranges 这章不太合适,但是如果想学 range 的思想,这章还是不错的

    10.1 仿函数并不是以前的仿函数

    • 仿函数定义
      • 如果一个类模板 F 包含一个transform(或 map)函数,则称 F 为仿函数
        • transform 函数必须遵守以下两条规定
          • 仿函数 transform 转换是等价转换,返回相同的仿函数实例
            • f | transform([](auto value) { return value; }) == f
          • 先用一个函数对仿函数进行转换,然后用另一个函数进行转换,等价于组合这两个函数对仿函数进行转换
            • f | transform(t1) | transform(t2) ==
            • f | transform([=](auto value) { return t2(t1(value)); })
        • 与 range 中的 std::transformview::transform 类似
          • STL 中通用集合和 range 都是仿函数 (functor)
          • 它们都是包装类型 (wrapper types)

    10.1.1 处理可选值

    • std::optional 类型是一个基本的仿函数
    • std::optional 定义转换函数示例
      template <typename T1, typename F>
      auto transform(const std::optional<T1> &opt, F f)
          -> decltype(std::make_optional(f(opt.value())))
      {
          if (opt) {
              return std::make_optional(f(opt.value()));
          } else {
              return {};
          }
      }
      
      std::string user_full_name(const std::string& login)
      {
          return "full_name";
      }
      std::string to_html(const std::string& text) 
      {
          return "html";
      }
      std::string current_login = "login";
      auto html =  transform(transform(current_login, user_full_name), to_html);
      
      • 书上的这个示例感觉有些不严谨,transform 返回值永远不为 nullotp
      • 但是给了我们自定义转换函数的思想
    • std::optional 定义 range 示例
      • Code_9_1_6

    10.2 monad: 更强大的仿函数

    • 仿函数允许对包装类型的值进行转换,但有一个严重缺陷
    • 在每次转换的时候,包装类型返回值会被嵌套
      • 也就是说,当第一次转换的返回值会被第二个转换函数再次包装
      • 转换函数越多,嵌套越多
    • monad 作用就是解决上述问题的
      • monad M 是一个定义了附加函数的仿函数
        • 去掉一层嵌套的函数
        • j o i n : M < M < T > > − > M < T > join: M> -> M join:M<M<T>>>M<T>
      • Code
      join(transform(
          join(transform(
              login,
              user_full_name)),
          to_html));
      
      • range 版本 Code
      auto login_as_range = as_range(current_login);
      login_as_range | ranges::v3::view::transform_fn(user_full_name)
                     | ranges::v3::view::join_fn
                     | ranges::v3::view::transform_fn(to_html)
                     | ranges::v3::view::join_fn;
      
    • 可以进行简化
      • 定义 monad 的常用方式
        • monad M 是一个包装类型,它包含一个构造函数和一个组合 transform 个 join 的 mbind 函数
          • 构造函数
            • 把 T 类型的值构造成 M 实例的函数
            • construct : T -> M
          • mbind 函数
            • 其实就是一个 bind 函数,只是为了避免与 std::bind 混淆
            • mbind: (M, T1 -> M) -> M
    • 所有的 monad 都是仿函数
    • 和仿函数一样, monad 也有几个条件(使用 monad, 这些也不是必须的)
      • 如果有一个函数 f : T 1 − > M < T 2 > f: T1->M f:T1>M<T2> 和一个 T 1 T1 T1 类型的值,把这个值包装成 monad M,并与函数 f f f 绑定,与直接对调用函数 f f f 是一样的
        • m b i n d ( c o n s t r u c t ( a ) , f ) = = f ( a ) mbind(construct(a), f) == f(a) mbind(construct(a),f)==f(a)
      • 如果把一个值与构造函数绑定,则得到与原来相同的值
        • mbind(m, construct) == m
      • 定义 mbind 操作的关联性
        • mbind(mbind(m, f), g) == mbind(m, [](auto x){ return mbind(f(x), g) })

    10.3 基本的例子

    • std::vector 构造一个仿函数
      • 需要做两项检查
        • 仿函数是一个带有一个模板参数的类模板
        • 需要一个 transform 函数,它接收一个向量,对向量元素进行转换的函数
          • 转换函数将返回转换后元素的向量
          //把给定的向量看作 range,对其中每个元素调用f进行转换
          //就像仿函数要求的一样,函数 f 返回一个向量
          template <typename T, typename F>
          auto transform(const std::vector<T> xs, F f) 
          {
              return xs | ranges::v3::view::transform_fn(T)
                  | ranges::v3::to_vector;
          }
          
      • 把仿函数转换成 monad
        • 需要构造函数和 mbind 函数
        • 构造函数接收一个值,并用它构造一个向量
          template <typename T>
          std::vector<T>  make_vector(T&& value)
          {
              return { std::forward<T>(value) };
          }
          
        • mbind 函数
          • 使用 transform 加 join 是最简单的做法
          • 需要一个能把多个值映射成monad实例的函数
          template <typename T, typename F>
          //f 接收一个T类型的值,返回 T 类型或其他类型的vector
          auto mbind(const std::vector<T>& xs, F f)
          {
              //调用 f 产生一个向量类型的 range, 可以把它转换成向量的向量
              auto transformed =
                  xs | ranges::v3::view::transform_fn(f)
                    | ranges::v3::to_vector;
              //所需要的不是向量的向量,而是所有值在一个向量中
              return transformed
                  | ranges::v3::view::join_fn
                  | ranges::v3::to_vector;
          }
          
      • 该向量的 mbind 函数不高效,有保存中间向量

    10.4 range 与 monad 的嵌套使用

    • mbind 更适合集合类结构
    • mbing 对与原集合中的每一个元素,不仅可以产生新元素,而且可以产生任意多的新元素
    • mbind 实现的过滤
      template <typename C, typename P>
      auto filter(const C& collection, P predicate)
      {   //依据当前元素是否满足谓词,接收0个或1个元素
          return collection
              | mbind([=](auto element) {
              return ranges::v3::view::single_fn(element)
                  | ranges::v3::view::take_fn( predicate(element) ? 1 : 0);
                  });
      }
      
    • range 就是 monad, 所有不仅可以用很酷的方式重新实现 range 转换,而且可以嵌套 range
    • range 嵌套就是带有过滤的 transform 或 mbind。
      • 因为任意的 monad 都包含这些函数,而不仅仅是 range
        • 所以也可以把它们称作 monad 嵌套

    10.5 错误处理

    • 函数式编程中的函数的主要功能
      • 唯一的功能
      • 计算结果并返回它
    • 如果函数执行失败,则返回一个值,或在发生错误时不返回任何值(std::optional)

    10.5.1 std::optional 作为 monad

    • optional 可以表示有可能出现的缺失值现象,但有一个缺陷
      • 如果要使用值的化,要检测它是否存在
      • 如果链接更多函数,每次调用都要检查错误
    • 可以使用 monad
      • 在调用其他函数时将值剥离出来
    • monad 可以做:组合函数而无须处理额外上下文信息
    • 实现
      //指定返回类型,如果没有值则返回{}
      template <typename T, typename F>
      auto mbind(const std::optional<T>& opt, F f) -> decltype(f(opt.value()))
      {
          if (opt) 
          {
              //如果包含一个值,则调用 f 对值进行转换
              //并返回转换结果
              return f(opt.value());
          }
          else 
          {
              return {};
          }
      }
      
    • 如果使用这种方式串联多个函数,就会自动处理错误
      • 函数会依次执行,知道有一个函数出现错误
      • 如果没有函数执行失败,将返回得到处理结果
        std::optional<std::string> current_user_html() 
        {
            return mbind(
                mbind(current_login, user_full_name),
                to_html
            );
        }
        
      • 使用管道语法,增加可读性
        std::optional<std::string> current_user_html()
        {
            return current_login | mbind(user_full_name)
                                | mbind(to_html);
            );
        }
        

    10.5.2 expected 作为 monad

    • expected 不但可以处理错误,还能知道发生了什么错误
    • 组合 expected monad
      template <
          typename T, typename E, typename F,
          //f 可能返回不同类型,因此在返回之前,需要类型推断
          typename Ret = std::invoke_result<F(T)>::type
      >
      Ret mbind(const expected<T, E>& exp, F f) 
      {
          if (!exp) 
          {
              //如果 exp 包含错误,则继续把它传递下去
              return Ret::error(exp.error());
          }
          return f(exp.value());
      }
      
    • 示例
      expected<std::_Invoker_strategy, int> cuurent_user_html() 
      {
          return current_login | mbind(user_full_name)
                               | mbind(to_html);
      }
      

    10.5.3 try monad

    • 把异常包装成 expected monad 的函数
      template <
          typename F,
          typename Ret = std::invoke_result<F()>::type,
          typename Exp = expected<Ret, std::exception_ptr>
      >
      //函数 f 没有参数,如果要使用参数调用它,可以传递lambda表达式
      Exp mtry(F f)
      {
          try 
          {
              //如果没有抛出异常,则返回一个 expected 示例
              //包含 f 的返回结果
              return (Exp::success(f());
          }
          catch (...) 
          {
              //如果有异常抛出,则返回一个 expected 实例
              //包含指向该异常的指针
              return Exp::error(std::current_exception());
          }
      }
      
    • 示例
      auto result = mtry([=] {
              auto users = system.users();
              if (user.empty()) 
              {
                  throw std::runtime_error("No users");
              }
              return users[0];
          });
      
    • 也可以用另一种方式实现
      • 如果函数返回一个包含指向异常指针的 expected 实例,可以创建一个函数,要么返回存储在 expected 对象中的值,要么抛出其中的异常
      template <typename T>
      T get_or_throw(const expected<T, std::exception_ptr>& exp) 
      {
          if (exp) 
          {
              return exp.value();
          }
          else 
          {
              std::rethrow_exception(exp.error());
          }
      }
      

    10.6 monad 状态处理

    • monad 在函数式编程中流行的原因时,它可以以 “纯” 的方式处理包含状态的程序
    • 如果要用 monad 或 monad 转换链的方式实现程序,跟踪链条中的每个转换的状态,这就十分有用了
    • 使用积类型数据
      • 因为和类型只能表示一个值
      • 不但要包含值,同时还要包含附加信息(调试日志)
    • 示例
      template <typename T>
      class with_log 
      {
      public:
          with_log(T value, std::string log = std::string())
              : m_value(value)
              , m_log(log)
          {}
          T value() const 
          {
              return m_value;
          }
          std::string log() const
          {
              return m_log;
          }
      private:
          std::string m_log;
          T m_value;
      }
      
    • mbind 维护日志
      template <
          typename T,
          typename F,
          typename Ret = std::invoke_result<F()>::type
      >
      Ret mbind(const with_log<T1>& val, F f)
      {
          //使用 f 进行转换,返回转换结果和 f 的日志字符串
          const auto result_with_log = f(val.value());
          //返回处理结果,但日志不仅仅是 f 的日志,还要与原来的日志进行拼接
          return Rec(result_with_log.value(), val.log() + result_with_log.log());
      }
      
    • 上述记录日志方法和把日志输出到标准输出相比有几个优点
      • 可以处理多个平行日志
        • 每个链中的转换对应一个日志
        • 而不需要特殊的日志组件
      • 一个函数根据调用者的不同,可以写出各种日志,而无需指明
        • “这个日志写到这里”“那个日志写到那里”
      • 使同一异步操作链中的日志记录在一起,而不会与其他操作链中的日志混杂

    10.7 并发和延续 monad

    10.7.1 future 作为 monad

    • future 对象就是一个 monad
      • 它是一个类似容器的东西,可以包含0个或1个结果,这取决于异步操作是否完成
    • mbing 可以串联任意多个异步操作
      future<std::string> current_user_html() 
      {
          return current_user() | mbind(user_full_name)
                                | mbind(to_html);
      }
      
      • 上述代码串联了三个异步操作。每个函数处理前一个函数的结果
      • 传递给 mbind 的函数通常称为延续函数 (continuation)
      • 定义的 future 值的 monad 称为延续 monad (continuation monad)

    10.7.2 future 的实现

    • std::futureexpected 分 future 类似
      • std::future 不能智能地附加延续函数
      • 为了延续,增加成员函数 then() (std::experimental::future)
    • then() 与 mbind 类似,但有不同之处
      • monad 的 bind 接收一个函数,它的参数是普通的值,返回值为 future 对象
      • then() 接收的函数要求其参数为一个完成的 future 对象,并返回一个新的 future
    • 因此 then() 并不是使 future 变为 monad, 而是使实现 mbind 变得简单
    • 示例
      template <typename T, typename F>
      //接收一个函数 f,它可以把类型 T 转换成 future 的实例 future
      auto mbind(const std::experimental::future<T>& future, F f)
      {
          //接收一个把 future 转换成 future的函数
          //在把它传递给函数f之前,需要用lambda表达式提取future中的值
          return future.then(
              [](std::experimental::future<T> finished) {
                  //不会阻塞任何程序,因为延续函数只有在结果准备好时(或有异常时)
                  //才会被调用
                  return f(finished.get());
              }
          );
      }
      

    10.8 monad 组合

    • 首先有如下函数定义
      • user_full_name: std::string -> M
      • to_html: std::string -> M
      • 其中 M 代替了 optionalexpected 或其他包装类型
    • 普通 monad 调用如下
      M<std::string> user_htlm(const m<std::string>& login) 
      {
          return mbind(
              mbind(login, user_full_name),
              to_html
          );
      }
      
    • 进行普通函数组合时
      • 假设有两个函数 f: T1->T2g: T2->T3
      • 得到一个把 T1 转换成 T3 的函数
    • 使用 monad 组合,则稍有些不同
      • 函数不是返回一个普通的值,而是包装在 monad 中
      • 因此组合的函数变为
        • f: T1->Mg: T2->M
    • monad 组合函数
      template <typename F, typename G>
      auto mcompose(F f, G g)
      {
          return [=](auto value) {
              return mbind(f(value), g);
          };
      }
      
    • 使用 mcompose()
      • auto user_html = mcompose(user_full_name, to_html);
    • 使用 mcompose() 函数可以编写更简短、更通用的代码
    • 如果 monad 的构造函数与任意 monad 的函数组合,则得到函数本身
      • mcompose(f, construct) == f
      • mcompose(construct, f) == f
    • 根据结合的原则,如果有3个函数f、g、h需要组合,无论先式组合f和g,再把结果与h组合,还是先组合g和h,结果再与f组合,都没关系
      • mcompose(f, mcompose(g, h)) == mcompose(mcompose(f, g), h)
    • 这也称为 Kleisli 组合,通常它与普通函数组合具有相同的属性

    总结

    • 个人认为这章是本书最核心的内容,需要重复去看

    第十一章 模板元编程

    • 元函数
      • 可以接收两种类型,给出一种类型作为结果,就像函数一样,但操作的不是值,而是自己的类型。这种函数称为元函数
    • 使用模板的元编程(或 TMP,模板元编程),有专门的书籍介绍
    • 本章集中介绍 C++17 引入的一些支持 TMP 的新特性

    11.1 编译时操作类型

    • 创建一个元函数,接收一个集合返回集合包含的元素类型
      template <typename T>
      using contained_type_t = decltype(*begin(T()));
      
      • contained_type_t 就是一个元函数
        • 它接收一个参数:类型 T
        • 这个元函数将返回包含再 decltype 中的表达式的类型
      • 不会在运行时出现错误
        • 因为在编译时处理的是类型
        • decltype 永远不会执行传递给它的代码,它只返回表达式类型,而不是计算它
      • 存在的第一个问题
        • 类型 T 必须是可默认构建的
          • 可以使用 std::declval()工具代替构造函数调用
            • 它接收任何类型的 T
            • 假装创建一个该类型的实例,以便在需要值而不是类型时,用在元函数中
              • 需要类型这就是 decltype
      • 元函数使用
        template <typename C,
                  typename R = contained_type_t<C>>
        R sum(const C& collection) 
        {
            ...
        }
        
        • 模板不知道返回类型,可以使用元函数给出
      • 虽然称这些函数为元函数,其实就是使用模板定义的函数

    11.1.1 推断类型调试

    • contained_type_t 第二个问题
      • 它所做的并不是用户想要的,如果试图使用它,会出现问题
      • 检测方法
        • 声明一个模板
          • 不需要实现
          • 当需要检查某个类型时,尝试实例化该模板,编译器将报告错误
    • 检查 contained_type_t 推断的类型
      template <typename T>
      class error {};
      error<contained_type_t<std::vector<std::string>>>();
      
    • 会编译错误 (但是我在VS2022中顺利通过编译,暂时忽略,按照书中的逻辑来)
      • contained_type_t 推断类型是字符串的常引用类型,而不是用户想要的字符串类型
    • 完善修正 contained_type_t
      • 移除类型引用部分和 const 修饰符
        • 移除 const 和 volatile 修饰符,使用 std::remove_cv_t 元函数
        • 移除引用使用 std::remove_reference_t 元函数
          template <typename T>
          using contained_type_t = 
                      std::remove_cv_t<
                          std::remove_reference_t<
                            decltype(*begin(std::declval<T>()))
                          >
                      >;
          
      • 大多数标准元函数都定义在此
      • 它包含十几个有用的元函数
      • 用于在元程序中操作类型,模拟 if 语句和逻辑操作
      • 以后缀 _t 结尾的元函数在 C++14 中引入
      • 不带后缀为 C++11 中元函数(笨拙结构)
    • 编写与调试元程序的工具是 static_assert
      static_assert(
              std::is_same<int, contained_type_t<std::vector<int>>>(),
              "std::vector should contain integers"
          );
      
      • 检查编译时的 bool 值,如果为 false,则停止编译
      • std::is_same
        • 表示元相等
        • 接收两个类型,相同返回 true, 否则返回 false

    11.1.2 编译时的模式匹配

    • 自己定义 is_same 元函数
      • 元函数不能返回 true 或 false 值
        • 需要返回 std::true_typestd::false_type
      • 对于 is_same 有两种情况
        • 如果给定两种不同类型,则返回 std::false_type
        • 如果给定两种类型相同,则返回 std::true_type
      • 第一种情况实现
        template <typename T1, typename T2>
        struct is_same : std::false_type{};
        
        • 创建了两个参数的元函数,无论 T1 和 T2 是什么情况,它总返回 std::false_type
      • 第二种情况
        template <typename T>
        struct is_same<T, T> : std::true_type{};
        
      • 示例
        • is_same>>
        • 首先
          • 计算 contained_type_t 的结果,结果为 int
        • 然后
          • 查找所有可用与 的 is_same 的定义,并选择最具体的那个
      • 对文中自定义 is_same 元函数实现的理解
        • 利用了编译器编译模板时的机制
        • 选择更具体地模板进行编译
    • 自定义 remove_reference_t 元函数
      • 有三种情况
        • 给定的类型为非引用类型
        • 给定的类型为左值引用
        • 给定的类型为右值引用
      • 对于第一种情况,应返回未修改的类型
      • 而对第二和第三种情况,需要将引用剥离
        //通常情况下,remove_reference::type 的类型应该是T
        //它接收什么类型就返回什么类型
        template <typename T>
        struct remove_reference {
          using type = T;
        };
        //如果接收左值引用T&,剥离引用返回T 
        template <typename T>
        struct remove_reference <T&>{
          using type = T;
        };
        
        //如果接收右值引用T&&,剥离引用返回T 
        template <typename T>
        struct remove_reference <T&&> {
          using type = T;
        };
        
      • contained_type_t 使用模板类型别名实现的
      • 而这里模板结构定义了内嵌的别名,称为 type
      • 要使用 remove_reference 元函数获取结果类型就必须创建该类型的模板,并获得嵌套在其中的类型定义
        • typename remove_reference::type
      • 这样写比较冗长,可以创建一个方便的 remove_reference_t
        • 类似与 C++ 对 type_traits 中元函数的所做操作
        template <class T>
        using remove_reference_t = typename remove_reference<T>::type;
        

    11.1.3 提供类型的元信息

    • 如果需要查找包含在集合中元素的类型时,最常用的方式是
      • 对集合来说,通常将包含的元素类型作为名为 value_type 的内嵌类型定义提供
      • 示例
        template <typename T, typename E>
        class expected
        {
        public:
          using value_tyoe = T;
        };
        

    11.2 编译时检查类型的属性

    • 如果涉及处理 value_type 的函数,检查给定的集合是否包含内嵌的 value_type 在进行相应的处理
    • 接收任意数目的类型并返回 void 的元函数
      template<typename ...>
      using void_t = void;
      
      • 该元函数的作用不在于它的结果
      • 它可以在编译时的 SFINAE 上下文中检查给定类型和表达式的有效性
        • SFINAE (substitution failure is not an error, 替代失败例程不是错误)
        • SFINAE 是模板重载解析时应用的规则
        • 如果用推断的类型替代模板参数失败,编译器不会报错,只是忽略这一重载
      • C++17 支持 void_t
      • 作用
        • 它可以和任意多的类型一起使用,如果有些类型无效,使用 void_t 的重载则被忽略
        • 可以很容易地创建一个元函数,检查给定类型是否内嵌 value_type
    • 检测类型是否包含 value_type 的元函数
      //通常情况:假设任意类型都没有内嵌 value_type 类型定义
      template <typename C, typename = std::void_t<>>
      struct has_value_type : std::false_type {};
      // 特殊情况,只考虑C::value类型为已存在类型
      //(如果C包含内嵌的value_type类型)
      template <typename C>
      struct has_value_type<C, std::void_t<typename C::value_type>> : std::true_type {};
      
    • 如果有两个 sum 函数
      • 一个处理内嵌 value_type 类型的集合
      • 一个处理任何迭代的集合
      • 就可以使用 has_value_type 判断使用哪个方法
        template <typename C>
        auto sum(const C& collection) 
        {
          if constexpr (has_value_type(collection)) 
          {
            return sum_collection(collection);
          }
          else 
          {
            return sum_iterable(collection);
          }
        }
        
    • constexpr-if
      • 正常的 if 语句在运行时检查它的类型,并把两个分支设置为可编译的
      • constexpr-if 要求两个分支都必须为有效的语法,但不会编译两个分支
    • void_t 不但可以检查类型的有效性,还可以通过 decltype 和 std::declval的帮助对表达式进行检查
      //通常情况:假设任何类型都不可迭代
      template <typename C, typename = std::void_t<>>
      struct is_iterable : std::false_type {};
      //特殊情况:仅考虑C可迭代且其begin迭代器可解引用
      template <typename C>
      struct is_iterable<
          C,
          std::void_t<
            decltype(*begin(std::declval<C>())),
            decltype(end(std::declval<C>()))
            > 
          >
          : std::true_type {};
      
    • 定义完整的 sum 函数
      template <typename C>
      auto sum(const C& collection)
      {
        if constexpr (has_value_type(collection))
        {
          return sum_collection(collection);
        }
        else if constexpr (is_iterable<C>)
        {
          return sum_iterable(collection);
        }
        else 
        {
        
        }
      }
      

    11.3 构造柯里化函数

    • 柯里化函数(略)
      • 前面章节已经做过笔记
    • 柯里化函数需要是有状态的函数对象,需要在 std::tuple 中存储所有捕获的参数
      • 因此需要使用 std::decay_t 保证类型参数不是引用而是实际的值
      • Code_11_3_1
    • std::is_invocable_v 元函数
      • 它接收可调用对象类型和参数类型列表
      • 返回这个对象可否使用参数列表调用

    11.3.1 调用所有可用的

    • std::invoke
      • 第一个参数为可调用对象
        • 无论是普通可调用对象还是成员函数指针
      • 第二个参数传递可调用对象的参数
      • 优势
        • 通用代码,不知道可调用对象的确切类型的时候调用它
      • 实现接收函数作为参数的高阶函数时,不应使用常规函数调用语法,而应使用 std::invoke
    • std::apply
      • 行为与 std::invoke 相似
      • 不同之处
        • 它接收包含参数的元组 (tuple) 作为参数,而不是接收独立的多个参数
    • Code_11_3_1
      • 柯里化的这种实现适用于
        • 普通函数
        • 指向成员函数的指针
        • 普通和通用 lambda
        • 具有调用操作符的类(通用的和非通用的)
        • 甚至对于具有多个不同调用操作符重载的类都是适用的

    11.4 DSL 构建块

    • 领域特定语言(domain-specific language, DSL)
    • 创建 DSL可能如下所示
      with(martha) (name = "Martha", surname = "Jones", age = 42);
      
      • 实现并不一定优美,但主要关注点是隐藏主代码的复杂性
      • 使主程序逻辑尽量简单,而牺牲大多数人看不见的底层部分
    • 思考上面代码的语法
      • 称为函数(或类型)的 with
        • 因为使用参数 martha 调用它,所以它是个函数
        • 调用结果是另一个函数,它应接收任意数目的参数
    • std::is_invocable
      • 检查给定的函数是否可用于特定的参数进行调用
    • std::is_invocable_r
      • 可以检查是否返回期望的类型
    • 实现
      • Code_11_4_1
    • C++ 实现 DSL 提供了方便的支持
      • 运算符重载和可变参数模板是两个主要工具
    • DSL 的优势
      • 主程序代码非常简洁,并可以在不同的事务间进行切换,而不需要修改主程序代码
        • 例如,如果需要把所有记录保存到数据库,只需要实现 transaction 类的调用操作符
          • 程序的其他部分就可以向数据库保存数据了,而不需要修改主程序的任何代码

    总结

    关键 API

    • std::declval()
    • decltype()
    • std::remove_cv_t()
    • std::remove_reference_t()
    • static_assert()
    • std::is_same()
    • std::true_type
    • std::false_type
    • void_t
    • has_value_type
    • constexpr-if
    • std::decay_t
    • std::invoke
    • std::apply
    • std::is_invocable
    • std::is_invocable_r

    思想

    • 柯里化函数 Code_11_3_1
    • DSL Code_11_4_1
  • 相关阅读:
    基于Echarts实现可视化数据大屏大数据可视化
    你不知道的JavaScript---对象
    【分享】订阅金蝶云进销存集简云连接器同步销货数据至金蝶云进销存系统
    SpringBoot+Vue前后端文件传输问题总结
    全同态加密-丁津泰:学习
    【kubernetes的基本API操作】
    Dubbo—— 一个服务既是消费者又是提供者
    同志们,都开什么题啊?
    开源现场总线协议栈
    基于反序位域的大端协议处理方法
  • 原文地址:https://blog.csdn.net/jiamada/article/details/127119006