在Qt 5中连接过载的信号和槽

166

我在学习Qt 5中描述的新信号/槽语法(使用成员函数指针)时遇到了麻烦,参考New Signal Slot Syntax。我尝试更改以下内容:

QObject::connect(spinBox, SIGNAL(valueChanged(int)),
                 slider, SLOT(setValue(int));

变成这样:

QObject::connect(spinBox, &QSpinBox::valueChanged,
                 slider, &QSlider::setValue);

但是当我尝试编译它时,出现了错误:

错误:没有找到匹配的函数来调用

我在Linux上尝试了clang和gcc,都使用了-std=c++11

我做错了什么,如何修复?


如果您的语法正确,那么唯一的解释就是您没有链接到Qt5库,而是链接到了Qt4等其他库。在“项目”页面上使用QtCreator很容易验证这一点。 - Matt Phillips
我包含了一些QObject的子类(QSpinBox等),所以应该已经包含了QObject。我尝试添加了那个include,但它仍然无法编译。 - dtruby
另外,我肯定是链接到Qt 5,我正在使用Qt Creator,并且我正在测试的两个工具包都将Qt 5.0.1列为它们的Qt版本。 - dtruby
4个回答

298

这里的问题在于有两个同名的信号:QSpinBox::valueChanged(int)QSpinBox::valueChanged(QString)。从Qt 5.7开始,提供了帮助函数来选择所需的重载,因此您可以编写以下代码:

connect(spinbox, qOverload<int>(&QSpinBox::valueChanged),
        slider, &QSlider::setValue);

对于Qt 5.6及更早版本,您需要将其转换为正确的类型,告诉Qt您想要选择哪一个:

connect(spinbox, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged),
        slider, &QSlider::setValue);

我知道,这看起来很丑。但是没有其他办法。今天的课程是:不要过载你的信号和槽!


补充说明:真正让人恼火的是转换操作符:

  1. 需要重复指定类名。
  2. 即使返回值通常为void(对于信号),也必须指定返回值。

所以,我有时会使用这个C++11片段:

template<typename... Args> struct SELECT { 
    template<typename C, typename R> 
    static constexpr auto OVERLOAD_OF( R (C::*pmf)(Args...) ) -> decltype(pmf) { 
        return pmf;
    } 
};

使用方法:

connect(spinbox, SELECT<int>::OVERLOAD_OF(&QSpinBox::valueChanged), ...)

我个人认为这并不是很有用。我期望当Creator(或您的IDE)在自动完成获取PMF操作时自动插入正确的转换时,此问题将自行解决。但同时...
注意:基于PMF的连接语法不需要C++11
补充2:在Qt 5.7中,添加了帮助函数来缓解此问题,模拟了我的上述解决方法。主要的帮助函数是qOverload(还有qConstOverloadqNonConstOverload)。
使用示例(来自文档):
struct Foo {
    void overloadedFunction();
    void overloadedFunction(int, QString);
};

// requires C++14
qOverload<>(&Foo:overloadedFunction)
qOverload<int, QString>(&Foo:overloadedFunction)

// same, with C++11
QOverload<>::of(&Foo:overloadedFunction)
QOverload<int, QString>::of(&Foo:overloadedFunction)

补充 3: 如果您查看任何重载信号的文档,现在解决重载问题的方法已经明确地在文档中说明。例如,https://doc.qt.io/qt-5/qspinbox.html#valueChanged-1 中写道:

Note: Signal valueChanged is overloaded in this class. To connect to this signal by using the function pointer syntax, Qt provides a convenient helper for obtaining the function pointer as shown in this example:

   connect(spinBox, QOverload<const QString &>::of(&QSpinBox::valueChanged),
[=](const QString &text){ /* ... */ });

1
啊,是的,这很有道理。我想对于像这样信号/槽被重载的情况,我会坚持使用旧语法 :-). 谢谢! - dtruby
17
我对新语法感到非常兴奋……现在却被一场冰冷打击所冻结的失望。 - RushPL
14
对于那些像我一样想知道的人: "pmf"代表“指向成员函数的指针”。 - Vicky Chijwani
18
我个人更喜欢使用static_cast的语法,尽管它不太美观,因为新语法能够在编译时检查信号/槽的存在性,而旧语法则只能在运行时失败。 - Vicky Chijwani
2
不幸的是,不重载信号通常不是一个选项 - Qt经常重载他们自己的信号。(例如 QSerialPort) - PythonNut
显示剩余6条评论

16
错误信息为:

错误:无法匹配函数调用 QObject::connect(QSpinBox*&, <未解决的重载函数类型>, QSlider*&, void (QAbstractSlider::*)(int))

其中重要的部分是提到了 "未解决的重载函数类型"。编译器不知道你是想使用 QSpinBox::valueChanged(int) 还是 QSpinBox::valueChanged(QString)
有几种方法可以解决这个重载问题:
  • Provide a suitable template parameter to connect()

    QObject::connect<void(QSpinBox::*)(int)>(spinBox, &QSpinBox::valueChanged,
                                             slider,  &QSlider::setValue);
    

    This forces connect() to resolve &QSpinBox::valueChanged into the overload that takes an int.

    If you have unresolved overloads for the slot argument, then you'll need to supply the second template argument to connect(). Unfortunately, there's no syntax to ask for the first to be inferred, so you'll need to supply both. That's when the second approach can help:

  • Use a temporary variable of the correct type

    void(QSpinBox::*signal)(int) = &QSpinBox::valueChanged;
    QObject::connect(spinBox, signal,
                     slider,  &QSlider::setValue);
    

    The assignment to signal will select the desired overload, and now it can be substituted successfully into the template. This works equally well with the 'slot' argument, and I find it less cumbersome in that case.

  • Use a conversion

    We can avoid static_cast here, as it's simply a coercion rather than removal of the language's protections. I use something like:

    // Also useful for making the second and
    // third arguments of ?: operator agree.
    template<typename T, typename U> T&& coerce(U&& u) { return u; }
    

    This allows us to write

    QObject::connect(spinBox, coerce<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged),
                     slider, &QSlider::setValue);
    

9
实际上,你可以使用lambda和this来包装你的插槽:
connect(spinbox, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged),
    slider, &QSlider::setValue);

会看起来更好。:\

0
以上的解决方案是有效的,但我用宏略微不同的方式解决了这个问题。以防万一,这里是我的代码:
#define CONNECTCAST(OBJECT,TYPE,FUNC) static_cast<void(OBJECT::*)(TYPE)>(&OBJECT::FUNC)

将以下代码添加到你的程序中。
然后,是你的示例:
QObject::connect(spinBox, &QSpinBox::valueChanged,
             slider, &QSlider::setValue);

变成:

QObject::connect(spinBox, CONNECTCAST(QSpinBox, double, valueChanged),
             slider, &QSlider::setValue);

2
解决方案“高于”什么?不要假设答案按照你当前看到的顺序呈现给每个人! - Toby Speight
1
你如何在有多个参数的重载函数中使用它?逗号不会引起问题吗?我认为你真的需要传递括号,即 #define CONNECTCAST(class,fun,args) static_cast<void(class::*)args>(&class::fun) - 在这种情况下使用 CONNECTCAST(QSpinBox, valueChanged, (double)) - Toby Speight
当括号用于多个参数时,这是一个非常有用的宏,就像托比的评论中所说的那样。 - ejectamenta

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