跨平台C++代码架构

16

我正在开发一个应用程序的平台抽象库,但是很难想出一种将平台无关代码与平台相关代码分离的简洁方法。

我认为有两种基本方法可行:带平台特定委托的平台无关类,或者具有平台特定派生类的平台无关类。这两种方法各有优缺点吗?在任何情况下,什么是最佳机制来建立委托/继承关系,以便于平台无关类的用户透明地进行操作?

如果您能提供一个简洁的架构方案,甚至只提供过去人们所采用方法和相应方法的优缺点的例子,我会非常感激。

编辑:针对那些建议使用Qt及类似框架的人,是的,我故意要“重造轮子”,因为我不仅关注于开发应用程序,而且也对开发自己的平台抽象库的理论挑战感兴趣。感谢建议!

8个回答

12
我正在使用平台中立的头文件,将任何平台特定的代码保留在源文件中(必要时使用PIMPL惯用语)。每个平台中立的头文件对应一个平台特定的源文件,其扩展名为*.win32.cpp*.posix.cpp等。只有在相关平台上编译平台特定的源文件。
我还使用boost库(filesystem、threads)来减少我需要维护的平台特定代码量。
这是平台无关的类声明和平台特定的定义。
优点:工作效果相当好,不依赖预处理器-没有#ifdef MyPlatform,使平台特定的代码易于识别,在平台特定的源文件中允许使用编译器特定的功能,通过不包含平台头文件来避免全局命名空间的污染。
缺点:使用pimpled类进行继承较为困难,有时需要PIMPL结构体拥有自己的头文件,以便从其他平台特定的源文件中引用。

1
为了进一步阅读,可以加入一些搜索词:我认为这也被称为“桥接模式”或“实现模式”。 - foraidt
有趣,这正是我一直在做的。我有一个问题,如果你有两个平台,在这两个平台上的某个类/文件中90%的代码相同,那该怎么办? - matt
2
@matt:你可以使用一个通用的源文件;foo.h(平台无关),foo.cpp(包含公共代码),foo.win32.cpp,foo.posix.cpp。根据foo.cpp中包含的内容,它可以单独编译然后链接,或者只需被#include到foo.win32.cpp和foo.posix.cpp中。 - JoeG
如果是一个大类,我会有3个实现文件。例如... window.cpp、window.win32.cpp和window.posix.cpp。如果是一个较小的类(或者只有一些小变化),可以使用#ifdefs。但是,如果#ifdefs开始累积并且模糊了您的设计,请准备好进行一些重构。 - darron
听起来这可能是正确的方法。我会进行一些调试并回报...感谢建议Joe! - Mac
显示剩余3条评论

4
另一种方法是拥有平台无关的约定,但在编译时替换平台特定的源代码。也就是说,如果您想象一个组件Foo,它必须是平台特定的(如套接字或GUI元素),但具有这些公共成员:
class Foo {
public:
  void write(const char* str);
  void close();
};

每个需要使用Foo的模块,显然都有#include "Foo.h",但在特定于平台的makefile中,你可能会有-IWin32,这意味着编译器会查找.\Win32并找到一个Windows特定的Foo.h,其中包含该类,具有相同的接口,但可能具有Windows特定的私有成员等。
因此,从未存在包含如上所述的Foo的文件,而只有一组特定于平台的文件,只有在由特定于平台的makefile选择时才会使用。

3

看看ACE。它使用模板和继承有相当不错的抽象。


1
链接失效了,有镜像吗?:http://www.voidcn.com/article/p-mlwknity-kq.html - Jonathan
链接已经失效。镜像链接也已经失效。这里是另一个链接(很可能也会在不久的将来失效):http://www.dre.vanderbilt.edu/~schmidt/ACE-overview.html - Eljay

2

我可能会去做一些政策方面的事情:

template<typename Platform>
struct PlatDetails : private Platform {
    std::string getDetails() const {
        return std::string("MyAbstraction v1.0; ") + getName();
    }
};

// For any serious compatibility functions, these would
// of course have to be in different headers, and the implementations
// would call some platform-specific functions to get precise
// version numbers. Using PImpl would be a smart idea for these 
// classes if they need any platform-specific members, since as 
// Joe Gauterin says, you want to avoid your application code indirectly
// including POSIX or Windows system headers, containing useless definitions.
struct Windows {
    std::string getName() const { return "Windows"; }
};

struct Linux {
    std::string getName() const { return "Linux"; }
};

#ifdef WIN32
    typedef PlatDetails<Windows> PlatformDetails;
#else
    typedef PlatDetails<Linux> PlatformDetails;
#endif

int main() {
    std::cout << PlatformDetails().getName() << "\n";
}

在这种情况下,使用CRTP进行常规的模拟动态绑定与此相比并没有太大区别。因此,通用的东西是基类,而具体的东西则是派生类:

template<typename Platform>
struct PlatDetails {
    std::string getDetails() const {
        return std::string("MyAbstraction v1.0; ") + 
            static_cast<Platform*>(this)->getName();
    }
};

struct Windows : PlatDetails<Windows> {
    std::string getName() const { return "Windows"; }
};

