模板化 class 包装成员函数调用的行为

Behaviour of templated class wrapping a member function call

我有一个函数 class 我想在一个循环内迭代调用,当循环是固定的时,我希望能够提供不同的函数(从给定的对象)。为了解决这个问题,我创建了一个模板化结构 MyWrapper 来获取我想要调用其函数的对象、函数本身以及要为其评估函数的数据。 (从这个意义上说,成员函数将始终具有相同的签名)

虽然我发现,使用成员函数指针会导致巨大的性能成本,即使在编译时,我知道我想要调用的函数。所以我四处乱逛试图解决这个问题,并且(虽然我仍然不清楚为什么会发生第一种情况),但我遇到了另一种有趣的行为。

在下面的情况下,每次调用包装函数MyWrapper::eval实际上会尝试将我的整个Grid对象复制到给定的参数中它必须包装的函数 f,即使对 MyEquation::eval 的调用将知道不会每次都复制它(因为优化)。


template<typename T>
double neighbour_average(T *v, int n)
{
    return v[-n] + v[n] - 2 * v[0];
}

template<typename T>
struct MyEquation
{
    T constant;
    int n;
    T eval(Grid<T, 2> v, int i)
    {
        return rand() / RAND_MAX + neighbour_average(v.values + i, n) + constant;
    }
};


template<typename T, typename R, typename A>
struct MyWrapper
{
    MyWrapper(T &t, R(T::*f)(A, int), A a) : t{ t }, f{ f }, a{ a } {}
    auto eval(int i)
    {
        return (t.*f)(a, i);
    }

protected:
    A a;
    T &t;
    R(T::*f)(A, int);
};


int main(int argc, char *argv[])
{

    srand((unsigned int)time(NULL));
    for (iter_type i = 0; i < config().len_; ++i)
    {
        op.values[i] = rand() / RAND_MAX;
    }

    srand((unsigned int)time(NULL));
    double constant = rand() / RAND_MAX;
    int n = 2;
    int test_len = 100'000, 
    int test_run = 100'000'000;

    Grid<double, 2> arr(100, 1000);
    MyEquation<double> eq{ constant, n };
    MyWrapper weq(eq, &MyEquation<double>::eval, arr); // I'm wrapping what I want to do

    {
        // Time t0("wrapper thing");
        for (int i = 0; i < test_run; ++i)
        {
            arr.values[n + i % (test_len - n)] += weq.eval(n + i % (test_len - n)); // a call to the wrapping class to evaluate
        }
    }
    {
        // Time t0("regular thing");
        for (int i = 0; i < test_run; ++i)
        {
            arr.values[n + i % (test_len - n)] += rand() / RAND_MAX + neighbour_average(arr.values + n + i % (test_len - n), n) + constant; // a usage of the neighbour function without the wrapping call
        }
    }

    {
        // Time t0("function thing");
        for (int i = 0; i < test_run; ++i)
        {
            arr.values[n + i % (test_len - n)] += eq.eval(arr, n + i % (test_len - n)); // raw evaluation of my equation
        }
    }

}

一些上下文:

Grid 只是一个美化的动态数组 Grid::values 和一些辅助函数。

我为我的函数和对象保留了一些(看似不必要的)模板,因为它与我的代码实际设置方式非常相似。

Time class 会给出对象生命周期的持续时间,因此它是一种快速而肮脏的测量某些代码块的方法。

所以无论如何...

如果更改以下代码,使 MyWrapper 所采用的函数签名为 R(T::*f)(A&, int),则 MyWrapper::eval 的执行时间将与其他调用几乎相同(这就是我想要的)。

如果签名函数在编译时给出?

函数参数是它自己的变量,它从函数调用参数中初始化。所以当调用函数中的函数参数是一个左值,比如之前定义的一个对象的名字,并且函数参数是一个对象类型,而不是引用类型时,形参和实参是两个不同的对象。如果参数具有 class 类型,这意味着必须执行该类型的构造函数(除非初始化是来自 {} 初始化列表的聚合初始化)。

换句话说,每次调用

T eval(Grid<T, 2> v, int i);

需要创建一个名为 v 的新 Grid<T, 2> 对象,无论是通过函数指针调用还是通过成员名称 eval.

但在很多情况下,引用的初始化不会创建新对象。您的 eval 似乎不需要修改 vMyEquation,因此最好将 eval 声明为:

T eval(const Grid<T, 2> &v, int i) const;

这意味着 Wrapper 中的函数指针需要为 R (T::*f)(const A&, int) const

但是您可能还想做另一个改变,特别是因为 Wrapper 已经是一个模板:只需让函数使用通用类型,这样它就可以保存 non-member 函数指针,包装器到具有任何签名、lambda 或任何其他 class 类型且具有 operator() 成员的成员函数指针。

#include <utility>

template<typename F, typename A>
struct MyWrapper
{
    MyWrapper(F f, A a) : f{ std::move(f) }, a{ std::move(a) } {}
    auto eval(int i)
    {
        return f(a, i);
    }

protected:
    A a;
    F f;
};

创建 Wrapper weq; 的两种方法是:

Wrapper weq([&eq](const auto &arr, int i) {
    return eq.eval(arr, i);
}, arr);

或(需要#include <functional>):

using namespace std::placeholders;
Wrapper weq(
    std::bind(std::mem_fn(&MyEquation<double>::eval), _1, _2),
    arr);