C++ 在虚方法中使用捕获 lambda

Posted

技术标签:

【中文标题】C++ 在虚方法中使用捕获 lambda【英文标题】:C++ using capturing lambda inside virtual method 【发布时间】:2019-03-11 00:36:36 【问题描述】:

我尝试使用以下 C++ 代码,但在使用 g++ 4.9.2 和 --std=c++11 进行编译时,由于在 bar::s 内调用 z 时出现 no matching function for call to ... 错误,但编译失败:

template <class T>
class foo 
    public:
        virtual void s( T scale , const foo<T>& rhs ) 
        
;

template <class T>
class bar : public foo<T> 
    public:
        T z( T init , T (*f)( T a , T& x , const T& y ) , const foo<T>& rhs ) 
        
        void s( T scale , const foo<T>& rhs ) 
            this->z( ((T)0) , [=]( T a, T& l, const T& r ) return ((l+=scale*r)?((T)0):((T)0)); , rhs );
        
;

int main () 
    bar<float>* a = new bar<float>();
    foo<float>* b = new foo<float>();
    return 0;

如果我删除方法定义前面的 virtual 以及如果我从 lambda 中删除捕获的 scale,则代码编译。如果正确理解问题,它的工作原理如下:当方法不是虚拟方法时,通过其关联类型的 operator () 使用带有捕获的 lambda。当方法是虚拟的时,不能使用该操作符,因为它不能通过动态调度调用,只能通过正常的方法调用。但是 lambda 也不能转换为函数指针,因为这种转换仅适用于无捕获 lambda。如果s 是虚拟的,我假设z 是通过动态调度调用的,即使z 不是虚拟的。这个分析正确吗?

我不明白为什么z 不能通过普通的方法调用来调用——毕竟它不是虚拟的。我也不明白为什么删除模板参数并将所有Ts 替换为floats 会使虚拟和非虚拟情况的编译失败(删除捕获的scale 仍然允许成功编译)。

无论如何,有没有一种简单而有效的方法来解决这个问题?我想在模板类的虚拟和非虚拟方法中使用 lambdas 和捕获(或类似的东西) .我知道std::function,但它相当重量级。而且无捕获 lambda 的功能远不如带捕获的 lambda,因此它们可能无法解决我的问题。

PS:如果你想知道 z 应该做什么:它是 Haskell 中 zipWith 后跟 foldl 的组合(或者有点像带有两个输入列表的 mapreduce,如果你不这样做的话'不知道 Haskell)并且可以用于大多数使用 foos 的一元和二元运算。在我最初的实现中,init 不是 T 类型,而是用作类型的附加模板参数,为简单起见,此处已将其删除。

【问题讨论】:

您需要std::function。捕获 lambdas 不能衰减到函数指针 使用 gcc 8,代码仅通过删除捕获(并且还从 z 返回一些内容)为我编译。虚拟性无关紧要。只有非捕获的 lambda 可以转换为普通函数指针。 virtual 是一个红鲱鱼。没有虚拟也无法编译代码。在您尝试调用 bar::s 方法之前,您不会收到错误消息。将此行添加到您的代码中:a-&gt;s(0, foo&lt;float&gt;());。出现相同的错误。要解决此问题,请让 z 接受函子:template&lt;typename F&gt; T z( T init, F&amp;&amp; f, const foo&lt;T&gt;&amp; rhs ) ... 【参考方案1】:

使用模板。

virtual 这里实际上与您的问题没有任何关系。问题是原始函数指针不能携带任何状态,因此无法将带有捕获的 lambda 转换为函数指针。

您应该将z 设为接受任何类型作为其第二个参数的模板,而不是原始函数指针:

template <class T>
class foo 
    public:
        virtual void s( T scale , const foo<T>& rhs ) 
        
;

template <class T>
class bar : public foo<T> 
    public:
        template <typename F>
        T z( T init , F&& f, const foo<T>& rhs ) 
            f(/*some args*/);
        
        void s( T scale , const foo<T>& rhs ) 
            this->z( ((T)0) , [=]( T a, T& l, const T& r ) return ((l+=scale*r)?((T)0):((T)0)); , rhs );
        
;

int main () 
    bar<float>* a = new bar<float>();
    foo<float>* b = new foo<float>();
    return 0;

Live Demo

现在z 通过其实际类型接受您的 lambda,它的状态可以被携带,一切正常。

【讨论】:

谢谢,这似乎确实有效。我希望它不会影响性能。 它实际上可能比函数指针执行得更好,因为对F::operator() 的调用可以在编译时解决。使用函数指针或std::function,该调用可能必须在运行时解析,这可能代表一定程度的开销。 virtual 与 OP 的问题有很大关系;当它不是虚拟的时,编译器会跳过从未调用过的s,因此编译器不会抱怨其中的错误代码。 :) 使用 virtual,如果从未调用过的 s 被实例化(但我知道的所有编译器都这样做),它就是实现定义的。因此,如果 OP 的问题是“它不会编译”,那么虚拟很重要;如果 OP 的问题是“它行不通”,那么,那么,不那么重要了。【参考方案2】:

正如 Raymond Chen 所指出的,虚拟性确实是一条红鲱鱼。

暂时,我决定采用这种丑陋的解决方法,但我不会接受我的答案,因为它不是真正的解决方案:

template <class T>
class foo 
    public:
        virtual void s( T scale , const foo<T>& rhs ) 
        
;

template <class T>
class bar : public foo<T> 
    public:
        T z( T init , T (*f)( T a , T& x , const T& y ) , const foo<T>& rhs ) 
        
        template <class U>
        U z2( U init , T capture_0 , T capture_1 , U (*f)( T capture_0 , T capture_1 , U a , T& x , const T& y ) , const foo<T>& rhs ) 
            return init;
        
        void s( T scale , const foo<T>& rhs ) 
            this->z2<T>( ((T)0) , scale , ((T)0) , []( T c0 , T c1 , T a, T& l, const T& r ) return ((l+=c0*r)?((T)0):((T)0)); , rhs );
            //this->z( ((T)0) , [=]( T a, T& l, const T& r ) return ((l+=scale*r)?((T)0):((T)0)); , rhs );
        
;

int main () 
    bar<float>* a = new bar<float>();
    foo<float>* b = new foo<float>();
    a->s(0,*b);
    return 0;

这编译并允许我使用 z2 最多 2 个正确类型的“伪捕获”,有或没有虚拟性,甚至还有我在问题中遗漏的附加模板参数。不需要的捕获可以简单地设置为 0 并忽略。

【讨论】:

以上是关于C++ 在虚方法中使用捕获 lambda的主要内容,如果未能解决你的问题,请参考以下文章

C++ 关于类与对象在虚函数表上唯一性问题 浅析

几秒读懂C++虚函数调用的汇编代码实现

探索c++虚函数表

在 C++11 或以上,有没有办法通过 lambda 实现单方法纯虚拟 C++ 接口?

Java – 虚函数抽象函数抽象类接口

C++ ——虚继承时的构造函数