将 std::invoke_result_t 与泛型 lambda 一起使用时出现硬错误

r3m*_*n0x 4 c++ sfinae generic-lambda c++17

我有一个类似容器的类,其方法与std::apply. 我想使用const限定符重载此方法,但是当我尝试使用通用 lambda 调用此方法时,我从std::invoke_result_t. 我std::invoke_result_t用来推断方法的返回值以及对参数执行 SFINAE 检查。

#include <type_traits>
#include <utility>

template <typename T>
class Container
{
public:
    template <typename F>
    std::invoke_result_t<F, T &> apply(F &&f)
    {
        T dummyValue;
        return std::forward<F>(f)(dummyValue);
    }

    template <typename F>
    std::invoke_result_t<F, const T &> apply(F &&f) const
    {
        const T dummyValue;
        return std::forward<F>(f)(dummyValue);
    }
};

int main()
{
    Container<int> c;
    c.apply([](auto &&value) {
        ++value;
    });
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

使用 Clang 6.0 编译时的错误消息:

main.cc:27:9: error: cannot assign to variable 'value' with const-qualified type 'const int &&'
        ++value;
        ^ ~~~~~
type_traits:2428:7: note: in instantiation of function template specialization 'main()::(anonymous class)::operator()<const int &>' requested here
      std::declval<_Fn>()(std::declval<_Args>()...)
      ^
type_traits:2439:24: note: while substituting deduced template arguments into function template '_S_test' [with _Fn = (lambda at main.cc:26:13), _Args = (no value)]
      typedef decltype(_S_test<_Functor, _ArgTypes...>(0)) type;
                       ^
type_traits:2445:14: note: in instantiation of template class 'std::__result_of_impl<false, false, (lambda at main.cc:26:13), const int &>' requested here
    : public __result_of_impl<
             ^
type_traits:2831:14: note: in instantiation of template class 'std::__invoke_result<(lambda at main.cc:26:13), const int &>' requested here
    : public __invoke_result<_Functor, _ArgTypes...>
             ^
type_traits:2836:5: note: in instantiation of template class 'std::invoke_result<(lambda at main.cc:26:13), const int &>' requested here
    using invoke_result_t = typename invoke_result<_Fn, _Args...>::type;
    ^
main.cc:16:10: note: in instantiation of template type alias 'invoke_result_t' requested here
    std::invoke_result_t<F, const T &> apply(F &&f) const
         ^
main.cc:26:7: note: while substituting deduced template arguments into function template 'apply' [with F = (lambda at main.cc:26:13)]
    c.apply([](auto &&value) {
      ^
main.cc:26:23: note: variable 'value' declared const here
    c.apply([](auto &&value) {
               ~~~~~~~^~~~~
Run Code Online (Sandbox Code Playgroud)

我不确定std::invoke_result_tSFINAE是否友好,但我不认为这是这里的问题,因为我尝试用尾随返回类型替换它,例如:

auto apply(F &&f) const -> decltype(std::declval<F>()(std::declval<const T &>()))
Run Code Online (Sandbox Code Playgroud)

并得到了类似的错误:

main.cc:27:9: error: cannot assign to variable 'value' with const-qualified type 'const int &&'
        ++value;
        ^ ~~~~~
main.cc:16:41: note: in instantiation of function template specialization 'main()::(anonymous class)::operator()<const int &>' requested here
    auto apply(F &&f) const -> decltype(std::declval<F>()(std::declval<const T &>()))
                                        ^
main.cc:26:7: note: while substituting deduced template arguments into function template 'apply' [with F = (lambda at main.cc:26:13)]
    c.apply([](auto &&value) {
      ^
main.cc:26:23: note: variable 'value' declared const here
    c.apply([](auto &&value) {
               ~~~~~~~^~~~~
Run Code Online (Sandbox Code Playgroud)

问题:

  1. 为什么会发生这种情况?更准确地说,为什么 lambda 的主体在似乎是重载决议期间被实例化?
  2. 我该如何解决?

Bri*_*ian 5

Lambda 已推导出返回类型,除非您明确指定返回类型。因此,std::invoke_result_t必须实例化主体以确定返回类型。此实例化不在直接上下文中,并导致硬错误。

您可以通过编写以下代码来编译代码:

[](auto &&value) -> void { /* ... */ }
Run Code Online (Sandbox Code Playgroud)

在这里,在 lambda 的主体之前不会实例化 lambda 的主体apply,并且您很清楚。


Yak*_*ont 5

所以这里的重载解析有点愚蠢。

它没有说“好吧,如果不起作用const apply,我永远不会打电话const apply,所以我不会考虑它”。

相反,重载决议会评估每个可能的候选者。然后它消除那些遭受替换失败的人。只有这样,它才会对候选人进行排序并从中挑选一个。

所以这两个都F代入了它们:

template <typename F>
std::invoke_result_t<F, T &> apply(F &&f)

template <typename F>
std::invoke_result_t<F, const T &> apply(F &&f) const
Run Code Online (Sandbox Code Playgroud)

我移走了他们的尸体。

现在,当您将 lambda 类型传递F给这些时会发生什么?

好吧,lambdas 相当于一个auto返回类型。为了在传递某些东西时找出实际的返回类型,编译器必须检查 lambda 的主体。

SFINAE 在检查函数体(或 lambdas)时不起作用。这是为了使编译器的工作更容易(因为 SFINAE 对编译器来说非常困难,让他们必须编译任意代码并遇到任意错误然后回滚它是一个巨大的障碍)。

我们可以通过以下方式避免实例化 lambda 的主体:

[](auto &&value) -> void { /* ... */ }
Run Code Online (Sandbox Code Playgroud)

在你这样做之后,两个重载apply:

template <typename F>
std::invoke_result_t<F, T &> apply(F &&f)

template <typename F>
std::invoke_result_t<F, const T &> apply(F &&f) const
Run Code Online (Sandbox Code Playgroud)

可以评估返回值(它只是void),我们得到:

template <typename F=$lambda$>
void apply(F &&f)

template <typename F=$lambda$>
void apply(F &&f) const
Run Code Online (Sandbox Code Playgroud)

现在,请注意apply const仍然存在。如果您调用apply const,您将得到由实例化该 lambda 体引起的硬错误。

如果您希望 lambda 本身对 SFINAE 友好,您应该需要这样做:

#define RETURNS(...) \
  noexcept(noexcept(__VA_ARGS__)) \
  -> decltype(__VA_ARGS__) \
  { return __VA_ARGS__; }

[](auto &&value) RETURNS(++value)
Run Code Online (Sandbox Code Playgroud)

请注意,此 lambda 略有不同,因为它返回对值的引用。我们可以通过以下方式避免这种情况:

[](auto &&value) RETURNS((void)++value)
Run Code Online (Sandbox Code Playgroud)

现在 lambda 既是 SFINAE 友好的,并且具有与原始 lambda 相同的行为,并且您的原始程序在进行此更改时按原样编译。

这样做的副作用const是现在 SFINAE 从重载解析中消除了 non- apply。这反过来又使它对 SFINAE 友好。

有一个建议采用RETURNS并重命名它=>,但最后我检查了它不被接受。