2

三天以来,我一直在阅读有关组合器的信息,终于开始用代码编写它们(更像是从不同地方复制东西并理解事物)。

这是我正在尝试运行的一些代码:

#include <iostream>
#include <utility>

template <typename Lambda>
class y_combinator {
  private:
    Lambda lambda;
  public:
    template <typename T>
    constexpr explicit y_combinator (T&& lambda)
        : lambda (std::forward <T> (lambda))
    { }

    template <typename...Args>
    decltype(auto) operator () (Args&&... args) {
        return lambda((decltype(*this)&)(*this), std::forward <Args> (args)...);
    }
};

template <typename Lambda>
decltype(auto) y_combine (Lambda&& lambda) {
    return y_combinator <std::decay_t <Lambda>> (std::forward <Lambda> (lambda));
}

int main () {
    auto factorial = y_combine([&] (auto self, int64_t n) {
        return n == 1 ? (int64_t)1 : n * self(n - 1);
    });
    
    int n;
    std::cin >> n;

    std::cout << factorial(n) << '\n';
}

如果我将 lambda 的返回类型明确声明为-> int64_t,则一切正常。但是,当我删除它时,编译器会抱怨。错误:

main.cpp|16|error: use of 'main()::<lambda(auto:11, int64_t)> [with auto:11 = y_combinator<main()::<lambda(auto:11, int64_t)> >; int64_t = long long int]' before deduction of 'auto'

为什么编译器无法确定返回类型并推断出 auto?我首先想到,也许我需要更改... ? 1 : n * self(n - 1)为,... ? int64_t(1) : n * self(n - 1)以便两个返回值的类型最终都为int64_t,并且不会留下任何可能的歧义。不过,情况似乎并非如此。我错过了什么?

此外,在y_combinator类中,声明lambda为类型的对象Lambda&&似乎会导致问题。为什么会这样?这只发生在我将operator ()重载中的强制转换写为(decltype(*this)&)而不是std::ref(*this). 他们在做不同的事情吗?

4

1 回答 1

3

类型扣除

的类型n == 1 ? (int64_t)1 : n * self(n - 1)取决于 的返回类型self,因此无法推断。您会认为这int64_t是一个显而易见的候选人,但floatdouble同样出色。您不能指望编译器考虑所有可能的返回类型并选择最佳候选者。

要解决此问题而不是使用三元表达式,请使用 if-else 块:

int main () {
    auto factorial = y_combine([&] (auto self, int64_t n) {
        if (n == 1) {
            return (int64_t)1;
        } else {
            return n * self(n - 1);
        }
    });
    // ...
}

有了这个 return 语句不依赖于 的返回类型self,所以可以发生类型推导。

在推导函数的返回类型时,编译器会依次查看函数体中的所有返回语句,并尝试推导它们的类型。如果它失败了,你会得到一个编译错误。

使用三元运算符,return 语句的类型return n == 1 ? (int64_t)1 : n * self(n - 1);取决于 的返回类型self,目前尚不清楚。因此,您会收到编译错误。

当使用 if 语句和多个 return 语句时,编译器可以从它遇到的第一个推断返回类型,因为

如果有多个 return 语句,它们必须都推导到相同的类型。

一旦在函数中看到 return 语句,从该语句推导出的返回类型就可以在函数的其余部分中使用,包括在其他 return 语句中。

cppreference所示。这就是为什么

        if (n == 1) {
            return (int64_t)1;
        } else {
            return n * self(n - 1);
        }

可以推断返回一个int64_t.

作为旁注

        if (n == 1) {
            return 1; // no explicit cast to int64_t
        } else {
            return n * self(n - 1);
        }

将无法编译,因为从第一个 return 语句中,函数的返回类型将被推断为int,而从第二个 return 语句中推断为int64_t

        if (n != 1) {
            return n * self(n - 1);
        } else {
            return (int64_t)1;
        }

也会失败,因为它遇到的第一个 return 语句取决于函数的返回类型,因此无法推断。

第二个问题

发生该错误是因为在调用时您由于 lambda 的参数而lambda试图制作副本。这是由于有一个右值引用成员。(请参阅 clang 在Godbolt上的重大错误消息)*thisauto self

要解决此问题,请使用std::ref原始问题中的提及或使 lambda 具有auto &&self参数(并使用std::forward<decltype(self)>(self))。

另请注意,该成员Lambda &&lambda是右值引用,因此您只能y_combinator使用临时或移动的 lambda 构造 的实例。通常复制函子没什么大不了的,标准库也通过复制获取函子参数。

于 2020-11-10T23:22:38.273 回答