C++11 lambda 和参数包

C++11 lambdas and parameter packs

我遇到的问题与 this question 基本相同,但不幸的是,唯一的 posted 答案现在已失效 link。

具体来说,使用 VS2013 Update 4,我试图编译以下代码,但它不合作:

template<typename T, typename... Params>
auto PostWeakTask(const boost::weak_ptr<IWorkQueue>& queue,
                  void (T::*member)(Params...),
                  const boost::weak_ptr<T>& weak)
    -> std::function<void (Params&&...)>
{
    return [queue, member, weak](Params&&... params)
    {
        if (auto qp = queue.lock())
        {
            qp->Post([weak, member, params]()
            {
                if (auto strong = weak.lock())
                {
                    ((*strong).*member)(std::forward<Params>(params)...);
                }
            });
        }
    };
}

(如所写,params 的捕获因 C3481: 'params': lambda capture variable not found 而失败。如果我尝试使用 = 的隐式捕获,它会显示 C2065: 'params' : undeclared identifier。如果我尝试params..., 我得到 C3521: 'params' is not a parameter pack.)

这个想法当然是 return 一个函数,当被调用时 post 一个带有任意参数的成员函数到一个只接受 void() 任务的工作队列,并且只在尚未执行时保持对队列和任务的弱引用。

不过,我认为此处的代码实际上没有任何错误。我想我已经找到了使用 bind 而不是 lambda 的解决方法,但奇怪的是它似乎只适用于 std::function 而不是 boost::function。 (使用 Boost 1.55。我怀疑这可能是 pre-rvalue-ref 支持?)

(附带说明,我最初尝试使用 decltype([](Params&&...){}) 作为 return 类型,因为这看起来更自然。但它会使编译器崩溃。)


解决方法也有点奇怪。也许我应该把这个作为一个单独的问题来问,因为它主要与完美转发部分有关,但是:

template<typename T, typename... Params>
auto PostWeakTask(const boost::weak_ptr<IWorkQueue>& queue,
                  void (T::*member)(Params...),
                  const boost::weak_ptr<T>& weak)
    -> std::function<void (Params...)>
{
    struct WeakCaller
    {
        typedef void (T::*member_type)(Params...);
        typedef boost::weak_ptr<T> weak_type;

        WeakCaller(member_type member, const weak_type& weak)
            : m_member(member), m_weak(weak) {}

        void operator()(Params&&... params)
        {
            if (auto strong = m_weak.lock())
            {
                ((*strong).*m_member)(std::forward<Params>(params)...);
            }
        }

    private:
        member_type m_member;
        weak_type m_weak;
    };

    return [=](Params&&... params)
    {
        if (auto qp = queue.lock())
        {
            qp->Post(std::bind(WeakCaller(member, weak),
                     std::forward<Params>(params)...));
        }
    }
}

这似乎应该有效,但是当我尝试调用它时(使用 void (tribool,tribool,const std::string&) 方法)我收到一个绑定错误,表明 tribool 参数与 [=26= 不兼容] 一.

(具体来说:C2664: 'void WeakCaller<T,boost::logic::tribool,boost::logic::tribool,const std::string &>::operator ()(boost::logic::tribool &&,boost::logic::tribool &&,const std::string &)' : cannot convert argument 1 from 'boost::logic::tribool' to 'boost::logic::tribool &&'。)

我认为以这种方式使用右值引用的部分原因是它们应该可以完美转发而不需要多次重载?

我可以通过让 WeakCaller 变成 void operator()(Params... params) 来编译它,但这不会破坏完美转发吗? (奇怪的是,将 && 留在顶级 lambda 似乎没问题......我不确定 std::function 签名和 lambda 签名之间的不匹配是否正常。)


仅供参考,这是我现在使用的最终版本,经过调整 :

namespace detail
{
    template<size_t... Ints> struct index_sequence
    {
        static size_t size() { return sizeof...(Ints); }
    };

    template<size_t Start, typename Indices, size_t End>
    struct make_index_sequence_impl;

    template<size_t Start, size_t... Indices, size_t End>
    struct make_index_sequence_impl<Start, index_sequence<Indices...>, End>
    {
        typedef typename make_index_sequence_impl<
            Start + 1, index_sequence<Indices..., Start>, End>::type type;
    };

    template<size_t End, size_t... Indices>
    struct make_index_sequence_impl<End, index_sequence<Indices...>, End>
    {
        typedef index_sequence<Indices...> type;
    };

    template <size_t N>
    using make_index_sequence = typename
        make_index_sequence_impl<0, index_sequence<>, N>::type;

    template<typename... Ts>
    using index_sequence_for = make_index_sequence<sizeof...(Ts)>;

    template<typename... Ts>
    struct MoveTupleWrapper
    {
        MoveTupleWrapper(std::tuple<Ts...>&& tuple)
            : m_tuple(std::move(tuple)) {}
        MoveTupleWrapper(const MoveTupleWrapper& other)
            : m_tuple(std::move(other.m_tuple)) {}
        MoveTupleWrapper& operator=(const MoveTupleWrapper& other)
            { m_tuple = std::move(other.m_tuple); }

