const lambda中捕获引用的生命周期

6

I have the following api:

old_operation(stream, format, varArgs);

我希望您能够写一个适配器,使得以下调用方式成为可能:
stream << operation(format, varArgs);

为实现此目的,我使用了一个临时对象,存储对varArgs的引用,并重载了operator<<,以应用old_operation(),代码如下:
template<typename ...T>
decltype(auto) storage(T&& ...t) {
   return [&](auto&& f) ->decltype(auto) {
       return std::forward<decltype(f)>(f)(t...);
   };
}

template<typename ...T>
class Operation
{
    using Storage = decltype(storage(std::declval<T>()...));
    public:
       template<typename ...Args>
       explicit Operation(Args&& ...args) : 
               mArgs(storage(std::forward<Args>(args)...)) {};
       template<class StreamType>
       StreamType& Apply(StreamType& stream)
       {
           auto f = [&](auto&& ...xs)
           {
               old_operation(stream, std::forward<decltype(xs)>(xs)...);
           }
           mArgs(f);
           return stream;
       }
   private:
       Storage mArgs;
};

template<typename ...Args>
Operation<Args...> MakeOperation(Args&&... args)
{
    return Operation<Args...>(std::forward<Args>(args)...);
}

template<class StreamType, typename ...Args>
StreamType& operator<<(StreamType& stream, Operation<Args...>&& operation)
{
    return operation.Apply(stream);
}

这很好,但现在我需要在操作调用中添加一些using namespace声明:
假设我有:
namespace X {namespace Y { namespace Z { int formater(double x) { return std::round(x); }}}

我不想为这个调用添加所有命名空间,所以我正在做类似于以下的事情:

#define OPERATION(...) \
   [&]() { \
        using namespace ::X:Y:Z; \
        return Operation("" __VA_ARGS__); }() \

这让我可以做到:

stream << OPERATION(format, formater(2.3));
lambda表达式的问题在于临时变量被创建在不同的作用域中,而Apply()调用却在另一个作用域中,这是未定义行为。
我不知道是否通过将const限定符添加到mArgs中可以延长捕获引用的生命周期,如此处所述。 我不确定是否适用,我假设它们是基于堆栈的引用,并且通过将const限定符添加到mArgs中,该限定符将应用于捕获的引用。

1
使用const &延长临时变量的生命周期只适用于本地函数变量。您不能将临时变量传递给类,然后使用const &持有它。 - NathanOliver
我很担心这个问题,我会继续尝试寻找不同的解决方案。 - dlavila
我在想,为什么像template <typename Stream, typename Format, typename... Rest> Stream& operator<<(Stream& strm, Format&& f, Rest&&... vargs) { old_operation(stm, std::forward<Format>(f), std::forward<Rest>(vargs)...); return strm; }这样简单的东西对你来说不能工作?只是出于好奇问一下。 - Arunmu
运算符 << 必须恰好接受两个参数。 - dlavila
1个回答

4
template<typename ...T>
decltype(auto) storage(T&& ...t) {
  return [&](auto&& f) ->decltype(auto) {
    return std::forward<decltype(f)>(f)(t...);
  };
}

这是一个类似 Haskell 风格的 functor(变长的,不过并不像 Haskell)。它接受 Ts... 并返回一个类型为 ((Ts...)->U)->U 的函数,也就是知道如何在传递给它的参数上评估函数。这使得 storage 的类型为 (Ts...)->( ((Ts...)->U)->U ),有一些代数学的趣味。

我猜你的问题是有一些暂时的变量没有存储。通常情况下,在函数中不存储暂时变量,而返回值又依赖于这些变量的生命周期,会导致脆弱的代码。

如果你有 C++1z 的 experimental::apply,我们可以这样做:

template<class... Ts>
decltype(auto) storage(Ts&&... ts) {
  return
    [tup=std::tuple<Ts...>(std::forward<Ts>(ts)...)]
    (auto&& f)
    ->decltype(auto) mutable
  {
    return std::experimental::apply(decltype(f)(f), std::move(tup));
  };
}

该函数返回一个一次性延迟调用到std::apply。Apply接受一个函数和一个元组,并将元组的参数传递给函数。它正确处理引用和左/右值。同时,元组的容器使捕获更简单,并使我们可以轻松地有条件地存储rvalue,同时将lvalue作为引用保留。
我认为这解决了您的问题,因为临时变量被移动到元组中而不是通过引用进行捕获,而非临时变量则通过引用存储。
应该很容易地找到比我在此处勾画的任何std::experimental::apply实现更好的实现。

不错,我之前也在做类似的事情,但是我在我的“operation”对象中存储的不是带有元组的lambda函数,而是直接存储了元组,然后我在“Apply”调用中直接将元组解包到“old_operation”中,但是我并不完全相信使用元组,因为编译时间太长。 - dlavila

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接