c++ - É possível passar lambda genérico como argumento não-modelo




c++14 generic-lambda (3)

É possível passar lambda genérico como argumento não-modelo

Não é possível declarar uma função não modelo que aceita um lambda como um argumento. O tipo de um lambda é anônimo: não tem nome. Não é possível escrever uma declaração de função que aceita um argumento de um tipo anônimo.

O tipo do lambda pode ser deduzido , e é por isso que os lambdas podem ser passados ​​para modelos de função cujos tipos de argumentos são deduzidos.

Enquanto isso responde à pergunta, ela não oferece uma solução. Eu não acho que uma solução seja simples.

Eu tenho um exemplo de brinquedo que gostaria de modificar arquitetonicamente para remover a dependência de tipo do Processor no EmitterT :

#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;
}

Motivação

Eu gostaria de separar a parte responsável por utilizar os resultados do processamento do próprio processamento. A estrutura da classe Emitter é fornecida para mim, então eu tenho que suportar funções sobrecarregadas.

Eu gostaria de passar uma função lambda para um processador que irá usá-lo. Como um mecanismo de retorno de chamada, no entanto, ele deve ser um lambda genérico, para suportar sobrecargas.

O que eu tentei:

O exemplo que escrevi funciona, mas depende do tipo Emitter como um parâmetro de modelo. Eu não gosto do tipo de Processor para mudar com base no Emitter . Também é contagioso, eu tenho uma hierarquia de Processor real e propagação de Emitter como const ou pior.

Depois de ler https://stackoverflow.com/a/17233649/1133179 , tentei jogar com o struct abaixo como membro:

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

Mas eu não consigo descobrir uma maneira de adiar a implementação do Emitter após o Processor ao usá-lo como um parâmetro normal. Ele funcionou com uma declaração avançada e uma referência EmitterC& mas suporta apenas uma definição de Emissor. A única maneira que eu poderia fazer era largar o lambda, e fazer sobrecargas virtuais no EmitterC para cada tipo que eu esperava no Emitter e usá-lo como uma classe base.

Então, existe uma maneira de passar o lambda (genérico) como um parâmetro, de modo que o tipo de Processor não dependa do Emitter ?

Estou restrito ao c ++ 14, mas também estou interessado em padrões mais modernos se tiver um suporte melhor.


Esta solução mais simples é tornar o Emitter um parâmetro para process :

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

};

No entanto, se ele deve ser um membro do Processor e você pode enumerar as assinaturas de função possíveis, você pode usar algum tipo de exclusão de tipo. std::function ou o std::function_ref proposto não funciona porque eles permitem somente uma única assinatura de função, mas podemos escrever nosso próprio 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;
};

Exemplo ao vivo

Isso requer o C ++ 17 para o using /* base */::operator()... , mas que pode ser emulado em C ++ 14; veja o artigo que introduziu este recurso: [P0195] , ou talvez o match do Boost HOF possa ser massageado para fazer isso. Esta é também apenas uma referência de função e não uma função proprietária.

Então podemos escrever:

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


Se você está disposto a pagar um alto custo de execução em troca de restrições mínimas, você pode usar std::function com std::any (para C ++ 14, use 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::any e std::function são ambos proprietários do tipo wrappers apagados. Você pode ter alocações de heap para isso, ou pode caber dentro de sua otimização de objeto pequeno. Você terá chamadas de função virtual (ou equivalente).

Link do Compiler Explorer