包含成员函数调用的模板化类的行为

时间:2019-05-26 14:33:03

标签: c++ visual-c++ wrapper

我有一个要在循环内迭代调用的类的函数,并且在循环固定的同时,我希望能够提供不同的功能(与给定的对象不同)。为了解决这个问题,我创建了一个模板化结构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类将为我提供对象生命周期的持续时间,因此它是一种快速而肮脏的方式来测量某些代码块。

所以反正...

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

为什么编译器(msvc 2017)不知道它应该以与直接求值相同的优化考虑方式对待调用weq.eval(n)(因此是(t.*f)(a, n))(如果签名和函数)是在编译时给出的?

1 个答案:

答案 0 :(得分:1)

函数参数是其自己的变量,该变量从函数调用参数进行初始化。因此,当调用函数中的函数参数是左值(例如先前定义的对象的名称),并且函数参数是对象类型而不是引用类型时,参数和参数是两个不同的对象。如果参数具有类类型,则意味着必须执行该类型的构造函数(除非初始化是来自{}初始化程序列表的聚合初始化)。

换句话说,每次致电

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

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

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

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

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

但是您可能要进行另一个更改,尤其是因为Wrapper已经是模板:只需使函数使用泛型类型,以便它可以容纳非成员函数指针,并使用以下方法包装成员函数指针:任何具有operator()成员的签名,lambda或任何其他类类型。

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