        template<typename T, typename... Params>
        void apply(void (T::*member)(Params...), T& object) const
        {
            applyHelper(member, object, index_sequence_for<Ts...>());
        }

        template<typename T, typename... Params, size_t... Is>
        void applyHelper(void (T::*member)(Params...), T& object,
                         index_sequence<Is...>) const
        {
            (object.*member)(std::move(std::get<Is>(m_tuple))...);
        }

    private:
        mutable std::tuple<Ts...> m_tuple;
    };

    template<typename... Ts>
    auto MoveTuple(Ts&&... objects)
        -> MoveTupleWrapper<std::decay_t<Ts>...>
    {
        return std::make_tuple(std::forward<Ts>(objects)...);
    }

    template<typename T, typename... Params>
    struct WeakTaskPoster
    {
        WeakTaskPoster(const boost::weak_ptr<IWorkQueue>& queue,
                       void (T::*member)(Params...),
                       const boost::weak_ptr<T>& weak) :
            m_queue(queue),
            m_member(member),
            m_weak(weak)
        {}

        template<typename... XParams>
        void operator()(XParams&&... params) const
        {
            if (auto qp = m_queue.lock())
            {
                auto weak = m_weak;
                auto member = m_member;
                auto tuple = MoveTuple(std::forward<XParams>(params)...);
                qp->Post([weak, member, tuple]()
                {
                    if (auto strong = weak.lock())
                    {
                        tuple.apply(member, *strong);
                    }
                });
            }
        }

    private:
        boost::weak_ptr<IWorkQueue> m_queue;
        void (T::*m_member)(Params...);
        boost::weak_ptr<T> m_weak;
    };
}

template<typename T, typename... Params>
auto PostWeakTask(const boost::weak_ptr<IWorkQueue>& queue,
                  void (T::*member)(Params...),
                  const boost::weak_ptr<T>& weak)
    -> detail::WeakTaskPoster<T, Params...>
{
    return { queue, member, weak };
}

我想它仍然不是真正通用的,因为它不适用于左值引用,但由于这些在延迟回调上下文中是危险的,所以这并不是那么重要。

(虽然,如果您真的急于传递左值引用,那么如果您使用 std::ref(x) 调用并接收为 std::reference_wrapper<T>(或 const&&&)。可能有一些额外的魔法可以使它变得透明,但我真的不需要它,所以我没有调查。)

让事情顺利进行是我的目标。

我会攻击这个:

qp->Post([weak, member, params]()
{
  if (auto strong = weak.lock()) {
    ((*strong).*member)(std::forward<Params>(params)...);
  }
});

首先,让我们params进入lambda。

auto tParams = std::make_tuple( std::forward<Params>(params)... );
qp->Post([weak, member, tParams]()
{
  if (auto strong = weak.lock()) {
    ((*strong).*member)(std::forward<Params>(params)...);
  }
});

但是,您会注意到现在正文无法编译!

然而,我们更接近了。

template<class T, class M>
struct member_invoke_t;
template<class T, class R, class...Args>
struct member_invoke_t<T*, R(::*)(Args...)> {
  T* t;
  R(::*m)(Args...);
  template<class...Ts>
  R operator()(Ts&&...ts)const{
    return (t->*m)(std::forward<Ts>(ts)...);
  }
};
template<class T, class M>
member_invoke_t<T*, M*> member_invoke(T* t, M*m) {
  return {t, m};
};

那我们只需要写std::apply

