【问题标题】:What are the type conversion rules for parameters and return values of lambdas?lambda 的参数和返回值的类型转换规则是什么?
【发布时间】:2016-07-11 08:35:03
【问题描述】:

我最近对 ​​lambda 可以分配给 std::functions 并略有不同签名这一事实感到惊讶。 略有不同意味着当 function 被指定为返回 void 时,lambda 的返回值可能会被忽略,或者参数可能是 function 中的引用但 lambda 中的值。

请参阅this example (ideone),我在其中强调了我怀疑不兼容的内容。我认为返回值不是问题,因为您总是可以调用函数并忽略返回值,但是从引用到值的转换对我来说看起来很奇怪:

int main() {
    function<void(const int& i)> f;
    //       ^^^^ ^^^^^    ^
    f = [](int i) -> int { cout<<i<<endl; return i; };
    //     ^^^    ^^^^^^
    f(2);
    return 0;
}

次要问题是:为什么这段代码可以编译和工作?主要问题是:与std::function一起使用时,lambda参数和返回值类型转换的一般规则是什么?

【问题讨论】:

  • 另外:我在这里描述的内容是否有一个特定的术语,或者 类型转换 是否正确?
  • 允许将非空函数存储在 std::function&lt;void(blah)&gt; 中是 GCC 始终支持的,现在是必需的,请参阅 cplusplus.github.io/LWG/lwg-defects.html#2420
  • “主要问题是:lambda 参数和返回值的类型转换的一般规则是什么?” 不,你的问题是关于在std::function 中使用 lambda,这与关于 lambda 返回类型的一般规则不同。
  • 事实上它根本不特定于 lambda,因为完全相同的规则适用于存储在 std::function 中的普通函数。普通函数也有参数和返回类型,通过std::function调用也可以进行转换。
  • 那是因为std::function 不是函数指针,也不是虚拟覆盖。它包装了一个可调用对象,添加了一个间接级别,并且可以在该间接级别进行转换。直接类比见我的回答。

标签: c++ c++11 lambda type-conversion


【解决方案1】:

当该签名的 lambda 为 Lvalue-Callable 时,您可以 assign a lambda to a function object 类型为 std::function&lt;R(ArgTypes...)&gt;。反过来,Lvalue-Callable 是根据 INVOKE operation 定义的,这意味着当 lambda 是左值并且它的所有参数都是所需类型和值的值时,它必须是可调用的类别(好像每个参数都是调用空函数的结果,该参数类型作为其签名中的返回类型)。

也就是说,如果你给你的 lambda 一个 id 以便我们可以引用它的类型,

auto l = [](int i) -> int { cout<<i<<endl; return i; };

要将其分配给function&lt;void(const int&amp;)&gt;,表达式

static_cast<void>(std::declval<decltype(l)&>()(std::declval<const int&>()))

必须格式正确。

std::declval&lt;const int&amp;&gt;() 的结果是对const int 的左值引用,但是将其绑定到int 参数没有问题,因为这只是lvalue-to-rvalue conversion,即considered an exact match 用于重载分辨率:

return l(static_cast<int const&>(int{}));

如您所见,返回值被丢弃if the function object signature has return type void;否则,返回类型必须是可隐式转换的。正如 Jonathan Wakely 指出的那样,C++11 在这方面的表现并不令人满意(Using `std::function<void(...)>` to call non-void function;Is it illegal to invoke a std::function<void(Args...)> under the standard?),但它已经在LWG 2420; 中得到了修复。该决议作为 C++14 的发布后修复应用。大多数现代 C++ 编译器将提供 C++14 行为(经修订)作为扩展,即使在 C++11 模式下也是如此。

【讨论】:

  • @skypjack 这是void 返回类型的特例,是的(因为您不能将值隐式转换为void)。但我认为 OP 更关心参数转换的行为以及这些行为在哪里可以。
  • 有道理。我只是证明具体情况是什么,仅此而已。 ;-)
  • @skypjack 好的,我明白你的意思。我已将链接添加到最后一段。
  • 注:该问题标记为 C++11,您正在链接到将成为 C++17 的工作草案,并引用了 2015 年更改的文本(请参阅LWG 2420)。这值得一提,因为 eel.is/c++draft 不是标准。
  • declval&lt;T&gt;() 可以是 xvalue、lvalue 或(恰好在四种情况下)prvalue。没有“const int&amp; 类型的纯右值”这样的东西; declval&lt;const int&amp;&gt;()const int 类型的左值。
