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

Первое решение основано на том, что С++ уже предоставляет нам механизм захвата переменных. Речь идет о лямбдах. Естественно, что самым очевидным и простым было бы использовать такой чудесный механизм. Для тех, кто не знаком с С++14 и выше, я приведу соответствующий код:

auto Variable = 1;

auto Lambda = [Variable]() {
    someFunction(Variable);
};

В этом коде создается лямбда функция, которая захватывает переменную с именем Variable. Сам объект лямбда функции копируется в переменную с именем Lambda. Именно через эту переменную в дальнейшем можно будет вызывать саму лямбда функцию. И такой вызов будет выглядеть совсем как вызов обычной функции:

Lambda();

Казалось бы, что поставленная задача уже решена, но в реальности это не так. Лямбда функцию можно вернуть из функции, метода или другой лямбды, но передать ее потом куда-то не используя шаблонов затруднительно.

auto makeLambda(int Variable) {
    return [Variable]() {
        someFunction(Variable);
    };
}

auto Lambda = makeLambda(3);

// Какой должна быть сигнатура функции, принимающей такой аргумент?
someOtherFunction(Lambda);

Лямда функции являются объектами какого-то анонимного типа, у них есть известная лишь только компилятору внутренняя структура. И чистый С++ (я имею ввиду язык без библиотек) предоставляет программисту не так уж и много операций над лямбдами:

  • лямбду можно вызвать;
  • лямбду можно привести к указателю на функцию, если эта лямбда не захватыает переменные;
  • лямбду можно скопировать.


В принципе, этих базовых операций вполне достаточно, ведь используя их и другие механизмы языка можно сделать очень и очень многое. Вот что у меня получилось в итоге.

#include <utility>
#include <cstdint>
#include <vector>

template <typename Function> class SignalTraits;

template <typename R, typename... A> class SignalTraits<R(A...)> {
public:
  using Result = R;
};

template <typename Function> class Signal {
public:
  using Result = typename SignalTraits<Function>::Result;

  template <typename Callable> Signal(Callable Fn) : Storage(sizeof(Fn)) {
    new (Storage.data()) Callable(std::move(Fn));

    Trampoline = [](Signal *S) -> Result {
      auto CB = static_cast<Callable *>(static_cast<void *>(S->Storage.data()));
      return (*CB)();
    };
  }

  Result invoke() { return Trampoline(this); }

private:
  Result (*Trampoline)(Signal *Self);

  std::vector<std::uint8_t> Storage;
};

В этом примере: благодаря шаблонному конструктору, лямбда создаваемая внутри этого конструктора будет иметь информацию о типе Сallable, а значит, сможет привести данные в Storage к нужному типу. Фактически, в этом и заключается весь фокус. Вся сложная работа по захвату переменных и вызову функций и лямбд возложена на плечи компилятора. На мой взгляд, такое решение предельно простое и элегантное.

Что же касается второго решения, то оно мне нравится меньше, т.к. в нем очень много самописного кода, который решает по сути то, что уже решено для нас компилятором. А именно: захват переменных. Не буду вдаваться в долгие рассуждения и обсуждения, а приведу сразу код всего решения. Т.к. он очень большой и мне не импанирует, то я его спрячу под кат:

не красивый код.
#include <cstdarg>
#include <cstdint>
#include <vector>

template <typename T> struct PromotedTraits { using Type = T; };
template <> struct PromotedTraits<char> { using Type = int; };
template <> struct PromotedTraits<unsigned char> { using Type = unsigned; };
template <> struct PromotedTraits<short> { using Type = int; };
template <> struct PromotedTraits<unsigned short> { using Type = unsigned; };
template <> struct PromotedTraits<float> { using Type = double; };

template <typename... Arguments> class StorageHelper;

template <typename T, typename... Arguments>
class StorageHelper<T, Arguments...> {
public:
  static void store(va_list &List, std::vector<std::uint8_t> &Storage) {
    using Type = typename PromotedTraits<T>::Type;
    union {                                       
      T Value;                                    
      std::uint8_t Bytes[sizeof(void *)];         
    };                                            
    Value = va_arg(List, Type);
    for (auto B : Bytes) {
      Storage.push_back(B);
    }
    StorageHelper<Arguments...>::store(List, Storage);
  }
};

