65.9K
CodeProject 正在变化。 阅读更多。
Home

使用Microsoft Visual Studio编译器介绍libsig c++

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (33投票s)

2003年11月20日

17分钟阅读

viewsIcon

185423

downloadIcon

840

使用Microsoft Visual Studio编译器介绍libsig c++

摘要

首先,将识别 C++ 事件驱动编程中的常见设计问题,然后考察几种可能的解决方案,列出它们的优缺点。然后,解释信号/槽机制,并论证为什么在许多情况下它比其他替代方案更受青睐。最后,提出一种实现该机制的方法:libsigc++ 库。接着,将解释一个使用该库的最基本信号/槽编程原理的小型示例程序。之后,该程序将被扩展,以适应其他多种情况。最后,将介绍其他一些用例并得出结论。尽管所提出的实现是跨平台的,但重点将放在使用 Microsoft C++ 编译器在 Microsoft 平台上使用它。

Outline

示例代码索引

关于示例代码

本文附带一个包含下面示例代码可编译版本的 zip 文件。提供了 Visual Studio 6 和 Visual Studio .Net 2002 的项目文件,但并非所有示例都在 Visual Studio 6 中都能正常工作。包含了一个精简版的 libsigc++ 库,但该版本不应用于生产系统;相反,应下载原始版本(位置在参考文献下)并使用目标编译器进行安装。同时,还需要将库的包含路径设置为解压 zip 文件内容的目录。

问题场景

面向对象编程的一个众所周知的基本方面是“封装”。封装是一种隐藏对象内部细节的做法,使其尽可能像一个“黑匣子”。隐藏内部细节有助于将来在不干扰现有代码的情况下修改类。

为了尽可能实现封装,类之间的依赖性应尽可能小。这在编写库时尤其重要,因为库的作者无法知道库将被如何以及在哪里使用。

但现在考虑以下情况:有一个对象需要在某个事件发生时通知另一个对象。这可能例如是按钮的单击、从网络接收数据或长时间操作的结束,例如通过 ftp 连接复制大量数据。简单的解决方案将如下所示:


#include <iostream>

class Notifier
{
    void WorkerIsFinished()
    {
        std::cout << "Worker is finished!" << std::endl;
    }
};

class Worker
{
    Worker(Notifier* notifier) : m_Notifier(notifier) {}

    void LongOperation()
    {
        // Do a long operation here
        m_Notifier->WorkerIsFinished();
    }

    Notifier* m_Notifier;
};

int main(int argc, char* argv[])
{
    Notifier notifier;
    Worker worker(notifier);

    worker.LongOperation();

    return 0;
}

需要注意的是,上面的代码仅仅是对问题的描述;更具体地说,在这种情况下,一个普通的返回值就足够了。因此,上述问题应以异步方式看待,即,假定 Worker::LongOperation() 将被启动到一个单独的线程中,并且程序将在 worker.LongOperation() 完成之前继续执行。

这显然违反了封装的目标。类 Worker 必须了解 Notifier 才能让 Notifier 知道长时间操作已完成。当有其他对象也想在 Worker 的 LongOperation() 完成时得到通知怎么办?Worker 将不得不调用它们上的函数,但为了做到这一点,它必须拥有指向它们的指针,或以其他方式知道它们的位置。这意味着 Worker 必须被修改:可能在其构造函数中接受更多参数,或通过其他方式提供获取指针的成员函数。Worker::LongOperation() 也必须更新:它也必须调用其他对象上的成员函数。这正是封装试图阻止的。显然需要一个更好的解决方案。

可能的解决方案

上述问题有几种可能的解决方案。尽管在本篇文章的范围内不可能详尽地探讨所有方案,但其中一些方案值得描述,以便对遇到此问题时必须做出的权衡进行比较。

轮询

Notifier 可以定期询问 Worker 是否已完成其 LongOperation()。这会带来几个进一步的问题,例如它应该多久询问一次。太频繁会降低性能,但等待太久会损失准确性。理想情况下,间隔应取决于 LongOperation() 预计需要多长时间,但如果无法预测怎么办?另一个问题是,当 Notifier 尚未注意到 Worker 已消失而 Worker 却消失了会发生什么?Worker 需要有一种方式来通知 Notifier 它不再可用——所有这些都必须由每个想要从 Worker 检查事件的类来实现。

Windows 消息

