模板化的 requires-expression

Templated requires-expression

我想要 C++20 中的 Functor 概念。

仿函数是一种可以映射的高级类型。一个简单的例子是std::optional;使用从类型 A 到类型 B 和类型 std::optional<A> 的函数,您可以通过将函数应用于值(如果存在)并返回空 optional 否则。此操作在 Haskell.

中称为 fmap
template<typename A, typename B>
std::optional<B> fmap(std::function<B(A)> f, std::optional<A> fa) {
    if (!fa) {
        return std::optional<B>{};
    }
    return std::optional<B>(f(*fa));
}

所有仿函数的概念很简单,可以编写。我想出了这个(使用 GCC——你必须删除 bool 才能在 Clang 中工作,我认为):

template<template<typename> typename F, typename A, typename B>
concept bool Functor = requires(std::function<B(A)> f, F<A> fa) {
    { fmap(f, fa) } -> F<B>;
};

还有一个简单的附加函数来确保它有效:

template<typename A, typename B>
std::function<B(A)> constant(B b) {
    return [b](A _) { return b; };
}

template<template<typename> typename F, typename A, typename B>
F<B> replace(B b, F<A> fa) requires Functor<F,A,B> {
    return fmap(constant<A,B>(b), fa);
}

有效。但这并不漂亮。我想要的是 replace 的签名如下所示:

template<Functor F, typename A, typename B>
F<B> replace(B b, F<A> fa);

这里不需要要求子句。好多了,你不同意吗?然而,为了让它发挥作用,我必须将我的概念模板缩减为单个参数。像这样:

template<template<typename> typename F>
concept bool Functor = requires(function<B(A)> f, F<A> fa) {    // Uh-oh
    { fmap(f, fa) } -> F<B>;
};

问题是,我还没有声明类型 A 和 B。据我所知,在必须使用它们之前,我无法在任何地方声明它们。我想做的能不能做到,能不能做到简单优雅?

我想到的一个可能的解决方案是使概念中的 requires-expression 成为模板(或至少是类似模板的东西)。然后我会有这样的东西:

template<template<typename> typename F>
concept bool Functor = requires<typename A, typename B>(function<B(A)> f, F<A> fa) {
    { fmap(f, fa) } -> F<B>;
};

不幸的是,这在 C++20 标准中是无效的,并且不会用 g++-8 编译。这样的事情可行吗?它能成为标准吗?

您的“Functor”概念代表三种不同类型之间的复杂关系:F(一个单参数模板,即被投影的模板),A(起始对象类型)和 B(结果对象类型)。您的概念代表了 3 个参数之间的关系,因此您的概念将必须采用 3 个参数。

简洁的模板语法适用于简单的情况:与单个(类型)参数相关的约束。您的情况并不简单,因此您将不得不用 requires 子句将其拼写出来。任何时候你有一个像这样的多个参数的概念,你都必须把它拼出来。

至于是不是"pretty",那是价值判断。但鉴于此处显示的复杂关系,看到它的详细说明可以清楚地了解所有这些参数之间的关系。清晰自有其美。

C++ 没有像这样的参数多态性——你不能像 "for any type" 那样以你想要的方式做事,也不能像 Haskell 那样做。我认为这在存在超载的世界中根本不可能。

你有以下内容(我继续删除了错误的 bool,它不是 C++20 概念的一部分,并修复了 -> Type,它也被删除了) :

template<template<typename> class F, typename A, typename B>
concept Functor = requires(std::function<B(A)> f, F<A> fa) {
    { fmap(f, fa) } -> std::same_as<F<B>>;
};

你想说的是对于任何类型ab,给定一个a -> b,你可以调用这个函数。我们不能那样做。但是我们可以自己选择任意类型。一种方法是选择仿函数实现不知道的秘密类型:

namespace secret {
    struct A { };
    struct B { };

    template <typename From, typename To>
    struct F {
        auto operator()(From) const -> To;
    };
}

template <template <typename> class F>
concept Functor = requires(secret::F<secret::A, secret::B> f, F<secret::A> fa) {
    { fmap(f, fa) } -> std::same_as<F<secret::B>>;
};

这可能是您最好的选择。您甚至可以添加多个 a/b 对,以使其更有可能是正确的。

无论如何,这个:

template<Functor F, typename A, typename B>
F<B> replace(B b, F<A> fa);

无论如何都不会发生,因为我们没有那种用于受限模板模板参数的简洁语法。你必须这样写:

template <template <typename> class F, typename A, typename B>
    requires Functor<F>
F<B> replace(B b, F<A> fa);

作为旁注,这是 optionalfmap 的错误实现:

template<typename A, typename B>
std::optional<B> fmap(std::function<B(A)> f, std::optional<A> fa);

取一个 std::function<Sig> 意味着这 只有 如果你传入 特别地 一个 std::function 就可以工作。不适用于 lambda 或函数指针或其他函数对象(如我之前使用的 secret::F)。即使它确实有效,你也不想这样做,因为这是不必要的开销。

你想要:

template <typename F, typename A, typename B = std::invoke_result_t<F&, A const&>>
std::optional<B> fmap(F f, std::optional<A> fa);

关于这个确切的问题,我有一个完整的 post,Declarations Using Concepts