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

目录

  • 第十章 monad
    • 注意
    • 10.1 仿函数并不是以前的仿函数
      • 10.1.1 处理可选值
    • 10.2 monad: 更强大的仿函数
    • 10.3 基本的例子
    • 10.4 range 与 monad 的嵌套使用
    • 10.5 错误处理
      • 10.5.1 std::optional 作为 monad
      • 10.5.2 expected 作为 monad
      • 10.5.3 try monad
    • 10.6 monad 状态处理
    • 10.7 并发和延续 monad
      • 10.7.1 future 作为 monad
      • 10.7.2 future 的实现
    • 10.8 monad 组合
    • 总结
  • 第十一章 模板元编程
    • 11.1 编译时操作类型
      • 11.1.1 推断类型调试
      • 11.1.2 编译时的模式匹配
      • 11.1.3 提供类型的元信息
    • 11.2 编译时检查类型的属性
    • 11.3 构造柯里化函数
      • 11.3.1 调用所有可用的
    • 11.4 DSL 构建块
    • 总结
      • 关键 API
      • 思想

第十章 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

你可能感兴趣的:(笔记,c++)