将 lambda 参数完美转发给成员函数,其中成员函数是非类型模板形参

Posted

技术标签:

【中文标题】将 lambda 参数完美转发给成员函数,其中成员函数是非类型模板形参【英文标题】:Perfect forwarding of lambda arguments to member function, where member function is a non-type template parameter 【发布时间】:2018-06-06 15:52:41 【问题描述】:

上下文

我想将一个成员函数和一个特定对象包装成一个函数对象(稍后我将用作回调)。我想为不同的成员函数和对象编写一次这个包装函数,特别是因为我的实际 lambda 在调用包装方法之前做了一些额外的工作。以下是一些可能的实现:

#include <iostream>
#include <string>
#include <utility>

template <class ClassT, class... ArgsT>
auto getCallbackPtr(ClassT* obj, void(ClassT::* memfn)(ArgsT...))

    return [obj, memfn](ArgsT&&... args) 
        (obj->*memfn)(std::forward<ArgsT>(args)...);
    ;

template <auto memFn, class ClassT>
auto getCallbackTemplate(ClassT* obj)

    return [obj](auto&&... args)
        return (obj->*memFn)(std::forward<decltype(args)>(args)...);
    ;

template <auto memFn, class ClassT, class... ArgsT>
auto getCallbackRedundant(ClassT* obj)

    return [obj](ArgsT&&... args)
        return (obj->*memFn)(std::forward<ArgsT&&>(args)...);
    ;


// Example of use
class Foo 
public:
    void bar(size_t& x, const std::string& s)  x=s.size(); 
;
int main() 
    Foo f; 
    auto c1 = getCallbackPtr(&f, &Foo::bar);
    size_t x1; c1(x1, "123"); std::cout << "c1:" << x1 << "\n";
    auto c2 = getCallbackTemplate<&Foo::bar>(&f);
    size_t x2; c2(x2, "123"); std::cout << "c2:" << x2 << "\n";
    auto c3 = getCallbackRedundant<&Foo::bar, Foo, size_t&, const std::string&>(&f);
    size_t x3; c3(x3, "123"); std::cout << "c3:" << x3 << "\n";

问题(简​​而言之)

我想要一个结合以上三个功能不同方面的功能:

它应该将成员函数作为编译时模板参数,不像getCallbackPtr()。 它的operator() 不应该是模板函数,不像getCallbackTemplate()。 与getCallbackRedundant()不同,它的模板参数(成员函数指针除外)应该从函数使用中推断出来。

一些细节

以下是我希望成员函数成为模板参数的原因,尽管我必须承认这些在实践中可能不会产生明显的影响:

优化器可能会直接调用成员函数,而不是通过函数指针。事实上,由于这是调用成员函数的唯一位置,它甚至可能被编译器内联到 lambda 中。 生成的函数对象更小(一个指针而不是一个指针加一个成员函数指针),因此更适合std::function 的占用空间(小对象优化)。

以下是getCallbackTemplate() 的问题,它有一个模板化的operator()

它不适用于 Visual Studio。这对我来说是一个表演终结者。 (错误为error C3533: a parameter cannot have a type that contains 'auto',参考template &lt;auto memFn, class ClassT&gt;。) 如果传入了错误类型的参数,我怀疑它会比非模板化的operator() 产生更复杂和令人困惑的编译器错误(诚然,这只是一种预感)。 模板化的operator() 不能接受参数的初始化列表。这对我来说根本不是问题,但我要记录在案。

我认为需要推断模板参数的原因相当清楚:getCallbackRedundant() 冗长且难以使用。

这可以吗?怎么样?

【问题讨论】:

我想将一个成员函数和一个特定对象包装到一个函数对象中 - 你想重新发明std::function吗? @SergeyA std::function 可以从成员函数构造,在这种情况下,第一个参数必须是适当的对象。但是你不能直接从成员函数*和对象*构造std::function @ArthurTacca 当然可以,只需将 lambda 传递给 std::function 构造函数。而且 lambda 比通过成员函数指针调用更小更高效。 亚瑟,你可以用它来捕获 lamda 或 std::bind(后者真的很老派) “它应该将成员函数作为编译时模板参数,这与 getCallbackPtr 不同。” - 您将无法将成员函数指针作为单个模板参数,除非你可以使用auto,这显然VC不支持。 【参考方案1】:

推断参数的一种简单方法是使用部分模板特化。

在本例中,我通过将非类型成员函数指针转发给自定义函子来解决问题,然后返回。

部分地专注于类型,然后剩下的就是直截了当。

#include <iostream>
#include <string>

template <auto memFnPtr, class memFn>
struct getCallbackTemplate;

template <auto memFnPtr, class Ret, class ClassT, class... Args>
struct getCallbackTemplate<memFnPtr, Ret(ClassT::*)(Args...)>

    getCallbackTemplate (ClassT* obj) : m_obj(obj) 

    Ret operator()(Args... args) 
        return (m_obj->*memFnPtr)(std::forward<Args>(args)...);
    

    ClassT* m_obj;
;

template <auto memFn, class ClassT>
auto getCallback(ClassT* obj) 
    return getCallbackTemplate<memFn, decltype(memFn)>(obj);


class Foo 
public:
    void bar(std::size_t& x, const std::string& s)  x=s.size(); 
;

int main() 
    Foo f; 
    auto c1 = getCallback<&Foo::bar>(&f);
    size_t x1; c1(x1, "123"); std::cout << "c1:" << x1 << "\n";

【讨论】:

你需要在operator()forward,否则当成员函数按值取参数时,你可能会做无用的复制,如果参数是右值,这将不起作用。 @Holt 你确定吗?即使参数是右值引用,它似乎也能工作,如果我们按值传递,某种类型的转换真的会优化任何东西吗?注意Args是从成员函数声明中推导出来的,而不是从传入的参数中推导出来的。 自己看:godbolt.org/g/YV3ojF我知道Args是从结构参数推导出来的。假设您有一个参数T,它可以(至少)是XX &amp;X const&amp;X &amp;&amp;。使用X&amp;X const&amp;,您的代码就可以了。使用X,您首先在调用c1(a); 时进行复制,然后在从operator() 调用Foo::bar 时再进行复制。使用X&amp;&amp;,您可以使用例如c1(std::move(a)) 调用operator(),但是您在operator() 中有一个左值,并且您无法将其传递给Foo::bar,因为您无法将右值绑定到左值。 @nitronoid 不,但由于它需要一个指向成员函数的指针,而不是重载集,因此您需要明确指定您想要的重载。有关示例,请参阅here。 Live demo.【参考方案2】:

我想要一个结合以上三个功能不同方面的功能[...]

如果我正确理解你想要什么...在我看来这是可能的,但我看到的只是一个复杂的解决方案。

希望其他人可以提出一个更简单的方法,我使用了几个帮助器:仅声明的模板函数gth1() 来检测方法指针中的Args...

template <typename ClassT, typename ... ArgsT>
constexpr auto gth1 (void(ClassT::*)(ArgsT...)) -> std::tuple<ArgsT...>;

以及模板 gth2 结构的特化,它具有构造和返回 lambda 的静态方法(Holt 的更正:谢谢!)

template <typename, typename, auto>
struct gth2;

template <typename ClassT, typename ... ArgsT, auto memFn>
struct gth2<ClassT, std::tuple<ArgsT...>, memFn>
  
   static auto getLambda (ClassT * obj)
     return [obj](ArgsT ... args)
        return (obj->*memFn)(std::forward<ArgsT>(args)...); ; 
 ;

现在你可以写一个getCallback()函数如下

template <auto memFn, typename ClassT>
auto getCallback (ClassT * obj)
  return gth2<ClassT, decltype(gth1(memFn)), memFn>::getLambda(obj);  

以下是一个完整的工作示例

#include <iostream>

template <typename, typename, auto>
struct gth2;

template <typename ClassT, typename ... ArgsT, auto memFn>
struct gth2<ClassT, std::tuple<ArgsT...>, memFn>
  
   static auto getLambda (ClassT * obj)
     return [obj](ArgsT ... args)
        return (obj->*memFn)(std::forward<ArgsT>(args)...); ; 
 ;

