【问题标题】:Using std::function for member functions对成员函数使用 std::function
【发布时间】:2015-05-08 18:45:58
【问题描述】:

我的问题是关于使用 std::function 对方法进行分类。假设我有以下类层次结构:

class Foo {
public:
    virtual void print() {
        cout << "In Foo::print()" << endl;
    }

    virtual void print(int) {
        cout << "In Foo::print(int)" << endl;
    }
};

class Bar : public Foo {
public:
    virtual void print() override {
        cout << "In Bar::print()" << endl;
    }

    virtual void print(int) override {
        cout << "In Bar::print(int)" << endl;
    }
}

现在有另一个函数应该根据其输入动态调用两个类方法之一:

void call(Foo* foo, void (Foo::*func)(void)) {
    (foo->*func)();
}

Foo* foo = new Foo();
Bar* bar = new Bar();
call(foo, &Foo::print);
call(bar, &Foo::print);

当我使用 g++/clang++ 编译上述代码 sn-p 时,它按预期工作,输出为:

In Foo::print()
In Bar::print()

我的问题是:

  1. 既然有两个函数(重载)同名:print,当我传递类函数的地址时:&amp;Foo::print,编译器怎么知道我实际上调用的是Foo::print(void)而不是@ 987654326@?

  2. 有没有另一种方法可以概括上面的代码,这样void call(Foo*, xxx) 的第二个参数可以同时使用Foo::print(void)Foo::print(int) 来传递

  3. 是否有使用 C++11 中的新功能 std::function 来实现此功能?我知道为了将std::function 与非静态类方法一起使用,我必须使用std::bind 将每个类方法与特定的类对象绑定,但这对我来说效率太低,因为我有很多类对象被绑定。

【问题讨论】:

  • 你在原型中说过call(Foo* foo, void (Foo::*func)(void)),因此你会得到(void)重载
  • @SeverinPappadeux 没错。但是当我在传递参数时给&amp;Foo::print时,编译器是如何决定要传递哪个的?

标签: c++ c++11 c++-standard-library std-function member-functions


【解决方案1】:

由于有两个函数(重载)同名:print,当我传递类函数的地址时:&amp;Foo::print,编译器怎么知道我实际上调用的是Foo::print(void)而不是@987654324 @?

这是允许的,因为 [over.over]/p1:

在某些上下文中使用不带参数的重载函数名称被解析为一个函数,一个 指向函数的指针或指向重载集中特定函数的成员函数的指针。

编译器可以使用参数类型列表的目标类型来确定指针指向成员引用的重载集中的哪个函数:

在某些上下文中使用不带参数的重载函数名称被解析为一个函数,一个 指向函数的指针或指向重载集中特定函数的成员函数的指针。一个函数 模板名称被认为是在这样的上下文中命名一组重载函数。选择的功能 是其类型与上下文中所需的目标类型的函数类型相同的类型。 [注:..]目标可以是

     - 正在初始化的对象或引用(8.5、8.5.3、8.5.4),
— 作业的左侧 (5.18),
——函数的参数(5.2.2),
— [..]

名称Foo:print 表示一个重载集,编译器通过它查找匹配项。目标类型Foo::print(void) 存在于重载集中,因此编译器将名称解析为该重载。

是否有另一种方法可以概括上面的代码,以便void call(Foo*, xxx) 的第二个参数可以同时使用Foo::print(void)Foo::print(int) 传递

对于名称本身没有通用的方法。该名称必须解析为重载。相反,请尝试更改代码以接受像 lambda 这样的函数对象:

template<class Callable>
void call(Foo* foo, Callable&& callback) {
    callback(foo);
}

int main()
{
    call(foo, [] (Foo* f) { f->print(); f->print(1); });
}

【讨论】:

  • 太棒了!重载集合而不是单个名称的解释对我来说非常有意义,最后一个带有移动语义的 lambda 技巧既优雅又高效。非常感谢!
【解决方案2】:

首先,std::bind(几乎)完全被 C++11 lambda 所淘汰。如果可以帮助,请不要使用 std::bind 。使用您的示例代码,其中一个比其他更清晰:

const auto lambda  = [=] { foo->print(); }; // Clear!
const auto binderv = std::bind( static_cast<void(Foo::*)()>( &Foo::print ), foo ); // Gets the void version
const auto binderi = std::bind( static_cast<void(Foo::*)(int)>( &Foo::print ), foo, std::placeholders::_1 ); // Gets the int version

//const auto binderv2 = std::bind( &Foo::print, foo ); // Error! Can't tell which Foo::print()
//const auto binderi2 = std::bind( &Foo::print, foo, std::placeholders::_1 ); // Error! Can't tell which Foo::print()

lambda();   // prints "void"
binderv();  // prints "void"
binderi(1); // prints "int"

其次,编译器如何知道调用哪个重载函数?与使用非成员函数的方式相同:

#include <iostream>

void call( void (*fn)() )
{
    fn();
}

void print()    { std::cout << "void\n"; }
void print(int) { std::cout << "int\n";  }

int main()
{
    call( &print ); // prints "void"
}

这些重载函数中只有一个符合被调用函数的原型,所以编译器知道。在上面的std::bind 的情况下,它不能完全分辨,但是你可以像我一样通过强制转换来强制它。

Lambda 或 std::function 可以包装任何一个成员函数,但请注意,您不能在不同的 std::function 签名上重载函数。见here


更新

处理你的问题 #3 的正确方法——让一个函数调用函数与你的签名完全不同——是使用一些中介,比如仿函数(lambda,std::functionstd::bind,手动函子)来消除差异。

std::function&lt;void()&gt; 具有相同签名的对象,无论您调用的实际函数具有什么签名。 std::function 比 lambda 更昂贵(在存储和调用方面),但它的优点是有一个类型名,如果你需要将它存储在容器或其他东西中,你可以使用它。如果你打得好,编译器有时会内联 Lambda,因此效率可能仍然有利于 Lambda。

【讨论】:

  • 感谢您的解释! const auto lambda 很好,但它对我不起作用,因为我有很多这样的对象,比如 foo,比如说 > 1000。我假设使用这些技术我必须为每个这样的对象创建一个单独的 lambda,这将是非常低效。谢谢!
  • 是的,我同意。无论如何,感谢您的解决方案!
猜你喜欢
  • 2015-11-24
  • 2023-03-31
  • 1970-01-01
  • 2015-03-04
  • 2012-08-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多