代码之家  ›  专栏  ›  技术社区  ›  Arthur Tacca wim

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

  •  3
  • Arthur Tacca wim  · 技术社区  · 6 年前

    语境

    我想将一个成员函数和一个特定的对象包装成一个函数对象(稍后我将使用它作为回调)。我想为不同的成员函数和对象编写一次这个包装函数,特别是因为我的实际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() ,它有一个模板 运算符() :

    • 它不适用于visual studio。这对我来说是一个表演障碍。(错误是 error C3533: a parameter cannot have a type that contains 'auto' ,参考 template <auto memFn, class ClassT> )
    • 如果传入的参数类型错误,我怀疑它的编译器错误会比未模板化的更复杂、更混乱。 运算符() (无可否认,这只是一种预感)。
    • 模板 小精灵 无法接受参数的初始值设定项列表。这对我来说根本不是问题,但我要把它记录在案。

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

    能做到吗?怎么用?

    3 回复  |  直到 6 年前
        1
  •  2
  •   super    6 年前

    推导参数的一个简单方法是使用部分模板专门化。

    在这个例子中,我通过转发非类型成员函数指针来解决这个问题 它是一个自定义函子的类型,然后返回。

    一部分专攻这个类型,其余的都是直接的。

    #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";
    }
    
        2
  •  1
  •   max66    6 年前

    我想要一个函数,它结合了上述三个函数的不同方面[…]

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

    希望其他人能提出一个更简单的方法,我使用了两个助手:一个声明的仅模板函数 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";
     }
    
        3
  •  0
  •   Arthur Tacca wim    6 年前

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

    内部函数接受第二个参数,其类型用于此推断,其运行时值等于非类型模板参数的编译时值。它在运行时被忽略,特别是它不会被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标准的自动模板参数,所以它在VisualStudio中不起作用。这是一个遗憾,但它似乎是不可能的只是C++ 14。

    脚注:

    另一个更主观但可能最正确的答案是,我不应该首先尝试将成员函数作为模板参数传递。原文 getCallbackPtr() ,它只需将成员函数指针绑定到lambda中,就比任何其他可能性都更容易被更多的人(以及更多的编译器)理解。虽然使用模板技巧的维护成本很高,但通过成员函数指针进行间接寻址的性能成本可能微不足道,因此我认为我将实际使用该版本,除非存在明显的性能成本。

    推荐文章