首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >替代std::lambdas的功能实现

替代std::lambdas的功能实现
EN

Code Review用户
提问于 2021-01-24 10:58:52
回答 1查看 997关注 0票数 4

我试图实现std::function更快、更替代的实现。我想出了下面的代码:

代码语言:javascript
运行
复制
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);
};

现在您可以实例化如下所示的函数:

代码语言:javascript
运行
复制
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不是个好主意吗?

EN

回答 1

Code Review用户

发布于 2021-01-24 19:40:45

问答

“与使用堆分配和虚拟调用的std::函数的传统实现相比,我的实现有什么缺点吗?也许静态分配lambda不是个好主意吗?“

是的,绝对是个坏主意,有很多缺点。

因为没有编译时检查来确保实际使用lambda,所以这种类型非常脆弱,非常危险。今天,我可以写这样的代码:

代码语言:javascript
运行
复制
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重组为:

代码语言:javascript
运行
复制
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的函数:

代码语言:javascript
运行
复制
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范围规则。这可能会导致令人沮丧和令人讨厌的惊喜。例如:

代码语言:javascript
运行
复制
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没有提供,比如空构造和重新分配的能力。我想,仅仅类型擦除是有用的,但在它自己的…上可能不够有用。特别是当简单的函数指针可以这样做的时候(还有更多)。

所以是的,使用静态变量来绕过动态分配不是一个好主意。

代码评审

代码语言:javascript
运行
复制
template <typename Lambda>
Function(Lambda f) {
static auto function = f;
func = + [] (Args... args) -> R {
    return function(args...);
};
};

适当的缩进肯定有助于提高这一点的可读性。它也会突出你在结尾的迷途分号。

f复制到function中的事实使整个类无法使用只移动的lambda类型:

代码语言:javascript
运行
复制
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)

您还应该在所有函数调用中使用转发引用。现在,每个函数参数都将由值获得,这至少是缓慢的,而且在很多情况下可能不会编译。

代码语言:javascript
运行
复制
Function(const Function& other) : func(other.func) {}

这是完全不必要的。

代码语言:javascript
运行
复制
inline auto operator() (Args... args) {
    return func(args...);
}

inline关键字在这里什么也不做。

同样,您应该使用转发引用。

Function的整个接口也相当笨重。它似乎似是而非,必须给返回和参数类型,当它们是显而易见的,并可推断-从lambda本身。更好的是能够写:

代码语言:javascript
运行
复制
auto f1 = Function{[&] (int z) {return x + y + z}};
// f1 is deduced as Function<int, int>

在这种情况下,扣减指南将是有用的。

摘要

您无法避免使用函数对象包装器实际包装函数对象。这么说吧,这应该是很明显的。

您的类型擦除包装要么需要动态分配内存来复制函数对象,要么使用小对象优化,或者两者兼而有之。试图将包装的对象隐藏在静态变量中是作弊。你可以暂时离开它,…但是您最终会被捕获,要么是通过内存泄漏、读取/写入悬空引用,要么是简单地读取/写入错误的对象。

票数 7
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/255176

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档