qp->Post([weak, member, tParams]()mutable
{
  if (auto strong = weak.lock())
    std::apply( member_invoke(strong.get(), member), std::move(tParams) );
}

这应该是可行的。 (上面的代码假定传递给 Post 的 lambda 只会被顺便调用一次——因此 movemutable)。

std::apply 有一个可能的 C++14 实现 over here。转换为 C++11 需要编写 index_sequence 并将 decltype(auto) 替换为明确的 ->decltype(expression) 我认为。无论如何,这是我将在此处跳过的子问题。

确实给了我一些灵感,让我对上面发布的解决方法稍作改动:

template<typename T, typename... Params>
struct WeakCaller
{
    typedef void (T::*member_type)(Params...);
    typedef boost::weak_ptr<T> weak_type;

    WeakCaller(member_type member, const weak_type& weak)
        : m_member(member), m_weak(weak) {}

    template<typename... Args>
    void operator()(Args&&... params)
    {
        if (auto strong = m_weak.lock())
        {
            ((*strong).*m_member)(std::forward<Args>(params)...);
        }
    }

private:
    member_type m_member;
    weak_type m_weak;
};

template<typename T, typename... Params>
auto PostWeakTask(const boost::weak_ptr<IWorkQueue>& queue,
                  void (T::*member)(Params...),
                  const boost::weak_ptr<T>& weak)
    -> std::function<void (Params...)>
{
    return [=](Params&&... params)
    {
        if (auto qp = queue.lock())
        {
            qp->Post(std::bind(WeakCaller<T, Params...>(member, weak),
                     std::forward<Params>(params)...));
        }
    }
}

这似乎可以按预期编译和工作,尽管我还没有用 move-only 类型测试它(我怀疑它们会因为使用 bindfunction 而出现问题).我不太确定为什么需要重新模板化运算符,但它似乎绕过了我在使用 && params.

时遇到的编译器错误

我也不确定PostWeakTask的return类型应该是std::function<void(Params...)>还是std::function<void(Params&&...)>;似乎两种方式都可以编译。

I thought part of the point of using rvalue references this way was that they were supposed to forward perfectly without needing multiple overloads?

不,右值引用和转发引用是两个不同的东西。它们都涉及 && 但它们的行为不同。转发引用仅在存在模板参数推导(或 auto 类型推导,使用相同规则)的情况下发生。

这是转发参考:

template<typename T>
void foo(T&& v);

模板参数推导会让T成为任何类型;如果 T 是左值引用,则 v 是左值引用,否则 v 是右值引用,因为引用折叠。

这不是转发参考:

void foo(int&& v);

这里没有模板参数推导,所以 v 是对 int 的普通右值引用,它只能绑定到右值。

这也不是转发参考:

template<typename T>
struct Foo
{
    void bar(T&& v);
};

同样是因为没有模板参数推导。函数 Foo<int>::bar 不是模板,它是一个普通函数,采用普通右值引用参数,只能绑定到右值。函数 Foo<int&>::bar 是一个普通函数,采用普通左值引用参数,只能绑定到左值。

On a side note, I originally tried to use decltype([](Params&&...){}) as the return type, as this seems more natural.

那是行不通的。每个 lambda 表达式都表示一个唯一的类型,因此 decltype([]{})decltype([]{}) 是两个不同的、不相关的类型。

这是一个可能的解决方案。唯一使用的 C++14 功能是 std::index_sequence,为此您可以 Google 一个 C++11 实现。

template<typename... Ts>
struct MoveTupleWrapper
{
    MoveTupleWrapper(std::tuple<Ts...>&& tuple) : m_tuple(tuple) {}
    MoveTupleWrapper(MoveTupleWrapper& other) : m_tuple(std::move(other.m_tuple)) {}
    MoveTupleWrapper& operator=(MoveTupleWrapper& other) { m_tuple = std::move(other.m_tuple); }
    std::tuple<Ts...> m_tuple;

    template<typename T, typename... Params>
    void apply(void (T::*member)(Params...), T& object)
    {
        applyHelper(member, object, std::index_sequence_for<Ts...>);
    }
    template<typename T, typename... Params, size_t... Is>
    void applyHelper(void (T::*member)(Params...), T& object, std::index_sequence<Is...>)
    {
        (object.*member)(std::move(std::get<Is>(m_tuple))...);
    }
};

template<typename... Ts>
auto MoveTuple(Ts&&... objects)
    -> MoveTupleWrapper<std::decay_t<Ts>...>
{
    return std::make_tuple(std::forward<Ts>(objects)...);
}

template<typename T, typename... Params>
struct WeakTaskPoster
{
    WeakTaskPoster(const boost::weak_ptr<IWorkQueue>& queue,
                   void (T::*member)(Params...),
                   const boost::weak_ptr<T>& weak) :
        m_queue(queue),
        m_member(member),
        m_weak(weak)
    {}
    boost::weak_ptr<IWorkQueue> m_queue;
    void (T::*m_member)(Params...);
    boost::weak_ptr<T> m_weak;

    template<typename... XParams>
    void operator()(XParams&&... params) const
    {
        if (auto qp = m_queue.lock())
        {
            auto weak = m_weak;
            auto member = m_member;
            auto tuple = MoveTuple(std::forward<XParams>(params)...);
            qp->Post([weak, member, tuple]() mutable
            {
                if (auto strong = weak.lock())
                {
                    tuple.apply(member, *strong);
                }
            });
        }
    }
};

template<typename T, typename... Params>
auto PostWeakTask(const boost::weak_ptr<IWorkQueue>& queue,
                  void (T::*member)(Params...),
                  const boost::weak_ptr<T>& weak)
    -> WeakTaskPoster<T, Params...>
{
    return { queue, member, weak };
}

注意WeakTaskPoster::operator()是一个模板,所以我们得到完美转发。执行此操作的 C++14 方法将是一个通用的 lambda。然后就是这个 MoveTuple 东西。这是一个 std::tuple 的包装器,它简单地根据移动实现复制,因此我们可以解决缺少 C++14 lambda 移动捕获的问题。它还准确地实现了 std::apply 的子集,这是必要的。

这与 Yakk 的答案非常相似,只是参数元组被移动而不是复制到 lambda 的捕获集中。