在 Microsoft Windows 环境中,如果 Notifier 拥有“窗口句柄”,Worker 可以向 Notifier 发送 Win32 术语中的“消息”。这揭示了该方法的两个最大问题:它只能用于 Microsoft Windows 平台的应用程序,并且 Notifier 必须有一个窗口句柄,换句话说,必须是一个“窗口”。这通常不是情况,尤其是在非 GUI 代码中。

函数指针

一个更复杂的解决方案是让 Notifier 在 Worker 上设置一个函数,当 Worker 完成其操作时必须调用该函数。最简单的方法是让 Notifier 设置一个指向全局函数的指针;Worker 可以将其存储为 void*。立即出现两个问题:函数的返回类型和参数是什么?类型安全又如何?C++ 的一个巨大优势在于其深厚的类型安全性;这一点将不得不被牺牲。

观察者模式

Gamma、Helm、Johnson 和 Vlissides (1995) 提出的 Observer 模式,并由 Patje (2002)Mariano (2003) 描述,旨在解决上述完全相同的问题。在上述情况下提供示例实现本身就很有趣。


#include <iostream>
#include <vector>

class Worker
{
    class WorkerObserver
    {
        virtual void LongOperationFinished() = 0;
    }

    void LongOperation()
    {
        // Do a long operation here
        for (size_t i = 0 ; i < m_Observers.size() ; i++) {
            m_Observers[i]->LongOperationFinished();
        }
    }

    AddObserver(WorkerObserver* o) { m_Observers.push_back(o); }

    std::vector<WorkerObserver*> m_Observers;
};

class Notifier : public Worker::WorkerObserver
{
    virtual void LongOperationFinished()
    {
        std::cout << "Worker is finished!" << std::endl;
    }
};

int main(int argc, char* argv[])
{
    Notifier notifier;
    Worker worker(notifier);

    Worker.LongOperation();

    return 0;
}

优点显而易见:Worker 完全不知道 Notifier,可以有多个对象在 Worker::LongOperation() 完成时得到通知,并且具有完全的类型安全。

但这里的封装原则仍然被破坏,因为 Notifier 需要了解 Worker。它只能监听 Worker 发送的事件。此外,当 Notifier 需要监听除 Worker 发送的事件之外的其他事件时怎么办?它必须为监听的每个对象继承一个不同的类。其他问题仍然存在:当 Worker::m_Observers 中的一个对象被删除时怎么办?必须将其从 Worker::m_Observers 中删除,以避免程序走上意外的路线。这将需要额外的费力的簿记代码。

上述所有解决方案都无法令人满意地解决最初的问题。因此,引入了另一种机制:信号/槽机制。

什么是信号/槽以及为什么使用它?

信号/槽是一种高级构造,用于将特定信号连接到事件发生时需要执行的代码,即所谓的“槽”。信号/槽编程的三个主要组成部分在这最后一句话中得到了清晰的阐述:“信号”、“槽”和“连接”。通过将信号连接到槽,可以在两者之间建立关系,以便在发出信号时调用槽。前提是槽是“可调用的”。这意味着槽可以是全局函数、成员方法或静态函数。信号和槽之间的关系是连接;如果信号或槽消失,连接也会消失,并且无需手动断开信号和槽的连接。任何信号都可以连接无限数量的槽,并且信号可以用任意数量的参数发出。它们也可以从连接到它们的槽中获得返回值。这些返回值被组合成一个的方式是通过使用 marshaller,稍后将讨论。

信号/槽在很大程度上等同于 C# 中的“委托”和 Java 中的“事件监听器”。

介绍:libsigc++

libsigc++ 库是上述信号/槽机制的 C++ 实现。该库最初是为了证明一种 C++ 表示事件的方式,用于封装基于 C 的 GTK API 为 C++ 库。第一个实现由 Tero Pulkkinen 完成,后来由 Karl Nelson 进行修订和维护。如今,它作为独立项目在其 Sourceforge 项目页面上由 Murray Cumming 和 Martin Schulze 进行维护。它仍然是 GTK 和 Gnome 库的 C++ 包装器中的关键部分,但也广泛部署在其他软件包中。

该库主要基于模板,但也有一些实现函数需要与使用它的程序进行链接。在撰写本文时(2003 年 10 月 31 日),该库的 2.0 版本即将发布,但在文本的其余部分将使用 1.2.5 版本。2.0 开发分支使用只有 GNU C++ 编译器 gcc 和 Visual Studio .Net 2003 C++ 编译器才能理解的高级模板。1.2.5 是可以使用 Visual Studio 6 编译器构建的最新版本。

