【问题标题】:Is it possible to pass generic lambda as non-template argument是否可以将通用 lambda 作为非模板参数传递
【发布时间】:2019-11-30 15:28:14
【问题描述】:

我有一个玩具示例,我想在架构上进行修改以删除 ProcessorEmitterT 的类型依赖性:

#include <iostream>
#include <utility>

using namespace std;

struct Emitter {
    void e(int) { cout << "emitting int\n";}
    void e(double) { cout << "emitting double\n";}
    void e(char*) { cout << "emitting char*\n";}
    void e(const char*) { cout << "emitting const char*\n";}
};

template <typename EmitterT>
struct Processor {

    Processor(EmitterT e) : e_{e} {}

    template <typename T>
    void process(T&& value) {
        cout << "some processing... ";
        e_(std::forward<T>(value));
    }

    EmitterT e_;

};

template<typename Emitter_>
Processor<Emitter_> makeProcessor(Emitter_ e) { return Processor<Emitter_>(e);}

int main() {
    Emitter em;
    auto p = makeProcessor([&em](auto v){em.e(v);});


    p.process(1);
    p.process("lol");
    return 0;
}

动机

我想将负责利用处理结果的部分与处理本身分离。 Emitter类结构给了我,所以我必须支持重载函数。

我想将 lambda 函数传递给将要使用它的处理器。有点像回调机制,但它必须是通用 lambda,才能支持重载。

我尝试过的:

我写的例子有效,但它依赖于Emitter 类型作为模板参数。我不喜欢根据Emitter 更改Processor 类型。它也具有传染性,我有一个真正的Processor 层次结构和Emitter 传播就像const 或更糟。

在阅读https://stackoverflow.com/a/17233649/1133179 之后,我尝试以成员身份使用以下结构:

struct EmitterC {
    template<typename T>
    void operator()(T value) { }
};

但是当将 Emitter 用作普通参数时,我无法想出一种方法来在 Processor 之后推迟实现 Emitter。它通过前向声明和引用EmitterC&amp; 解决,但它只支持一个发射器定义。我能想到的唯一方法是删除 lambda,并在 EmitterC 中为我期望在 Emitter 中的每种类型进行虚拟重载,并将其用作基类。

那么,有没有办法将(通用)lambda 作为参数传递,使Processor 类型不依赖于Emitter

我仅限于 c++14,但如果有更好的支持,我也对更现代的标准感兴趣。

【问题讨论】:

  • 在您的代码中,无需将发射器存储为成员,因此您也可以简单地接受它是一个参数。
  • 您可以将构造函数改为模板函数,并将其类型擦除为std::function 类成员。
  • @SamVarshavchik:std::function 的签名是什么?
  • lambda 采用的参数类型及其返回值。
  • 类型擦除参数到 lambda 本身。在某个时刻,某处,类型必须是已知的,或者以某种方式确定。在这一点上,真的没有什么可以说的了,太模糊了。

标签: c++ lambda c++14 generic-lambda


【解决方案1】:

这个最简单的解决方案是让Emitter成为process的参数:

struct Processor {
    template <typename T, typename EmitterFn>
    void process(T&& value, EmitterFn emit) {
        cout << "some processing... ";
        emit(std::forward<T>(value));
    }

};

但是,如果它必须是 Processor 的成员并且您可以枚举可能的函数签名,则可以使用某种类型擦除。 std::function 或建议的 std::function_ref 将不起作用,因为它们只允许单个函数签名,但我们可以编写自己的 overloaded_function_ref

template <typename Derived, typename Sig>
class function_ref_impl;

template <typename Derived, typename R, typename... Args>
class function_ref_impl<Derived, R(Args...)> {
    using fn_t = R(*)(void const*, Args...);

public:
    auto operator()(Args... args) const -> R {
        return fn(static_cast<Derived const&>(*this).object, std::forward<Args>(args)...);
    }

protected:
    template <typename F,
        std::enable_if_t<!std::is_base_of<function_ref_impl, F>::value, int> = 0>
    explicit function_ref_impl(F const& f)
        : fn{[](void const* self, Args... args) -> R {
            return (*static_cast<F const*>(self))(std::forward<Args>(args)...);
        }}
    {}

private:
    fn_t fn;
};

template <typename... Sig>
class overloaded_function_ref
    : public function_ref_impl<overloaded_function_ref<Sig...>, Sig>...
{
public:
    template <typename F,
        std::enable_if_t<!std::is_base_of<overloaded_function_ref, F>::value, int> = 0>
    overloaded_function_ref(F const& f)
        : function_ref_impl<overloaded_function_ref, Sig>(f)...
        , object{std::addressof(f)}
    {}

    // Can be done pre-C++17, but it's not easy:
    using function_ref_impl<overloaded_function_ref, Sig>::operator()...;

    // This can be encapsulated with techniques such as the "passkey" idiom.
    // Variadic friend expansion isn't a thing (`friend bases...`).
    void const* object;
};

Live example

对于using /* base */::operator()...确实需要 C++17,但可以在 C++14 中模拟;请参阅介绍此功能的论文:[P0195],或者也许可以通过按摩 Boost HOF's match 来做到这一点。这也只是一个函数引用,而不是一个拥有函数。

那么我们可以这样写:

struct Processor {
    template <typename T>
    void process(T&& value) {
        cout << "some processing... ";
        emit(std::forward<T>(value));
    }

    using emitter_t = overloaded_function_ref<
        void(int),
        void(double),
        void(char*),
        void(char const*)
    >;

    emitter_t emit;
};

Demo

