Каков наиболее эффективный способ передачи не общей функции?

Я изучаю функциональное программирование в C++. Мое намерение состоит в том, чтобы передать не общую функцию в качестве аргумента. Я знаю о методе шаблона, однако я хотел бы ограничить подпись функции как часть дизайна API. Я разработал 4 различных примера методов на cpp.sh:

// Example program
#include <iostream>
#include <string>
#include <functional>

typedef int(functor_type)(int);


int by_forwarding(functor_type &&x)  {
    return x(1);
}

int functor_by_value(functor_type x) {
    return x(1);
}

int std_func_by_value(std::function<functor_type> x) {
    return x(1);
}

int std_func_by_forwarding(std::function<functor_type> &&x) {
    return x(1);
}

int main()
{
    std::cout << functor_by_value([](int a){return a;}); // works
    std::cout << std_func_by_value([](int a){return a;}); // works
    std::cout << std_func_by_forwarding(std::move([](int a){return a;})); // works

    //std::cout << by_forwarding([](int a){return a;}); // how to move lambda with forwarding ?
}

Выполнена ли какая-либо из вышеуказанных попыток? Если нет, как мне достичь своей цели?

Ответы

Ответ 1

(на основе пояснений по комментариям)

Подпись может быть ограничена использованием std::is_invocable:

template<typename x_Action> auto
functor_by_value(x_Action && action)
{
    static_assert(std::is_invocable_r_v<int, x_Action, int>);
    return action(1);
}

онлайн-компилятор

Ответ 2

Другая альтернатива:

template <typename Func>
auto functor_by_value(Func&& f)
-> decltype(std::forward<Func>(f)(1))
{
    return std::forward<Func>(f)(1);
}

Ответ 3

Как обычно, это зависит от того, насколько хорош ваш компилятор сегодня, и насколько он хорош в будущем.

В настоящее время компиляторы не очень хорошо оптимизируют std::function. Удивительно, что std::function - это сложный объект, который иногда должен выделять память для поддержания функций лямбда-состояния с сохранением состояния. Это также усложняет то, что std::function должна иметь возможность ссылаться на функцию-член, регулярные функции и лямбды и делать это прозрачным образом. Эта прозрачность имеет высокую стоимость исполнения.

Итак, если вам нужен самый быстрый код, вы должны быть осторожны с std::function. По этой причине первый вариант является самым быстрым (на сегодняшний день компилятором):

int functor_by_value(functor_type x) {
    return x(1);
}

Он просто передает указатель на функцию.

Когда речь идет о янтарных состояниях, у вас есть только два варианта. Либо передайте лямбду в качестве аргумента шаблона, либо конвертируйте в std::function. Следовательно, если вам нужен самый быстрый код с помощью lambdas (в современных компиляторах), вы передадите функцию в качестве шаблонного аргумента.

Поскольку функция лямбда может иметь большое состояние, ее передача может скопировать большое состояние (когда исключение копирования невозможно). GCC построит lambda непосредственно в списке параметров (без копирования), но вложенная функция вызовет конструктор копирования для лямбда. Чтобы избежать этого, либо передайте его по ссылке const (в этом случае она не может быть изменчивой), либо по ссылке rvalue:

template<class Func>
void run2(const Func & f)
{
    std::cout << "Running\n";
    f();
}
template<class Func>
void run(const Func & f)
{
    run2(f);
}
int main()
{
    run([s=BigState()]() { std::cout << "apply\n"; });
    return 0;
}

Или же:

template<class Func>
void run2(Func && f)
{
    f();
}
template<class Func>
void run(Func && f)
{
    run2(std::forward<Func>(f));
}
int main()
{
    run([s=BigState()]() { std::cout << "apply\n"; });
    return 0;
}

Без использования ссылок BigState() будет скопирован при копировании лямбда.

ОБНОВЛЕНИЕ: после прочтения вопроса снова я вижу, что он хочет ограничить подпись

template<typename Func, 
         typename = std::enable_if_t<
            std::is_convertible_v<decltype(Func(1)), int>>>
void run2(const Func & f)
{
    std::cout << "Running\n";
    f();
}

Это ограничит его любой функцией, которая может принять int (возможно, с неявным литьем) и возвращает int или любой тип, который неявно приводится к int. Тем не менее, если вы хотите принять только функциональноподобные объекты, которые принимают точно int и возвращают точно int, вы можете увидеть, является ли лямбда конвертируемой в std::function<int(int)>

Ответ 4

однако я хотел бы ограничить подпись функции как часть дизайна API.

Поэтому ограничьте это:

#include <functional>
#include <type_traits>
#include <iostream>

/// @tparam F is a type which is callable, accepting an int and returning an int
template
<
    class F, 
    std::enable_if_t
    <
        std::is_convertible_v<F, std::function<int(int)>>
    >* = nullptr
>
int myfunc(F &&x) {
    return x(1);
}

int main()
{
    auto a = myfunc([](int x) { std::cout << x << std::endl; return 1; });

    // does not compile
    // auto b = myfunc([]() { std::cout << "foo" << std::endl; return 1; });
}