libsigc++ 库在 LGPL 许可下发布。与普遍看法相反,这并不意味着它不能在非 GPL 应用程序中使用。它可以自由地用于以任何许可证形式发布的软件,并且您仍然可以访问完整的源代码,并且您可以根据需要对其进行修改。然而,一个重要的注意事项是,如果您决定修改库本身,并且这些修改不是为了您的私人使用,您必须发布这些修改的源代码。这可以通过例如将修改发送到开发邮件列表来完成,也可以通过将其放在公共网站上。有关详细信息,请参阅LGPL 的文本。

在 Windows 上构建和安装 libsigc++

该库的源代码包可以从项目的 Sourceforge 下载页面下载。其中包含一个用于使用 Visual Studio 6 构建该库的自述文件。简而言之,步骤如下:

  • 从 www.cygwin.com/setup.exe 下载 Cygwin 并安装默认(最小)设置
  • 打开 shell,转到 /cygdrive/path/to/libsigc/ 目录
  • 键入 './configure'。
  • 键入 'make'。
  • 更改注册表项以使 Visual Studio 识别 *.cc 文件为 C++ 文件。该键是 "HKEY_CURRENT_USER\Software\Microsoft\DevStudio\6.0\Build System\Components\Platforms\Win32 (x86)\Tools\32-Bit C/C++-Compiler"。其中有一个名为 'Input_Spec' 的字段。将 ';*.cc' 添加到该字符串的值中。

为 Visual Studio 6 和 Visual Studio .Net 2003 提供了项目文件。在 Visual Studio 中编译源代码将生成一个文件,无论选择 Debug 还是 Release 生成目标:libsigc++1.2-vc6.lib。建议将 Debug 生成目标目录中的文件重命名为 libsigc++1.2-vc6D.lib,然后将两个文件复制到链接器可以找到的地方。

创建静态库的另一种方法是将源文件包含到将使用它的项目中。不建议这样做,因为它会使将来的升级和重用更加困难。

还需要配置 Visual Studio 环境,使其能够找到使用该库所需的头文件。这是通过将源分发的根目录添加到包含路径列表来完成的。

使用 libsigc++:基础

一个简单的代码示例将演示该库的基本用法


#include <iostream>
#include <sigc++/sigc++.h>

class Worker
{
public:
    void LongOperation()
    {
        // Do a long operation here
        m_SignalFinished.emit();
    }

    SigC::Signal0<void> m_SignalFinished;
};

class Notifier : public SigC::Object
{
public:
    void LongOperationFinished()
    {
        std::cout << "Worker is finished!" << std::endl;
    }
};

int main(int argc, char* argv[])
{
    Notifier notifier;
    Worker worker;

    worker.m_SignalFinished.connect(SigC::slot(notifier, 
        &Notifier::LongOperationFinished));

    worker.LongOperation();

    return 0;
}

这展示了使用 libsigc++ 的几个关键点。

首先,在稍后将发出该信号的类中声明一个“信号”。此处显示的信号是最简单的信号类型;它不接受任何参数(名称中的 0)并且返回 void(模板参数)。所有 libsigc++ 类都在 SigC 命名空间中,因此除非在第一次使用 libsigc++ 类之前有 #using namespace SigC 指令,否则指定它至关重要。这取决于个人喜好。

其次,其成员函数之一将用作槽的类必须派生自 SigC::Object。由于 C++ 允许多重继承,这很少会造成问题。继承是为了实现信号与槽的自动断开连接。当槽或信号消失时,将执行自动断开连接。对于无法从基类继承的情况(例如,当连接到因许可限制而无法更改的类库的成员函数时),仍然可以使用 SigC::slot_class() 而不是 SigC::slot() 手动管理连接。

第三,信号和槽必须连接。这是整个过程中最复杂的部分。信号有一个名为 'connect' 的成员函数,必须用一个 'SigC::slot' 参数来调用它。提供了几个实用函数来创建此类对象。此处使用的函数将从一个对象和一个基于该对象的类的成员函数创建 SigC::slot。稍后将讨论创建 SigC::slot 对象的其他函数。

最后,在长时间操作完成后必须发出信号。这是通过调用信号的 emit() 成员函数来完成的。一种快捷方式是调用 () 运算符,如下所示:

m_SignalFinished();

同样,这取决于个人喜好。

