在C++中实现跨平台面向对象编程

10
当然,我知道最好的答案是“不要自己编写跨平台代码,其他人已经做了你需要的事情”,但我正在以业余/学习的方式进行这项工作,并非在任何付费职位上。基本上,我正在用C++编写一个较小的控制台应用程序,我想使它跨平台,处理文件、套接字和线程之类的事情。面向对象编程似乎是一个很好的处理方法,但我还没有找到一个好的模式来编写跨平台共享相同接口的类。
简单的方法是规划出一些元接口,在整个程序中使用它,并根据平台编译相同的类的不同文件,但我觉得肯定有更优雅的方法;至少,不会混淆IntelliSense等工具的方法会更好。
我看了一下wxWidgets源代码中的一些较小类,它们使用一种私有成员保存类的数据的方法,例如
class Foo
{
    public:
        Foo();

        void Bar();
    private:
        FooData data;
};

您可以根据平台选择不同的实现文件来编译它。但我认为这种方法相当笨拙。

另一种方法是编写接口,并根据平台交换从该接口继承的类。大致如下:

class Foo
{
    public:
        virtual ~Foo() {};
        virtual void Bar() = 0;
};

class Win32Foo
{
    public:
        Win32Foo();
        ~Win32Foo();
        void Bar();
};

当然,这样做会破坏实际实例化的过程,因为您不知道要创建哪个实现对象,但可以通过使用函数来解决这个问题。

Foo* CreateFoo();

根据你所运行的平台来改变函数实现的方法。我也不太喜欢这种方法,因为它仍然需要在代码中添加许多实例化方法(而且这也与创建非跨平台对象的方法不一致)。

这两种方法哪个更好?有更好的方法吗?

编辑:澄清一下,我的问题不是"如何编写跨平台的C++代码?"而是"在C++中使用类抽象掉跨平台代码的最佳方法,同时尽可能保留类型系统的好处是什么?"

8个回答

15
要实现跨平台功能(如套接字)需要依赖操作系统支持,这意味着你需要编写的代码在某些平台上根本无法编译通过。因此,你需要使用预处理指令来补充面向对象设计。
然而,既然你必须使用预处理器,那么像win32socket这样从socket基类继承的类是否有必要或者有用就值得怀疑了。面向对象通常在不同函数需要在运行时进行多态选择时非常有用。但是,跨平台功能通常更多是编译时问题。(例如,在Linux上甚至无法编译win32socket::connect函数,使用多态调用毫无意义!)因此,更好的做法可能是简单地创建一个socket类,并使用预处理指令在不同平台上实现不同的功能。

9
没必要在代码中大量使用预处理器条件编译语句,只需将特定于平台的代码放入单独的文件中,并在makefile或构建脚本中进行选择。 - Nikolai Fetissov
1
是的,预处理器指令可以非常简单,例如根据平台包含不同的头文件。 - Charles Salvia
当然可以- 我的问题更多是关于如何创建通用套接字类的不同实现的。对于那里的歧义表示道歉。 - ShZ
2
@ShZ,我理解你的意思,但我的观点是虚函数和面向对象编程总体来说是一种运行时机制,因此对于设计跨平台代码并不是非常有用或必要的。 - Charles Salvia

14

定义你的接口,它会转发到 detail 调用:

#include "detail/foo.hpp"

struct foo
{
    void some_thing(void)
    {
        detail::some_thing();
    }
}

"detail/foo.hpp"是类似于以下内容:

namespace detail
{
    void some_thing(void);
}

你需要在 detail/win32/foo.cppdetail/posix/foo.cpp 中实现此功能,并根据编译的平台选择其中一种进行编译。

常见接口只是将调用转发到特定于实现的实现中。这类似于boost的工作方式。您需要查看boost以获取全部详细信息。


1
这是个好主意,我一定会看看Boost在那方面是怎么做的。不过我很好奇,这种模式是否允许类状态在实现之间有所变化?例如,Posix和Win32文件句柄具有不同的类型 - 如果我正在编写一个文件包装器,状态结构/类将不同。 - ShZ
2
你可以创建一个file_handle类,然后将句柄塞入void*中。实现可以将其强制转换回它所需的类型。这在标准中是可靠安全的。如果您需要动态加载实现,此方法也非常有效,因为你只需要用从库中加载的some_function_pointer替换detail::whatever即可。 - GManNickG
1
+1:始终将构建系统委托处理构建时特定的事务。 - Steve Schnepp

4
你不需要使用面向对象编程来实现跨平台。跨平台更多的是一种架构和设计范式。
我建议将所有特定于平台的代码与标准代码隔离开来,如套接字GUI等。比较这些在不同平台上的差异,并编写一个通用的层或包装器。在你的代码中使用这个层。创建库函数来实现通用层的特定于平台的实现。
特定于平台的函数应该在单独的库或文件中。你的构建过程应该为特定的平台使用正确的库。标准代码不应该有任何更改。

2
理想情况下,您应该尽可能将差异集中在最低层级。因此,您的顶层代码调用Foo(),只有Foo()需要在内部关心它正在调用哪个操作系统。
附注:看看“boost”,它包含了处理跨平台网络文件系统等内容的很多东西。

1
第二种方法更好,因为它允许您创建仅存在于一个平台上的成员函数。然后,您可以在与平台无关的代码中使用抽象类,在特定于平台的代码中使用具体类。
例如:
class Foo
{
    public:
        virtual ~Foo() {};
        virtual void Bar() = 0;
};   
class Win32Foo : public Foo
{
    public:
        void Bar();
        void CloseWindow(); // Win32-specific function
};

class Abc
{
    public:
        virtual ~Abc() {};
        virtual void Xyz() = 0;
};   
class Win32Abc : public Abc
{
    public:
        void Xyz()
        {
           // do something
           // and Close the window
           Win32Foo foo;
           foo.CloseWindow();
        }
};

1

您可以使用模板特化来分离不同平台的代码。

enum platform {mac, lin, w32};

template<platform p = mac>
struct MyClass
{
 // Platform dependent stuff for whatever platform you want to build for default here...
};

template<>
struct MyClass<lin>
{
 // Platform dependent stuff for Linux...
};

template<>
struct MyClass<w32>
{
 // Platform dependent stuff for Win32...
};

int main (void)
{
 MyClass<> formac;
 MyClass<lin> forlin
 MyClass<w32> forw32;
 return 0;
}

在这里也遇到了这个“成语”:http://altdevblog.com/2011/06/27/platform-abstraction-with-cpp-templates/。 - user1596212

1

委托,就像你第一个例子中的委托或GMan的委托一样,非常有效,特别是如果平台特定部分需要许多平台特定成员(例如仅存在于一个平台上的类型的成员)。 缺点是您必须维护两个类。

如果类将更简单,没有很多平台特定成员,您可以只使用通用类声明,并在两个不同的.cc文件中编写两个实现(加上可能需要第三个.cc的平台无关方法实现)。 您可能需要在头文件中使用一些#ifdef来处理少数平台特定成员或具有平台特定类型的成员。 这种方式更像是一种hack,但可以更容易地开始。 如果情况失控,您总是可以切换到委托。


1

正如其他人所指出的那样,继承并不是解决这个问题的正确工具。在您的情况下,选择使用的具体类型是在构建时做出的决定,而经典的多态性使得该决策可以在运行时(即作为用户操作的结果)进行。


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