【讨论】:

    【解决方案2】:

    恕我直言:继承就是为了这个。

    #include <iostream>
    #include <utility>
    
    using namespace std;
    
    struct BaseEmitter {
        virtual void e(int) =0;
        virtual void e(double)=0;
        virtual void e(char*)=0;
        virtual void e(const char*)=0;
    };
    
    struct Emitter :public BaseEmitter {
        virtual void e(int) { cout << "emitting int\n";}
        virtual void e(double) { cout << "emitting double\n";}
        virtual void e(char*) { cout << "emitting char*\n";}
        virtual void e(const char*) { cout << "emitting const char*\n";}
    };
    
    struct Processor {
        BaseEmitter& e_;
        Processor(BaseEmitter& e) : e_(e) {}
    
        template <typename T>
        void process(T&& value) {
            cout << "some processing... ";
            e_(std::forward<T>(value));
        }
    };
    
    
    int main() {
        Emitter em;
        auto p = Processor(em);
        p.process(1);
        p.process("lol");
        return 0;
    }
    

    您可以通过接口中的继承来进行混合以捕获 lambda:

    struct bypass
    {
            virtual void operator()() = 0;
    };
    
    template<typename callable> struct capture: public bypass
    {
            callable& _ref;
            capture(callable &ref)
            : _ref(ref)
            {;};
    
            virtual void operator()()
            {
                    _ref();
            }
    };
    
    struct test
    {
            bypass *_c;
            template<class T> test(T& callback)
            : _c(nullptr)
            {
              _c = new capture<decltype(callback)>(callback);
            };
            virtual ~test()
            {
                delete _c;
            };
            void doit()
            {
                (*_c)();
            }
    
    };
    
    
    
    int main(int argc, char* argv[])
    {
            auto lambda = [](){std::cout << "hello\n";};
            test z=test(lambda);
            z.doit();
            return 0;
    }
    

    【讨论】:

    • 这是对动机的建议,而不是对所问问题的建议。
    • 这正是我在这里所描述的:“我能想出的唯一方法是删除 lambda,并在 EmitterC 中为我期望在 Emitter 中的每种类型进行虚拟重载并将其用作一个基类。”所以我明确表示我对这个解决方案不感兴趣。我想为了完整性而写它是件好事。
    • 它得到了一些支持,所以一定对某些人有用!
    • 我添加了另一个想法,也许这可能接近你喜欢的? ;)
    • e_(std::forward&lt;T&gt;(value)); 你的意思是e_.e(std::forward&lt;T&gt;(value));
    【解决方案3】:

    如果你愿意付出高昂的运行时成本来换取最小的限制,你可以使用std::functionstd::any(对于C++14,使用boost::any):

    #include <iostream>
    #include <utility>
    #include <any>
    #include <functional>
    
    struct Processor {
        Processor(std::function<void(std::any)> e) : e_{e} {}
    
        template <typename T>
        void process(T&& value) {
            std::cout << "some processing... ";
            e_(std::forward<T>(value));
        }
    
        std::function<void(std::any)> e_;
    };
    
    struct Emitter {
        void e(int) { std::cout << "emitting int\n";}
        void e(double) { std::cout << "emitting double\n";}
        void e(char*) { std::cout << "emitting char*\n";}
        void e(const char*) { std::cout << "emitting const char*\n";}
    };
    
    int main() {
        Emitter em;
        auto p = Processor(
            [&em](std::any any){
                // This if-else chain isn't that cheap, but it's about the best
                // we can do. Alternatives include:
                // - Hashmap from `std::type_index` (possibly using a perfect hash)
                //   to a function pointer that implements this.
                // - Custom `any` implementation which allows "visitation":
                //
                //   any.visit<int, double, char*, char const*>([&em] (auto it) {
                //        em.e(it);
                //   });
                if (auto* i = std::any_cast<int>(&any)) {
                    em.e(*i);
                } else if (auto* d = std::any_cast<double>(&any)) {
                    em.e(*d);
                } else if (auto* cstr = std::any_cast<char*>(&any)) {
                    em.e(*cstr);
                } else {
                    em.e(std::any_cast<char const*>(any));
                }
            }
        );
    
    
        p.process(1);
        p.process("lol");
        return 0;
    }
    

    std::anystd::function 都拥有类型擦除包装器。您可能为此进行了堆分配,或者您可能适合他们的小对象优化。您将有虚函数调用(或等效函数)。

    Compiler Explorer link

    【讨论】:

      【解决方案4】:

      是否可以将通用 lambda 作为非模板参数传递

      不能声明接受 lambda 作为参数的非模板函数。 lambda 的类型是匿名的:它没有名字。无法编写接受匿名类型参数的函数声明。

      lambda 的类型可以推导,这就是为什么可以将 lambdas 传递到参数类型被推导的函数模板中。

      虽然这回答了问题,但它并没有提供解决方案。我认为解决方案不会很简单。

      【讨论】:

      • “不能声明接受 lambda 作为参数的非模板函数。”除非您可以隐式转换为非模板类​​型,例如std::function 或即将推出的std::function_ref
      • 但是 std::function 包装器只能用于调用 lambda 的一个特定签名,这在此处是不够的......
      • @MaxLanghof 嗯,是的。函数包装器在这里可能没有用。
      • @MaxLanghof 您可以编写自己的接受多个签名,只要您可以枚举可能的签名。例如。 overloaded_function_ref&lt;void(int), void(double), void(char*), void(char const*)&gt;
      • @Justin: 遗憾的是 std::function_ref 被延迟到 C++23。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-06-14
      • 1970-01-01
      • 1970-01-01
      • 2017-07-08
      • 1970-01-01
      • 2021-11-24
      相关资源
      最近更新 更多