现在,当 Worker::LongOperation 成员函数完成时,它将发出信号,Notif​​ier 的槽将响应调用。如果 Notif​​ier 在 Worker::LongOperation 完成之前消失,程序将正常继续运行。同样,如果其他对象(无论是 Notif​​ier 类型还是其他类型)对 m_SignalFinished 信号感兴趣,它们可以轻松地将一个槽连接到同一个信号,并且所有槽都将在响应信号发出时被调用。

带参数的信号

如果信号应该有一个或多个参数怎么办?只需要更改很少的几项,如下所示:


#include <iostream>
#include <string>
#include <sigc++/sigc++.h>

class Worker
{
public:
    void LongOperation()
    {
        // Do a long operation here
        m_SignalFinished.emit("I'm finished!");
    }

    SigC::Signal1<void, std::string> m_SignalFinished;
};

class Notifier : public SigC::Object
{
public:
    void LongOperationFinished(std::string str)
    {
        std::cout << "Worker is finished!" << std::endl;
        std::cout << "The argument is: " << str << std::endl;
    }
};

int main(int argc, char* argv[])
{
    Notifier notifier;
    Worker worker;

    worker.m_SignalFinished.connect(SigC::slot(notifier, 
        &Notifier::LongOperationFinished));

    worker.LongOperation();

    return 0;
}

信号的声明已更改,槽的签名以及信号发出的方式。

信号的声明已发生两种变化:为了表明它接受 1 个参数,Signal0 已更改为 Signal1,并且添加了一个新的模板参数来指示参数的类型。创建 2 个参数信号的模板变化可以很容易地推断出来:它将被命名为 Signal2,并且会有 3 个模板参数:第一个用于返回类型,第二个和第三个用于参数类型。默认情况下,提供了最多 5 个参数的信号模板。如果需要更多,您将需要修改头文件生成的 m4 源代码文件;这是本文未涵盖的主题。libsigc++ 源代码分发版中的 'examples/nine.h.m4' 文件提供了一个示例。

槽的签名也已调整为接受一个参数。这里,C++ 和库的类型安全性显现出来:如果忘记更改槽签名或指定了错误的参数,编译器将拒绝编译代码。这样就可以确保所有参数类型的正确性。

最后一个变化在于信号的发出的方式:参数直接传递到 emit() 函数。同样,这可以被快捷方式替换,如下所示:

m_SignalFinished("I'm finished!");

其他类型的槽

也可以将其他类型的槽连接到信号。例如,假设当操作完成时应该调用一个全局函数;代码将如下所示:


#include <iostream>
#include <string>
#include <sigc++/sigc++.h>

void notifierfunction(std::string str)
{
    std::cout << "Worker is finished!" << std::endl;
    std::cout << "The argument is: " << str << std::endl;
}

class Worker
{
public:
    void LongOperation()
    {
        // Do a long operation here
        m_SignalFinished.emit("I'm finished!");
    }

    SigC::Signal1<void, std::string> m_SignalFinished;
};

int main(int argc, char* argv[])
{
    Worker worker;

    worker.m_SignalFinished.connect(SigC::slot(notifierfunction));

    worker.LongOperation();

    return 0;
}

这很简单:将要调用的函数作为参数传递给 SigC::slot 创建函数,然后将调用该函数。

要调用静态函数,使用的语法与全局函数相同,如下所示:


#include <iostream>
#include <string>
#include <sigc++/sigc++.h>

class Worker
{
public:
    void LongOperation()
    {
        // Do a long operation here
        m_SignalFinished.emit("I'm finished!");
    }

    SigC::Signal1<void, std::string> m_SignalFinished;
};

class Notifier : public SigC::Object
{
public:
    static void LongOperationFinished(std::string str)
    {
        std::cout << "Worker is finished!" << std::endl;
        std::cout << "The argument is: " << str << std::endl;
    }
};

int main(int argc, char* argv[])
{
    Notifier notifier;
    Worker worker;

    worker.m_SignalFinished.connect(SigC::slot(
        &Notifier::LongOperationFinished));

    worker.LongOperation();

    return 0;
}

槽返回值

槽<>模板始终给出的第一个参数是槽的返回类型。到目前为止,这始终是 void。但是,也可以编写具有其他返回类型的槽。信号如何获得此值?对于只有一个槽连接的信号,这非常简单。

#include <iostream>
#include <string>
#include <sigc++/sigc++.h>