template <> class StorageHelper<> {
public:
  static void store(...) {}
};

template <bool, typename...> class InvokeHelper;

template <typename... Arguments> class InvokeHelper<true, Arguments...> {
public:
  template <typename Result>
  static Result invoke(Result (*Fn)(Arguments...), Arguments... Args) {
    return Fn(Args...);
  }
};

template <typename... Arguments> class InvokeHelper<false, Arguments...> {
public:
  template <typename Result> static Result invoke(...) { return {}; }
};

struct Dummy;

template <std::size_t Index, typename... Types> class TypeAt {
public:
  using Type = Dummy *;
};

template <std::size_t Index, typename T, typename... Types>
class TypeAt<Index, T, Types...> {
public:
  using Type = typename TypeAt<(Index - 1u), Types...>::Type;
};

template <typename T, typename... Types> class TypeAt<0u, T, Types...> {
public:
  using Type = T;
};

template <typename Function> class Signal;

template <typename Result, typename... Arguments>
class Signal<Result(Arguments...)> {
public:
  using CFunction = Result(Arguments...);

  Signal(CFunction *Delegate, Arguments... Values) : Delegate(Delegate) {
    initialize(Delegate, Values...);
  }

  Result invoke() {
    std::uintptr_t *Args = reinterpret_cast<std::uintptr_t *>(Storage.data());
    Result R = {};
    using T0 = typename TypeAt<0u, Arguments...>::Type;
    using T1 = typename TypeAt<0u, Arguments...>::Type;
    // ... and so on.
    switch (sizeof...(Arguments)) {
    case 0u:
      return InvokeHelper<(0u == sizeof...(Arguments)),
                          Arguments...>::template invoke<Result>(Delegate);
    case 1u:
      return InvokeHelper<(1u == sizeof...(Arguments)),
                          Arguments...>::template invoke<Result>(Delegate,
                                                                 (T0 &)Args[0]);
    case 2u:
      return InvokeHelper<(2u == sizeof...(Arguments)),
                          Arguments...>::template invoke<Result>(Delegate,
                                                                 (T0 &)Args[0],
                                                                 (T1 &)Args[1]);
      // ... and so on.
    }
    return R;
  }

private:
  void initialize(CFunction *Delegate, ...) {          
    va_list List;                                      
    va_start(List, Delegate);                          
    StorageHelper<Arguments...>::store(List, Storage); 
    va_end(List);                                      
  }                                                    

  CFunction *Delegate;

  std::vector<std::uint8_t> Storage; 
};

Тут вся интересность, на мой взгляд, заключается в двух вспомогательных классах: StorageHelper и InvokeHelper. Первый комбинирует эллипсис и рекурсивный проход по списку типов для того, чтобы заполнить хранилище аргументов. Второй предоставляет безопасный в плане типов способ извлечения аргументов из этого хранилища. Кроме того, есть еще одна небольшая хитрость: эллипсис промоутит одни типы к другим. Т.е. float переданный через… будет приведен к double, char к int, short к int и т.д.

Хочу подвести этакий итог всему выше сказанному. По моему мнению, оба решения не идеальны: они много чего не умеют и пытаются изобрести колесо. Если бы меня спросили как правильно захватить аргументы и передать их в некую функцию, я бы не раздумывая сказал, что нужно использовать std::function + лямбду. Хотя в качестве упражнения для ума поставленная задачка очень даже неплоха.

Надеюсь, что все прочитанное вами окажется полезным. Спасибо, что так далеко дочитали!

