【问题标题】:How to guarantee order of argument evaluation when calling a function object?调用函数对象时如何保证参数评估的顺序?
【发布时间】:2012-12-27 17:21:46
【问题描述】:

how to avoid undefined execution order for the constructors when using std::make_tuple 上问题的答案引发了一次讨论,在讨论中我了解到可以保证构造函数的参数评估顺序:使用 braced-init-list 可以保证顺序从左到右:

T{ a, b, c }

表达式abc 按给定顺序计算。情况就是这样,即使 T 类型只定义了一个普通的构造函数。

显然,并非所有被调用的东西都是构造函数,有时在调用函数时保证评估顺序会很好,但没有像 brace-argument-list 这样的东西来调用函数对他们的论点进行定义的评估顺序。问题变成了:对构造函数的保证能否用于构建函数调用工具(“function_apply()”),并为参数评估提供排序保证?要求调用函数对象是可以接受的。

【问题讨论】:

  • 有一个参数,只要它有意义,评估哪个参数需要先评估到一个变量,然后将它传递给调用。适用于包装器对象过于繁重或您希望使依赖关系更容易看到代码的情况。
  • 前段时间我们在聊天中玩得很开心chat.stackoverflow.com/transcript/message/6383436#6383436 :)

标签: c++ c++11


【解决方案1】:

像这样一个愚蠢的包装类呢:

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

用法:

void foo(int, char, bool);

OrderedCall{foo, 5, 'x', false};

如果你想要一个返回值,你可以通过引用传递它(你需要一些特征来提取返回类型),或者将它存储在对象中,以获得如下接口:

auto x = OrderedCall{foo, 5, 'x', false}.get_result();

【讨论】:

  • 这看起来真的很不错!我正在考虑先创建一个std::tuple&lt;...&gt;,但似乎没有必要创建std::tuple&lt;...&gt;,这需要拼出参数名称。
  • 有趣的是,在 MacOS 上使用 OrderedCall{&amp;foo, fi(), fd(), fb() }; 调用 fb(),然后是 fd(),最后是使用 gcc 的 fi()。正如预期的那样,使用 clang 的顺序是 fi()fd()fb()
  • @JiveDadson:braced-init-list 确实保证了根据 12.6.1 [class.explicit.init] 第 2 段和 8.5.4 [dcl .init.list] 第 4 段. ...我怀疑旧的编译器是否符合 C++ 2011。
  • @JohannesSchaub-litb:这个我也没有想太深,但是不会推导出函数类型,然后可以用decltype(f(std::declval&lt;Args&gt;()...))作为返回类型吗?
  • @DietmarKühl 同样值得怀疑的是,C++11 之前的编译器是否支持 C++11 列表初始化语法,所以如果 OrderedCall{......} 编译,它应该可以工作
【解决方案2】:

我提出的解决方案使用std::tuple&lt;...&gt; 将参数放在一起,而不是使用该对象的元素调用函数对象。好处是可以推导出返回类型。实际的具体逻辑是这样的:

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>());
}

... 使用如下表达式调用:

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() });
}

主要缺点是这种方法需要指定std::tuple&lt;...&gt; 的元素。另一个问题是 MacOS 上当前版本的 gcc 以与它们出现的相反顺序调用函数,即不遵守 braced-init-list 中的评估顺序(一个 gcc 错误)或不存在(即,我误解了使用花括号初始化列表的保证。同一平台上的 clang 按预期顺序执行函数。

使用的函数make_indices() 只是创建了一个合适的指针,指向indices&lt;I...&gt; 类型的对象,其中包含一个可用于std::tuple&lt;...&gt; 的索引列表:

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

【讨论】:

    【解决方案3】:

    首先,我认为如果顺序确实很重要,最好在调用之前显式构造这些元素,然后将它们传入。更容易阅读,但没有那么有趣!

    这只是对 Kerrek 的回答的扩展:

    #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__)
    

    还有一个例子:

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

    最后一行是用来验证大括号初始值设定项确实有效,通常情况下。

    不幸的是,正如从其他答案/cmets 中发现的那样,GCC 没有正确,所以我无法测试我的答案。此外,MSVC Nov2012CTP 也没有正确处理(并且在ordered_call_result† 上有一个令人讨厌的错误)。如果有人想用 clang 测试这个,那就太好了。

    †对于这个特定示例,尾随返回类型可以改为 decltype(f(0, 0, 0))

    【讨论】:

    • 我通常同意,通常有更好的方法,然后依赖于表达式中的评估顺序。然而,当获得一个可变参数列表时,为堆栈上的每个参数放置一个临时参数不是一种选择。不过,可以将带有参数的std::tuple&lt;...&gt; 放在堆栈上,然后扩展它以调用函数(直接调用函数的两阶段替代方案)。
    【解决方案4】:

    可以使用对构造函数的保证来构建函数调用工具(“function_apply()”),并为参数的评估提供排序保证吗?

    是的,Fit 库已经用 fit::apply_eval 做到了这一点:

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

    所以foo() 将在bar() 之前被调用。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-11-05
      • 2018-11-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-02-12
      相关资源
      最近更新 更多