class Worker
{
public:
    void LongOperation()
    {
        // Do a long operation here
        int result = m_SignalFinished.emit();
        std::cout << result << std::endl;
    }

    SigC::Signal0<int> m_SignalFinished;
};

class Notifier : public SigC::Object
{
public:
    int LongOperationFinished()
    {
        std::cout << "Worker is finished!" << std::endl;
        return 5;
    }
};

int main(int argc, char* argv[])
{
    Notifier notifier;
    Worker worker;

    worker.m_SignalFinished.connect(SigC::slot(notifier, 
        &Notifier::LongOperationFinished));

    worker.LongOperation();

    return 0;
}

marshaller

如果有几个槽连接到信号,则返回最后一个注册的槽的结果。但如果需要更高级的功能怎么办?这正是另一个对象的功能,即所谓的“marshaller”。marshaller是一个对象,它获取每个后续槽调用的结果,并可以根据这些值进行操作。marshaller可以简单地收集结果并将它们返回到 std::vector 中,或者它可以对结果进行计算(求和、平均值、任何数学运算)。这当然是假设结果类型是数字的;它也可以连接字符串或对用户定义的类型执行其他操作。

marshaller是什么样的?说明这一点最简单的方法是检查一个示例。下面的示例将把几个槽的返回值组合成一个长字符串,每个槽也都返回一个字符串。


class Concatenator
{
public:
    typedef std::string OutType;
    typedef std::string InType;

    Concatenator() {};

    OutType value()
    {
        return m_Result;
    }

    static OutType default_value()
    {
        return "";
    }

    bool marshal(InType str)
    {
        m_Result += str;

        return false;
    }
private:
    std::string m_Result;
};

类声明顶部的两个 typedefs 对于让 libsigc++ 了解返回类型和传入参数的类型是必需的。value() 成员函数被调用以获取将返回给发出信号者的最终值。然后,marshal() 函数是在每个槽完成且结果值可用时调用的函数。在示例中,它始终返回 false;当它返回 true 时,将停止信号的发出。例如,当返回某个特定值或达到某个阈值时,可以使用此功能。

现在如何使用marshaller?它只需作为最后一个参数传递给信号的声明,如下所示:

    SigC::Signal0<std::string, Concatenator> m_SignalFinished;

现在,如果发出此信号并且有几个槽连接到它,则结果值将是一个字符串,其中所有单独的结果值都连接到一个字符串中。

其他用例

除了“notifier”和“worker”这两个抽象对象之外,还将提供一些实际示例。

一个明显的用途当然是用于 libsigc++ 最初开发的用途:将窗口系统生成的事件连接到事件的处理程序;例如,当按下按钮时,调用一个成员函数。

(请注意,本节中的所有代码都只是伪代码,无法编译)

class Application
{
public:
    void OnButton1Pressed()
    {
        std::cout << "Button pressed!" << std::endl;
    }

}

int main(int argc, char* argv[])
{
    Application app;

    Button btn1 = new Button;
    btn1.Clicked.connect(app, &Application::OnButton1Pressed());

    return 0;
}

另一个用途是让 Socket 对象在数据到达网络时通知其他对象。

class Socket
{
    void Listen()
    {
        <do connection setup here>
        while(data_received) {
            <read data from socket>
            m_DataArrived(data);
        }
    }

    SigC::Signal1<void, std::string> m_DataArrived;
}

void DataHasArrived(std::string str)
{
    std::cout << "Got data from network:" << str << std::endl;
}

int main(int argc, char* argv[])
{
    Socket sock;
    sock.Listen();

    return 0;
}

信号/槽机制的用途在日常编程中经常出现:当数据库中的记录发生变化时,用户界面可以自行更新;当发生某些错误情况时,可以向系统管理员发送通知(屏幕上或通过电子邮件)...还可以想到许多其他示例。

结论

在检查了几种实现回调机制的设计替代方案后,介绍了 libsigc++ 信号/槽库。在介绍了通用术语和实现原理之后,展示了几个日常示例。与所有针对旧问题的新型高级方法一样,需要时间来适应另一种思考它们的方式,并且与所有这些新型高级方法一样,并非所有情况都适合使用它们。然而,许多情况都适合,因此了解这种模式并理解它是如何工作的,对于程序员来说很重要,这样她就能识别出最适合它的时机。

参考文献

其他 C++ 信号/槽实现

致谢

感谢 Martin Schulze 和 Murray Cumming 对本文早期版本的评论。

© . All rights reserved.