Комментарии (20)


  1. CodingMayhem Автор
    10.06.2015 19:24

    В коде второго примера заметил опечатку:
    using T1 = typename TypeAt<0u, Arguments...>::Type;

    Вместо той сторки должна была быть эта:
    using T1 = typename TypeAt<1u, Arguments...>::Type;


  1. kmu1990
    10.06.2015 20:05

    new (Storage.data()) Callable(std::move(Fn));
    


    а деструктор у Callable вызывать не нужно? Кроме того, чем вам type erasure не угодил, зачем в vector-е конструировать Callable (может у меня вкус плохой, но мне конкретно этот момент не кажется элегантным решением)?


  1. Eivind
    10.06.2015 22:32
    +2

    Я это вижу как-то так:

    Код
    #include <tuple>
    #include <memory>
    #include <cassert>
    
    namespace detail {
    
    template <typename T>
    struct call_type : call_type<decltype(&T::operator())>
    {};
    
    template <typename ClassType, typename ResultType, typename ... Args>
    struct call_type<ResultType(ClassType::*)(Args...) > : call_type<ResultType(Args...)> {};
    
    template <typename ClassType, typename ResultType, typename ... Args>
    struct call_type<ResultType(ClassType::*)(Args...) const > : call_type<ResultType(Args...)> {};
    
    template <typename ClassType, typename ResultType, typename ... Args>
    struct call_type<ResultType(ClassType::*)(Args...) volatile > : call_type<ResultType(Args...)> {};
    
    template <typename ClassType, typename ResultType, typename ... Args>
    struct call_type<ResultType(ClassType::*)(Args...) const volatile > : call_type<ResultType(Args...)> {};
    
    template <typename ClassType, typename ResultType, typename ... Args>
    struct call_type<ResultType(ClassType::*)(Args...) &> : call_type<ResultType(Args...)> {};
    
    template <typename ClassType, typename ResultType, typename ... Args>
    struct call_type<ResultType(ClassType::*)(Args...) const & > : call_type<ResultType(Args...)> {};
    
    template <typename ClassType, typename ResultType, typename ... Args>
    struct call_type<ResultType(ClassType::*)(Args...) volatile & > : call_type<ResultType(Args...)> {};
    
    template <typename ClassType, typename ResultType, typename ... Args>
    struct call_type<ResultType(ClassType::*)(Args...) const volatile & > : call_type<ResultType(Args...)> {};
    
    template <typename ClassType, typename ResultType, typename ... Args>
    struct call_type<ResultType(ClassType::*)(Args...) && > : call_type<ResultType(Args...)> {};
    
    template <typename ClassType, typename ResultType, typename ... Args>
    struct call_type<ResultType(ClassType::*)(Args...) const && > : call_type<ResultType(Args...)> {};
    
    template <typename ClassType, typename ResultType, typename ... Args>
    struct call_type<ResultType(ClassType::*)(Args...) volatile && > : call_type<ResultType(Args...)> {};
    
    template <typename ClassType, typename ResultType, typename ... Args>
    struct call_type<ResultType(ClassType::*)(Args...) const volatile && > : call_type<ResultType(Args...)> {};
    
    template <typename ResultType, typename ... Args>
    struct call_type<ResultType(*)(Args...)> : call_type<ResultType(Args...)> {};
    
    template <typename ResultType, typename ... Args>
    struct call_type<ResultType(&)(Args...)> : call_type<ResultType(Args...)> {};
    
    template <typename ResultType, typename ... Args>
    struct call_type<ResultType(Args...)>
    {
        using type = ResultType(Args...);
    };
    
    template <class R, class ... Args>
    struct any_function {
        virtual ~any_function() {};
        virtual R invoke(Args ... args)=0;
        virtual std::unique_ptr<any_function<R,Args...>> clone() const = 0;
    };
    
    template <class F, class R, class ... Args>
    struct concrete_function : any_function<R, Args...> {
        template <class Q>
        concrete_function(Q&& f)
          : f_(std::forward<Q>(f))
        {};
    
        R invoke(Args ... args) override {
            return f_(std::forward<Args>(args)...);
        }
    
        std::unique_ptr<any_function<R,Args...>> clone() const override {
            return std::make_unique<concrete_function<F,R,Args...>>(f_);
        }
    private:
        F f_;
    };
    
    }
    
    template <class Function>
    struct function;
    
    template <class R, class ... Args>
    struct function<R(Args...)> {
        using self_type = function<R(Args...)>;
        using result_type = R;
        using argument_types = std::tuple<Args...>;
    
        function() {};
    
        function(self_type&& other)
          : ptr(std::move(other.ptr))
        {}
    
        function(const self_type& other)
          : ptr(other.clone())
        {}
    
        self_type& operator=(const self_type& other) {
            ptr = other.clone();
            return *this;
        }
    
        self_type& operator=(self_type&& other) {
            ptr = std::move(other.ptr);
            return *this;
        }
    
        template <class F>
        function(F&& f)
          : ptr(std::make_unique<detail::concrete_function<F,R,Args...>>(std::forward<F>(f)))
        {}
    
        R operator()(Args ... args) {
            assert(ptr);
            return ptr->invoke(std::forward<Args>(args)...);
        }
    private:
        std::unique_ptr<detail::any_function<R,Args...>> clone() const {
            if(ptr) {
                return ptr->clone();
            } else {
                return nullptr;
            }
        }
    
        std::unique_ptr<detail::any_function<R,Args...>> ptr;
    };
    
    template<class F>
    decltype(auto) make_function(F&& f) {
        return function<typename detail::call_type<F>::type>(std::forward<F>(f));
    }
    


    1. Orient
      11.06.2015 07:48

      Вы используете forwarding-references, но для случая `const T &` код не сработает:

      template <typename T>
      struct call_type : call_type<decltype(&T::operator())>
      {};
      


      Такой user-код даёт hard-error:
      struct A { void operator () () const { ; } };
      A const a{};
      auto f = make_function(a);
      


      Необходимо использовать `std::remove_reference_t`.


      1. Eivind
        11.06.2015 11:53

        Как раз об этом подумал, но вы меня обогнали. Еще нужно поправить и в конструкторе. Но обычный std::remove_reference_t не подойдет, тогда не получится использовать простые функции. Напишем свой:

        Код
        #include <tuple>
        #include <memory>
        #include <cassert>
        
        namespace detail {
        
        template <typename T>
        struct call_type : call_type<decltype(&T::operator())>
        {};
        
        template <typename ClassType, typename ResultType, typename ... Args>
        struct call_type<ResultType(ClassType::*)(Args...) > : call_type<ResultType(Args...)> {};
        
        template <typename ClassType, typename ResultType, typename ... Args>
        struct call_type<ResultType(ClassType::*)(Args...) const > : call_type<ResultType(Args...)> {};
        
        template <typename ClassType, typename ResultType, typename ... Args>
        struct call_type<ResultType(ClassType::*)(Args...) volatile > : call_type<ResultType(Args...)> {};
        
        template <typename ClassType, typename ResultType, typename ... Args>
        struct call_type<ResultType(ClassType::*)(Args...) const volatile > : call_type<ResultType(Args...)> {};
        
        template <typename ClassType, typename ResultType, typename ... Args>
        struct call_type<ResultType(ClassType::*)(Args...) &> : call_type<ResultType(Args...)> {};
        
        template <typename ClassType, typename ResultType, typename ... Args>
        struct call_type<ResultType(ClassType::*)(Args...) const & > : call_type<ResultType(Args...)> {};
        
        template <typename ClassType, typename ResultType, typename ... Args>
        struct call_type<ResultType(ClassType::*)(Args...) volatile & > : call_type<ResultType(Args...)> {};
        
        template <typename ClassType, typename ResultType, typename ... Args>
        struct call_type<ResultType(ClassType::*)(Args...) const volatile & > : call_type<ResultType(Args...)> {};
        
        template <typename ClassType, typename ResultType, typename ... Args>
        struct call_type<ResultType(ClassType::*)(Args...) && > : call_type<ResultType(Args...)> {};
        
        template <typename ClassType, typename ResultType, typename ... Args>
        struct call_type<ResultType(ClassType::*)(Args...) const && > : call_type<ResultType(Args...)> {};
        
        template <typename ClassType, typename ResultType, typename ... Args>
        struct call_type<ResultType(ClassType::*)(Args...) volatile && > : call_type<ResultType(Args...)> {};
        
        template <typename ClassType, typename ResultType, typename ... Args>
        struct call_type<ResultType(ClassType::*)(Args...) const volatile && > : call_type<ResultType(Args...)> {};
        
        template <typename ResultType, typename ... Args>
        struct call_type<ResultType(*)(Args...)> : call_type<ResultType(Args...)> {};
        
        template <typename ResultType, typename ... Args>
        struct call_type<ResultType(&)(Args...)> : call_type<ResultType(Args...)> {};
        
        template <typename ResultType, typename ... Args>
        struct call_type<ResultType(Args...)>
        {
            using type = ResultType(Args...);
        };
        
        template <typename T>
        struct remove_reference {
            using type = T;
        };
        
        template <typename ResultType, typename ... Args>
        struct remove_reference<ResultType(&)(Args...)> {
            using type = ResultType(&)(Args...);
        };
        
        template <typename T>
        struct remove_reference <T&> {
            using type = T;
        };
        
        template <class R, class ... Args>
        struct any_function {
            virtual ~any_function() {};
            virtual R invoke(Args ... args)=0;
            virtual std::unique_ptr<any_function<R,Args...>> clone() const = 0;
        };
        
        template <class F, class R, class ... Args>
        struct concrete_function : any_function<R, Args...> {
            template <class Q>
            concrete_function(Q&& f)
              : f_(std::forward<Q>(f))
            {};
        
            R invoke(Args ... args) override {
                return f_(std::forward<Args>(args)...);
            }
        
            std::unique_ptr<any_function<R,Args...>> clone() const override {
                return std::make_unique<concrete_function<F,R,Args...>>(f_);
            }
        private:
            F f_;
        };
        
        }
        
        template <class Function>
        struct function;
        
        template <class R, class ... Args>
        struct function<R(Args...)> {
            using self_type = function<R(Args...)>;
            using result_type = R;
            using argument_types = std::tuple<Args...>;
        
            function() {};
        
            function(self_type&& other)
              : ptr(std::move(other.ptr))
            {}
        
            function(const self_type& other)
              : ptr(other.clone())
            {}
        
            self_type& operator=(const self_type& other) {
                ptr = other.clone();
                return *this;
            }
        
            self_type& operator=(self_type&& other) {
                ptr = std::move(other.ptr);
                return *this;
            }
        
            template <class F>
            function(F&& f)
              : ptr(std::make_unique<detail::concrete_function<typename detail::remove_reference<F>::type,R,Args...>>(std::forward<F>(f)))
            {}
        
            R operator()(Args ... args) {
                assert(ptr);
                return ptr->invoke(std::forward<Args>(args)...);
            }
        private:
            std::unique_ptr<detail::any_function<R,Args...>> clone() const {
                if(ptr) {
                    return ptr->clone();
                } else {
                    return nullptr;
                }
            }
        
            std::unique_ptr<detail::any_function<R,Args...>> ptr;
        };
        
        template<class F>
        decltype(auto) make_function(F&& f) {
            return function<typename detail::call_type<typename detail::remove_reference<F>::type>::type>(std::forward<F>(f));
        }
        


        1. Orient
          11.06.2015 12:25

          template <typename T>
          struct call_type : call_type<decltype(&std::remove_reference_t< T >::operator())>
          {};
          

          Я бы сделал так.


          1. Eivind
            11.06.2015 12:40

            Да, но нам еще нужно вывести тип функтора в конструкторе, и при этом сохранить ссылки на обычные функции.

            template <class F>
            function(F&& f)
              : ptr(std::make_unique<detail::concrete_function<typename detail::remove_reference<F>::type,R,Args...>>(std::forward<F>(f)))
            {}
            


  1. vScherba
    11.06.2015 00:37

    // Какой должна быть сигнатура функции, принимающей такой аргумент?
    someOtherFunction(Lambda);
    

    void someOtherFunction(decltype(Lambda) lam);
    


    1. Hertz
      11.06.2015 00:46

      template<typename F> return_type someOtherFunction(F&& f)
      


      1. vScherba
        11.06.2015 00:48

        но передать ее потом куда-то не используя шаблонов затруднительно.


  1. Amomum
    11.06.2015 01:29
    +4

    Один мой знакомый подкинул мне интересную задачку: нужно вызвать функцию через указатель и передать в нее предварительно сохраненные аргументы. Обязательным условием было не использовать std::function.

    Вероятно, я чего-то не понял, но почему нельзя было именно это и сделать? То есть, создать просто указатель на функцию и вызвать функцию через него?


    1. buratino
      11.06.2015 02:45
      +3

      я так понимаю, что стояла задача потрахаться и под страхом смерти не использовать С


      1. Amomum
        11.06.2015 09:55

        Просто в условиях я этого не увидел, вот и спросил.


  1. gshep
    11.06.2015 09:07
    -1

    ещё один вариант — Parameter Object design pattern.

    У данного подхода большой плюс — не зависит от языка программирования.


  1. Orient
    11.06.2015 11:34

    вызвать функцию через указатель и передать в нее предварительно сохраненные аргументы

    Я себе это вижу так:
    #include <utility>
    
    template< typename ...types >
    auto
    make_caller(types &&... _values)
    {
        return [_values...] (auto && callee) mutable -> decltype(auto) { return std::forward< decltype(callee) >(callee)(std::forward< types >(_values)...); };
    }
    
    // main.cpp
    #include <iostream>
    
    #include <cstdlib>
    
    struct A
    {
      
        template< typename ...types >
        void
        operator () (types...) const &
        {
            std::cout << __PRETTY_FUNCTION__ << std::endl;
        }
      
        template< typename ...types >
        void
        operator () (types...) &&
        {
            std::cout << __PRETTY_FUNCTION__ << std::endl;
        }
      
    };
    
    void
    f(int, float, double, long double)
    {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }
    
    int
    main()
    {
        auto caller = make_caller(1, 1.0f, 1.0, 1.0L);
        caller(A{});
        A const a{};
        caller(a);
        caller(f);
        caller(&f); // в точности то, что нужно
        return EXIT_SUCCESS;
    }
    


  1. topa
    11.06.2015 12:07
    -4

    Это уже не тот C++, который я знал и любил :(


    1. dyadyaSerezha
      11.06.2015 12:47
      +2

      Просто чел обожает лечить зубы перректально. Новый стандарт призван сделать код более чистым, кратким и понятным, а не для того, чтобы воротить горы трубочек, колесиков и рычажков для тривиальной вещи, прямо поддерживаемой языком.


      1. dyadyaSerezha
        11.06.2015 13:17
        +1

        Чтобы не быть голословным:

        int p1 = 1, p2 = 2;
        typedef double (*FuncT)(int i1, int i2);

        FuncT f1 = xyz; // тут имя любой фунции с той же сигнатурой.
        double d1 = f1(p1, p2);

        или, если сам вызоыв надо передать как параметр:

        double myFunc() { return f1(p1, p2); }

        или как-угодно еще, в зависимости от требований, но в 2-3 строчки!


  1. kloppspb
    11.06.2015 13:41

    >Обязательным условием было не использовать std::function

    Этот ёжик — что, вообще про C и стек ничего не знает?


  1. nickolaym
    11.06.2015 14:52
    +3

    Лямбду можно вызвать, скопировать, привести к функции (если она без связанных переменных)… и, внезапно, привести к std::function.
    Если std::function не проходит через слишком узкие ограничения «мой приятель мне запретил», — ну какие проблемы, копируем минимум-миниморум из стандартной библиотеки, переименовываем и получаем то же самое, только без костылей и велосипедов.

    Но, видимо, каждый программист должен написать велосипедную версию std::function, а в перспективе — велосипедные форт и лисп. (Точнее, написать велосипедный лисп — это не долженствование, а неизбежность).