【解决方案2】:

从引用到值的转换对我来说看起来很奇怪

为什么?

这看起来也很奇怪吗?

int foo(int i) { return i; }

void bar(const int& ir) { foo(ir); }

这完全一样。通过值获取 int 的函数被另一个函数调用,通过 const-reference 获取 int

bar 内部,变量ir 被复制,返回值被忽略。这正是std::function&lt;void(const int&amp;)&gt; 内部发生的情况,当它有一个带有签名int(int) 的目标时。

【讨论】:

    【解决方案3】:

    赋值运算符被定义为有效果:

    function(std::forward<F>(f)).swap(*this);
    

    14882:2011 20.8.11.2.1 par. 18

    this 引用的构造函数,

    template <class F> function(F f);
    

    要求:

    F 应为CopyConstructible。对于参数类型 ArgTypes 和返回类型 Rf 应为 Callable (20.8.11.2)。

    其中Callable定义如下:

    F 类型的可调用对象f 对于参数类型ArgTypes 和返回类型RR,如果表达式INVOKE(f, declval&lt;ArgTypes&gt;()..., R) 被视为未计算的操作数(第5 条),则格式正确(20.8.2)。

    INVOKE又是defined as

    1. 定义 INVOKE(f, t1, t2, ..., tN) 如下:

      • ... 这里省略的成员函数处理案例 ...
      • f(t1, t2, ..., tN) 在所有其他情况下。
    2. 如果Rvoid,则将INVOKE(f, t1, t2, ..., tN, R) 定义为static_cast<void>(INVOKE(f, t1, t2, ..., tN)),否则将INVOKE(f, t1, t2, ..., tN) 隐式转换为R

    由于INVOKE 定义变成一个普通函数调用,在这种情况下,参数可以是converted:如果您的std::function&lt;void(const int&amp;)&gt; 接受const int&amp;,那么它可以转换为int 以进行调用.下面的例子用clang++ -std=c++14 -stdlib=libc++ -Wall -Wconversion编译:

    int main() {
        std::function<void(const int& i)> f;
        f = [](int i) -> void { std::cout << i << std::endl; };
        f(2);
        return 0;
    }
    

    返回类型 (void) 由 static_cast&lt;void&gt; 的特殊情况处理 INVOKE 定义。

    但是请注意,在撰写本文时,使用clang++ -std=c++1z -stdlib=libc++ -Wconversion -Wall 编译时会产生错误,但使用clang++ -std=c++1z -stdlib=libstdc++ -Wconversion -Wall 编译时不会:

    int main() {
        std::function<void(const int& i)> f;
        f = [](int i) -> int { std::cout << i << std::endl; return i;};
        f(2);
        return 0;
    }
    

    这是由于 libc++ 实现了 C++14 中指定的行为,而不是上面描述的 amended behaviour(感谢 @Jonathan Wakely 指出这一点)。 @Arunmu 描述了在 his post 中负责同一事物的 libstdc++ 类型特征。在这方面,在处理具有 void 返回类型的可调用对象时,实现的行为可能会略有不同,具体取决于它们是实现 C++11、14 还是更新的版本。

    【讨论】:

    • 感谢您的回答!我真的很喜欢您引用标准的事实,但是@ecatmur 的回答以稍微更简洁的方式解释了相同的事情,而没有牺牲任何要点,所以我接受了那个。但是恕我直言,您的回答也应该得到同样多的支持:-)
    • libc++ 行为不是错误,因为 libc++ 实现了 C++14 规则,而不是您链接到的工作草案中的规则。规则更改为cplusplus.github.io/LWG/lwg-defects.html#2420
    • @JonathanWakely 感谢您指出这一点。我已经修改了答案以反映这一点。
    【解决方案4】:

    只是添加到@ecatmur 答案,g++/libstd++ 只是选择忽略callable 的返回值,这与忽略常规代码中的返回值一样正常:

    static void
    _M_invoke(const _Any_data& __functor, _ArgTypes&&... __args)
    {
       (*_Base::_M_get_pointer(__functor))(  // Gets the pointer to the callable
               std::forward<_ArgTypes>(__args)...);
    }   
    

    在 libstd++ 中明确允许这样做的类型特征是:

    template<typename _From, typename _To>     
    using __check_func_return_type = __or_<is_void<_To>, is_convertible<_From, _To>>;
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-02-16
      • 2011-12-18
      • 2019-12-01
      • 2012-04-15
      相关资源
      最近更新 更多