C++ lambda表达式作为类方法

9

作为一个假设性的问题,我想使用lambda作为类方法。我理解在专业环境中这是不好的,但我还是非常好奇。最好用一个例子来展示我想要做什么。这里是一个关于复数的基本类:

class Complex {
    private:
        double re, im;
    public:
        Complex() : re(0.0), im(0.0) {}
        Complex(double re, double im) : re(re * 1.0), im(im * 1.0) {}
        Complex(const Complex &c) = default;

        ~Complex() = default;

        function<double(void)> getRe = [=]() -> double { return re; };
        function<void(double)> setRe = [&](double re) -> void { this->re = re; };

        function<double(void)> getIm = [=]() -> double { return im; };
        function<void(double)> setIm = [&](double im) -> void { this->im = im; };

};

起初我尝试使用auto代替显式指定函数类型,但是错误提示说我不能在非静态字段中使用auto
看起来似乎确实可以工作,因为它显然产生了期望的行为。我用它来绘制一些使用OpenGL的分形图形,因此最终需要进行一些相当密集的工作。
正如我所说,似乎可以工作,我使用引用捕获来设置setter,特别是因为我想到由于this是对当前实例的引用,可能会需要它;而对于getter,我使用值捕获,因为默认情况下标识符会搜索(在这种情况下)类范围并找到字段。
我有两个问题:
  1. 我还没有使用Visual Studio测试过,但在我使用的CLion和MSVC编译器中,this被强调为一个不存在的变量(即使它“工作”)。你有什么想法吗?
  2. 这个类最终变得很慢。从使用像double getRe() {return re;}这样的普通getter和setter时绘图是完全瞬间完成的,到需要2-3秒钟。为什么会发生这种情况?

7
请注意,std::function 不是 lambda。它是一种类型擦除的类似函数的类型,可以保存 lambda。 - Justin
此外,std::function 是可变的,因此其他代码可以用任意其他方法替换这些方法。 - Mooing Duck
2
在这里使用lambda与常规方法相比有什么好处? - Slava
1
在您的代码中,每个实例都有自己巨大的 std::function 成员。如果您为该类使用一些动态分配,减速很可能是由于更多的内存分配引起的。在普通情况下,这些函数地址是硬编码到汇编中的。 - llllllllll
1
注意:如果你要有一个public的getter和setter方法,而这些方法并没有做任何保护成员或提供其他附加值,那么你只比拥有一个public成员好一点点。任何傻瓜都可以使用getter来查看值,并使用setter将成员更改为他们选择的任何值,就像一个public成员一样不被察觉。你所得到的只是一个放置调试器断点以尝试找出谁是傻瓜的地方。 - user4581301
显示剩余2条评论
2个回答

7

我喜欢这个想法,但在实践中它并不太奏效。

std::function是一个类类型,就像您可能编写的任何其他自定义类一样。sizeof(std::function)因实现而异,但合理的值为24字节1。这意味着,相对于每个成员函数添加0字节,您将为每个成员std::function添加24字节,从而为sizeof(Complex)添加24字节。与大多数计算机上的sizeof(double) == 8相比,这是很大的开销:您的Complex类型可以是16字节,但实际上却约为112字节。

此外,每个std::function成员都必须被初始化,可能需要堆分配,并且调用std::function涉及虚函数(或等效函数)因为类型擦除。这使得编译器很难进行优化,几乎不可能内联函数,而常规成员函数由于其简单性几乎保证被内联。

使用std::function来处理成员函数会使你的类型变得更大,初始化需要更多工作,并且更难进行优化。这就是为什么它速度更慢的原因。
1: 目前,libstdc++,libc++和MSVC的STLsizeof(std::function)分别为32字节、48字节和64字节。
为了避免每个对象的开销,你可以使用static constexpr成员(至少在C++17中),但这样你就需要一个显式的this参数,这将删除所有成员函数具有的好处。你需要写Complex::getRe(myComplex)而不是myComplex.getRe()

1
具有显式this参数的想法已经在C++23中实现。它具有一些很好的用例,如去重cont和non-const成员函数以及更容易的CRTP。但是,如果您使用它,必须在函数内部使用self来访问其他成员(类似于Python3)。 - Sourav Kannantha B

-2

让我为您修复这段代码:

struct Complex {
    double re, im;
    Complex() : re(0.0), im(0.0) {}
    Complex(double re, double im) : re(re * 1.0), im(im * 1.0) {}
    Complex(const Complex &c) = default;

    ~Complex() = default;
};

完全相同的结果,更好的可读性,更小的内存占用。去吧。你的代码在const-correctness方面也非常糟糕。

是的,你的代码非常慢,因为有多个因素:

  • 你现在调用的方法已经虚拟化了,所以它们不会被内联,最终你会因此受到惩罚
  • 你的对象创建/销毁现在很可能会产生动态内存分配/释放
  • 你的对象变得更大了,因此会出现更多的缓存未命中

7
这并没有回答问题。OP 表示他知道自己的想法不是最优的。这个问题是在询问关于他次优设计带来性能损失的具体问题的澄清。 - François Andrieux
2
@FrançoisAndrieux,这是一个我喜欢的老笑话,讲述了一位病人去看医生并问道,“医生,当我往我的耳朵里插钉子时,非常疼痛,我该怎么办?” 然后好医生回答说 - “不要往你的耳朵里插钉子。” 在这里同样如此,所有这些苦难都是无意义的,避免苦难的最好方法就是不去造成它。 - SergeyA
6
这里并没有涉及到痛苦,这是关于解释观察的问题。尝试理解为什么糟糕的代码是糟糕的,或者是什么使得糟糕的代码变得糟糕,这是值得追求的。当问到“这段代码为什么不好?”时,“它很糟糕,不要那样做”这样的回答并没有提供多少帮助。 - François Andrieux
@FrançoisAndrieux,我仍然认为代码总体上不好,但如果你坚持,我添加了一些指针。 - SergeyA
1
@SergeyA 嘿,抱歉但是Francois是对的,这个回答实际上并没有为问题增加任何有建设性的内容。我知道它不是最佳选择,显然如此。毫无疑问,这段代码很糟糕,而且我已经声明过我不打算写像那样的实际代码。我也认为,通过自己的发现,那段代码非常糟糕。但正如其他人所说,我的问题仅仅是关于C++内部机制,即在底层发生了什么使其变得糟糕。让我好奇的是,那段代码看起来很酷,这让我想知道底层发生了什么。仅此而已。 - Rares Dima
1
@RaresDima 我想,我的最新编辑应该解决了那些问题吧? - SergeyA

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接