How to guarantee order of argument evaluation when calling a function object?

前端 未结 4 1726
深忆病人
深忆病人 2020-11-27 16:41

The answers to the question on how to avoid undefined execution order for the constructors when using std::make_tuple led to a discussion during which I learned that the ord

相关标签:
4条回答
  • 2020-11-27 17:14

    The solution I had come up uses std::tuple<...> to put the arguments together than calls a function object using the elements of this object. The advantage is that it can deduce the return type. The actual specific logic looks like this:

    template <typename F, typename T, int... I>
    auto function_apply(F&& f, T&& t, indices<I...> const*)
        -> decltype(f(std::get<I>(t)...)) {
        f(std::get<I>(t)...);
    }
    
    template <typename F, typename T>
    auto function_apply(F&& f, T&& t)
        -> decltype(function_apply(std::forward<F>(f), std::forward<T>(t),
                                   make_indices<T>())) {
        function_apply(std::forward<F>(f), std::forward<T>(t),
                       make_indices<T>());
    }
    

    ... which is called using an expression like this:

    void f(int i, double d, bool b) {
        std::cout << "i=" << i << " d=" << d << " b=" << b << '\n';
    }
    
    int fi() { std::cout << "int\n"; return 1; }
    double fd() { std::cout << "double\n"; return 2.1; }
    bool fb() { std::cout << "bool\n"; return true; }
    
    int main()
    {
        std::cout << std::boolalpha;
        function_apply(&f, std::tuple<int, double, bool>{ fi(), fd(), fb() });
    }
    

    The main disadvantage is that this approach requires the specification of the std::tuple<...>'s elements. Another problem is that the current version of gcc on MacOS calls the functions in the opposite order they appear, i.e., the order of evaluation in a braced-init-list isn't obeyed (a gcc bug) or doesn't exist (i.e., I misunderstood the guarantees of using a braced-init-list. clang on the same platform executes the functions in the expected order.

    The used function make_indices() just creates a suitable pointer to an object of type indices<I...> with a list of indices usable with a std::tuple<...>:

    template <int... Indices> struct indices;
    template <> struct indices<-1> { typedef indices<> type; };
    template <int... Indices>
    struct indices<0, Indices...>
    {
        typedef indices<0, Indices...> type;
    };
    template <int Index, int... Indices>
    struct indices<Index, Indices...>
    {
        typedef typename indices<Index - 1, Index, Indices...>::type type;
    };
    
    template <typename T>
    typename indices<std::tuple_size<T>::value - 1>::type const*
    make_indices()
    {
        return 0;
    }
    
    0 讨论(0)
  • 2020-11-27 17:21

    Can the guarantees to constructors be used to build a function call facility ("function_apply()") with an ordering guarantee for the evaluation of arguments?

    Yes, the Fit library already does this with fit::apply_eval:

     auto result = fit::apply_eval(f, [&]{ return foo() }, [&]{ return bar(); });
    

    So foo() will be called before bar().

    0 讨论(0)
  • 2020-11-27 17:32

    What about a silly wrapper class like this:

    struct OrderedCall
    {
        template <typename F, typename ...Args>
        OrderedCall(F && f, Args &&... args)
        {
            std::forward<F>(f)(std::forward<Args>(args)...);
        }
    };
    

    Usage:

    void foo(int, char, bool);
    
    OrderedCall{foo, 5, 'x', false};
    

    If you want a return value, you could pass it in by reference (you'll need some trait to extract the return type), or store it in the object, to get an interface like:

    auto x = OrderedCall{foo, 5, 'x', false}.get_result();
    
    0 讨论(0)
  • 2020-11-27 17:34

    First, I think if the order does matter it's probably best to explicitly construct those elements prior to the call, then pass them in. Much easier to read, but far less fun!

    This is just expanding on Kerrek's answer:

    #include <utility>
    
    namespace detail
    {
        // the ultimate end result of the call;
        // replaceable with std::result_of? I do not know.
        template <typename F, typename... Args>
        static auto ordered_call_result(F&& f, Args&&... args)
            -> decltype(std::forward<F>(f)
                        (std::forward<Args>(args)...)); // not defined
    
        template <typename R>
        class ordered_call_helper
        {
        public:
            template <typename F, typename... Args>
            ordered_call_helper(F&& f, Args&&... args) :
            mResult(std::forward<F>(f)(std::forward<Args>(args)...))
            {}
    
            operator R()
            {
                return std::move(mResult);
            }
    
        private:
            R mResult;
        };
    
        template <>
        class ordered_call_helper<void>
        {
        public:
            template <typename F, typename... Args>
            ordered_call_helper(F&& f, Args&&... args)
            {
                std::forward<F>(f)(std::forward<Args>(args)...);
            }
        };
    
        // perform the call then coax out the result member via static_cast,
        // which also works nicely when the result type is void (doing nothing)
        #define ORDERED_CALL_DETAIL(r, f, ...) \
                static_cast<r>(detail::ordered_call_helper<r>{f, __VA_ARGS__})
    };
    
    // small level of indirection because we specify the result type twice
    #define ORDERED_CALL(f, ...) \
            ORDERED_CALL_DETAIL(decltype(detail::ordered_call_result(f, __VA_ARGS__)), \
                                f, __VA_ARGS__)
    

    And an example:

    #include <iostream>
    
    int add(int x, int y, int z)
    {
        return x + y + z;
    }
    
    void print(int x, int y, int z)
    {
        std::cout << "x: " << x << " y: " << y << " z: " << z << std::endl;
    }
    
    int get_x() { std::cout << "[x]"; return 11; }
    int get_y() { std::cout << "[y]"; return 16; }
    int get_z() { std::cout << "[z]"; return 12; }
    
    int main()
    {
        print(get_x(), get_y(), get_z());
        std::cout << "sum: " << add(get_x(), get_y(), get_z()) << std::endl;
    
        std::cout << std::endl;   
    
        ORDERED_CALL(print, get_x(), get_y(), get_z());
        std::cout << "sum: " << ORDERED_CALL(add, get_x(), get_y(), get_z()) << std::endl;
    
        std::cout << std::endl;
    
        int verify[] = { get_x(), get_y(), get_z() };
    }
    

    That last line is there to verify brace initializers do in fact have effect, normally.

    Unfortunately as has been discovered from other answers/comments, GCC does not get it right so I cannot test my answer. Additionally, MSVC Nov2012CTP also does not get it right (and has a nasty bug that chokes on the ordered_call_result†). If someone wants to test this with clang, that would be swell.

    †For this particular example, the trailing return type can be decltype(f(0, 0, 0)) instead.

    0 讨论(0)
提交回复
热议问题