我试图实现std::function更快、更替代的实现。我想出了下面的代码:
template <typename R, typename ...Args>
struct Function {
template <typename Lambda>
Function(Lambda f) {
static auto function = f;
func = + [] (Args... args) -> R {
return function(args...);
};
};
Function(const Function& other) : func(other.func) {}
inline auto operator() (Args... args) {
return func(args...);
}
R (*func) (Args... args);
};
现在您可以实例化如下所示的函数:
int y = 100;
int x = 56;
auto f1 = Function<int , int>([&] (int z) {return x + y + z});
// If you need to store a generic custom functor class:
auto functor = SomeCustomFunctorClass<int, int>(); // Assuming functor takes an int as an argument and returns an int
auto f2 = Function<int , int>([functor] (int x) {return functor(x);});
请注意,我的实现函数只能存储lambda,但是可以通过上面的示例来存储泛型lambda。
这里的想法是通过静态存储来清除构造函数中lambda的类型,这意味着我可以使用它函数指针func
。注意,每次调用构造函数时都会实例化不同的静态变量,因为lambdas都有唯一的类型,因此每次使用不同的构造函数实例化构造函数的新版本。
我已经对这个实现进行了基准测试,它的运行速度明显快于std::function (启用了优化)。它的构造速度是6-7倍,复制速度是15倍,调用它的速度和调用函数指针一样快(尽管比调用始终内联的lambda要慢得多)。
然而,这似乎太简单了,这个实现只有15行代码。与使用堆分配和虚拟调用的std::函数的传统实现相比,我的实现有什么缺点吗?也许静态分配lambda不是个好主意吗?
发布于 2021-01-24 19:40:45
。
是的,绝对是个坏主意,有很多缺点。
因为没有编译时检查来确保实际使用lambda,所以这种类型非常脆弱,非常危险。今天,我可以写这样的代码:
auto x = 0;
auto y = 0;
auto func_x = [&x] { ++(*x); };
auto func_y = [&y] { ++(*y); };
// ... later:
auto function_x = Function<void>{func_x};
auto function_y = Function<void>{func_y};
那里没问题。(嗯,我的意思是,是的,问题,但不是这个特定的假定用例。但我们会回到这个问题上。)
但是,在稍后的开发过程中,有人将这两个lambdas重组为:
struct indirect_incrementer
{
int* p_val = nullptr;
auto operator()() { ++(*p_val); }
};
// then the following two lines:
//auto func_x = [&x] { ++(*x); };
//auto func_y = [&y] { ++(*y); };
// become:
auto func_x = indirect_incrementer{&x};
auto func_y = indirect_incrementer{&y};
这是一个无辜的,完全合乎逻辑的重构…但是现在一切都崩溃了,因为您的假设是,每个函数对象类型Function
的构造都不再有效。静态变量function
是用func_x
的副本初始化的,因此调用function_y
不调用func_y
,而是调用func_x
。
这不仅仅是使用函数对象的问题。即使您在某种程度上完全禁止在Function
-like中使用函数对象,比如使用某种linter之类的东西,并且100%地确保始终使用合法的lambda对象调用Function
,但问题仍然存在。因为不管你的想法,它是有可能的类型是非独特的。做…甚至很简单只需多次调用包含lambda的函数:
auto render()
{
// ... [snip] ...
// somewhere in your render function, you use Function:
auto func = Function<int, int>{[&] (int i) { return ++i; }};
// ... [snip] ...
}
// elsewhere, in your main game loop:
while (not done)
{
input();
update();
render(); // <- !!!
}
render()
在循环中被调用,但是只有第一个循环将初始化静态function
变量。这意味着每隔一次使用func
,它将在第一个render()
调用中使用对局部变量的引用。
现在,您可以用各种黑客“修复”这个问题,基本上可以检测function
以前是否被初始化,或者计算它被初始化的次数,或者类似的事情。但它解决不了其他问题。
使用静态变量来保存lambda副本的另一个大问题是,您正在悄悄地欺骗C++‘S范围规则。这可能会导致令人沮丧和令人讨厌的惊喜。例如:
auto p_weak = std::weak_ptr<int>{};
// in some more restricted scope:
{
auto p = std::make_shared<int>();
p_weak = p;
auto func = Function<void>{[p] { if (p) ++(*p); }};
// use func somehow
// scope is ending, so func and p are being destroyed, right?
//
// ... *right*?
}
if (auto p = p_weak.lock(); p)
std::cerr << "memory leak!";
事实证明,p
从未被实际销毁;lambda获取一个副本,然后复制到静态function
变量中,然后该变量保持它为…。永远(至少在main()
回来之后)。这不仅仅是一个shared_ptr
问题;我只是使用shared_ptr
来访问丢失的内存。任何由lambda捕获的值在程序的生存期内都不会被销毁。换句话说,每次使用Function
-even就越多地避免了提到的其他问题--越多地泄漏内存。
这可能就是为什么你看到了如此大的速度增长:函数对象只被复制一次--即使这是不正确的--而且它永远不会被破坏。
(而且,std::function
还有很多非常重要和有用的其他能力和好处,Function
没有提供,比如空构造和重新分配的能力。我想,仅仅类型擦除是有用的,但在它自己的…上可能不够有用。特别是当简单的函数指针可以这样做的时候(还有更多)。
所以是的,使用静态变量来绕过动态分配不是一个好主意。
template <typename Lambda>
Function(Lambda f) {
static auto function = f;
func = + [] (Args... args) -> R {
return function(args...);
};
};
适当的缩进肯定有助于提高这一点的可读性。它也会突出你在结尾的迷途分号。
将f
复制到function
中的事实使整个类无法使用只移动的lambda类型:
auto p = std::make_unique<int>();
auto lambda_p = [p = std::move(p)] { if (p) { *p = 42; } };
auto func_p = Function<void>{lambda_p}; // won't compile
// (also, as mentioned previously, p won't ever be freed)
您还应该在所有函数调用中使用转发引用。现在,每个函数参数都将由值获得,这至少是缓慢的,而且在很多情况下可能不会编译。
Function(const Function& other) : func(other.func) {}
这是完全不必要的。
inline auto operator() (Args... args) {
return func(args...);
}
inline
关键字在这里什么也不做。
同样,您应该使用转发引用。
Function
的整个接口也相当笨重。它似乎似是而非,必须给返回和参数类型,当它们是显而易见的,并可推断-从lambda本身。更好的是能够写:
auto f1 = Function{[&] (int z) {return x + y + z}};
// f1 is deduced as Function<int, int>
在这种情况下,扣减指南将是有用的。
您无法避免使用函数对象包装器实际包装函数对象。这么说吧,这应该是很明显的。
您的类型擦除包装要么需要动态分配内存来复制函数对象,要么使用小对象优化,或者两者兼而有之。试图将包装的对象隐藏在静态变量中是作弊。你可以暂时离开它,…但是您最终会被捕获,要么是通过内存泄漏、读取/写入悬空引用,要么是简单地读取/写入错误的对象。
https://codereview.stackexchange.com/questions/255176
复制相似问题