模板化 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
似乎不需要修改 v
或 MyEquation
,因此最好将 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);
我有一个函数 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
似乎不需要修改 v
或 MyEquation
,因此最好将 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);