struct Linux : PlatDetails<Linux> {
    std::string getName() const { return "Linux"; }
};

#ifdef WIN32
    typedef Windows PlatformDetails;
#else
    typedef Linux PlatformDetails;
#endif

int main() {
    std::cout << PlatformDetails().getName() << "\n";
}

基本上在较新的版本中,getName必须是公共的(尽管我认为可以使用“friend”),继承也必须是公共的。而在旧版本中,继承可以是私有的和/或接口函数可以是受保护的(如果需要的话)。因此,适配器可以成为平台必须实现的接口与应用程序代码使用的接口之间的防火墙。此外,在旧版本中可以拥有多个策略(即同一平台无关类使用的多个平台相关facet),但对于新版本则不能。
与使用委托或非模板继承的版本相比,任何一种的优点都是你不需要任何虚函数。尽管最初接触策略设计和CRTP可能会让人感到害怕,但这些优点在实践中是具有可行性的。
但实际上,我同意quamrana的看法,通常您可以在不同的平台上拥有相同事物的不同实现。
// Or just set the include path with -I or whatever
#ifdef WIN32
    #include "windows/platform.h"
#else
    #include "linux/platform.h"
#endif

struct PlatformDetails {
    std::string getDetails() const {
        return std::string("MyAbstraction v1.0; ") + 
            porting::getName();
    }
};

// windows/platform.h
namespace porting {
    std::string getName() { return "Windows"; }
}

// linux/platform.h
namespace porting {
    std::string getName() { return "Linux"; }
}

哈哈,只要Linux和Windows是您唯一的两个平台就可以了 :) 我知道这只是一个简单的例子,但是当我们决定需要另一个平台时,使用“#ifdef platform else”会带来很多麻烦。因此,如果使用ifdefs,我建议您不要假设您不会添加任何其他平台,而是使用#if defined plat1 #elif defined plat2 #else #error #endif。 - matt
当然。无论兼容性层的每个组件是否有一个ifdef,还是只有一个设置所有内容的大型ifdef,它都应该积极地识别每个平台,而不像我一样懒惰地处理最后一个平台。使用依赖于平台的包含路径的优点之一是不需要进行任何ifdeffery。 - Steve Jessop

0

您可能还想看看poco

POCO C++库(POCO代表可移植组件)是开源的C++类库,简化和加速了基于网络的便携式应用程序在C++中的开发。这些库与C++标准库完美集成,并填补了许多功能空缺。它们的模块化和高效的设计和实现使得POCO C++库非常适合嵌入式开发,这是C++编程语言因其适用于低级别(设备I/O、中断处理程序等)和高级面向对象开发而变得越来越受欢迎的领域。当然,POCO C++库也准备好迎接企业级挑战。

poco architecture
(来源:pocoproject.org)


0
如果你想使用一个可用于多个平台且具有宽松的Copyleft许可证的完整C++框架,那么请使用Qt。

0

所以...你不想简单地使用Qt吗?对于真正使用C++进行工作,我非常强烈推荐它。它是一个绝对优秀的跨平台工具包。我刚刚写了一些插件让它在Kindle和Palm Pre上运行。Qt使一切变得容易和有趣。甚至可以说是令人振奋的。好吧,直到你第一次遇到QModelIndex,但他们据说已经意识到过度设计了它并且正在替换它;)

然而,作为学术练习,这是一个有趣的问题。作为一个重复发明轮子的人,我自己也做过几次。:)

简短的答案:我会选择PIMPL。(Qt源代码有很多例子)

我过去使用过基类和特定于平台的派生类,但通常结果比我想象中更混乱。我还使用了一些函数指针来实现平台特定的部分,但我对此感到更不满意。

两次我都有一种非常强烈的感觉,即我正在过度设计并且迷失了方向。

我发现使用不同文件中的私有实现类(PIMPL)来处理不同平台特定位最容易编写和调试。但是......如果只有几行代码并且非常清楚正在发生什么,那么不要过于害怕使用#ifdef。我讨厌杂乱或嵌套的#ifdef逻辑,但是偶尔使用一两个可以真正帮助避免代码重复。

使用PIMPL时,您不需要在发现需要在平台之间使用不同实现的新位时不断重构设计。这样可能会导致问题。

在实现级别上,从应用程序中隐藏......也没有什么错处使用一些平台特定的派生类。如果两个平台实现相当明确并且几乎没有共享数据成员,则它们是一个很好的候选对象。只需在意识到这一点后再进行操作,而不是出于某种想法而提前选择所有内容都需要符合您选择的模式。

如果有什么的话,我对今天编码的最大抱怨就是人们似乎很容易迷失在理想主义中。PIMPL是一种模式,具有平台特定的派生类是另一种模式。使用函数指针是一种模式。没有什么可以说它们是互斥的。

然而,作为一般准则......从PIMPL开始。


0

还有一些大型框架,例如Qt4(完整的框架+GUI)、GTK+(仅GUI)和Boost(仅框架,没有GUI),这三个都支持大多数平台,GTK+是C语言,Qt4/Boost是C++语言,大部分基于模板。


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