template <typename ClassT, typename ... ArgsT>
constexpr auto gth1 (void(ClassT::*)(ArgsT...)) -> std::tuple<ArgsT...>;

template <auto memFn, typename ClassT>
auto getCallback (ClassT * obj)
  return gth2<ClassT, decltype(gth1(memFn)), memFn>::getLambda(obj);  

// Example of use
struct Foo
  void bar(size_t& x, const std::string& s)  x=s.size();  ;

int main ()
 
   Foo f;

   auto l  getCallback<&Foo::bar>(&f) ;

   size_t x;

   l(x, "1234567");

   std::cout << x << "\n";
 

【讨论】:

你应该去掉operator()args...之前的rvalue-reference修饰符,否则pass-by-value参数会变成rvalue-reference参数,你会影响最终函数的行为。 @Holt - 抱歉...我承认我在左/右引用和转发方面有很多问题,所以你可能是对的...但我不明白问题。你能准确举一个错误用例的例子吗? 由于Args 是结构gth2 而不是operator() 的模板参数,所以args 不是转发引用。您基本上是将右值修饰符 &amp;&amp; 应用于现有类型(在本例中为多个)。如果类型是(const-)引用,那么按照标准规则,一切正常,因为X &amp; &amp;&amp;X &amp;。如果你有一个右值,那也没关系,因为X &amp;&amp; &amp;&amp;X &amp;&amp;。但是如果你有一个非引用类型X,那么X&amp;&amp; 是一个对X 的右值引用,所以你对operator() 和原始Foo::bar 有不同的签名。见godbolt.org/g/NnkMm9 @Holt - 今天完美的转发会让我发疯。是的:你是对的:将Foo::bar() 的第一个参数的类型从std::size_t &amp; 更改为std::size_t 我得到一个错误。更正(我希望)。谢谢。 你不需要右值修饰符,但你仍然需要std::forward(否则它是右值引用参数不起作用);)请参阅超级答案的 cmets 中的讨论。 【参考方案3】:

这是另一种可能性。它受到其他答案的启发,但是虽然它们都使用了一些部分模板专业化,但这个只使用函数模板参数推导。

内部函数采用第二个参数,其类型用于此推导,其运行时值等于非类型模板参数的编译时值。它在运行时被忽略,特别是它不会被 lambda 捕获。

template <auto memFn, class ClassT, class RetT, class... ArgsT>
inline auto getCallbackInner(ClassT* obj, RetT(ClassT::*)(ArgsT...))

    return [obj](ArgsT... args)->RetT 
        return (obj->*memFn)(std::forward<ArgsT>(args)...);
    ;

template <auto memFn, class ClassT>
auto getCallback(ClassT* obj)

    return getCallbackInner<memFn, ClassT>(obj, memFn);

与其他两个答案一样,这个答案仍然使用 C++17 标准中的自动模板参数,因此它在 Visual Studio 中不起作用。这很遗憾,但似乎仅使用 C++14 是不可能的。

脚注:

另一个更主观但可能最正确的答案是,我不应该首先尝试将成员函数作为模板参数传递。最初的getCallbackPtr(),它只是将一个成员函数指针绑定到 lambda 中,可能比其他任何可能性都被更多的人(以及更多的编译器)理解。通过成员函数指针间接的性能成本可能微不足道,而使用模板技巧的维护成本很高,所以我认为我会在实践中实际使用该版本,除非有可证明的性能成本。

【讨论】:

以上是关于将 lambda 参数完美转发给成员函数,其中成员函数是非类型模板形参的主要内容,如果未能解决你的问题,请参考以下文章

Python: 通过引用将类成员项传递给它的一个成员函数。

C ++ 11 lambda作为成员变量?

C ++ lambda函数作为类成员:奇怪的行为

从通用 lambda 调用 `this` 成员函数 - clang vs gcc

如何使用 C++ lambda 将成员函数指针转换为普通函数指针以用作回调

涉及派生类成员函数