代码之家  ›  专栏  ›  技术社区  ›  Mac Umer

跨平台C++代码体系结构

  •  15
  • Mac Umer  · 技术社区  · 14 年前

    我正在尝试为我正在编写的应用程序开发一个平台抽象库,并努力想出一种将我的平台独立代码与平台特定代码分离的简洁方法。

    正如我所看到的,有两种基本的方法是可行的:具有平台特定委托的独立于平台的类,或者具有平台特定派生类的独立于平台的类。这两种方法都有什么固有的优点/缺点吗?在这两种情况下,建立委托/继承关系的最佳机制是什么,以便流程对平台独立类的用户是透明的?

    我将感谢任何关于整洁的架构的建议,甚至只是人们过去所做的一些例子,以及给定方法的优缺点。

    编辑:对于那些建议qt和类似的建议,是的,我有目的地寻找“重新发明轮子”,因为我不仅关心开发应用程序,我还对滚动我自己的平台抽象库的智力挑战感兴趣。不过,谢谢你的建议!

    8 回复  |  直到 14 年前
        1
  •  11
  •   JoeG    14 年前

    我正在使用平台中立的头文件,在源文件中保留任何特定于平台的代码(使用 PIMPL idiom 必要时)。每个平台中性报头在每个平台上都有一个特定于平台的源文件,扩展名如 *.win32.cpp , *.posix.cpp . 特定于平台的代码只在相关平台上编译。

    我还使用boost库(文件系统、线程)来减少我必须维护的平台特定代码的数量。

    它是具有平台特定定义的独立于平台的类声明。

    赞成的意见: 工作得相当好,不依赖于预处理器-不 #ifdef MyPlatform ,使特定于平台的代码易于识别,允许在特定于平台的源文件中使用特定于编译器的功能,而不因包括平台头而污染全局命名空间。

    欺骗: 很难将继承与褶皱类一起使用,有时PIMPL结构需要它们自己的头,以便可以从其他平台特定的源文件引用它们。

        2
  •  4
  •   quamrana    14 年前

    另一种方法是使用独立于平台的约定,但在编译时替换特定于平台的源代码。

    也就是说如果你 想象 组件, Foo 必须是特定于平台的(如套接字或GUI元素),但具有以下公共成员:

    class Foo {
    public:
      void write(const char* str);
      void close();
    };
    

    每个模块必须使用 ,显然有 #include "Foo.h" ,但在特定于平台的make文件中 -IWin32 ,这意味着编译器查找 .\Win32 并查找特定于Windows的 Foo.h 它包含类,具有相同的接口,但可能是特定于Windows的私有成员等。

    所以没有任何文件包含 如上所述,但仅限于平台特定的文件集,这些文件集仅在由平台特定的make文件选择时使用。

        3
  •  3
  •   amit kumar    14 年前

    看一看 ACE . 它使用模板和继承具有相当好的抽象性。

        4
  •  2
  •   Steve Jessop    14 年前

    我可能会选择政策类型的东西:

    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 )继承也必须如此,而在前一种情况下,继承可以是私有的,并且/或者如果需要,可以保护接口函数。因此,适配器可以是平台必须实现的接口和应用程序代码使用的接口之间的防火墙。此外,前者中可以有多个策略(即同一个独立于平台的类所使用的多个依赖于平台的方面),但后者不能。

    与使用委托的版本或使用继承的非模板版本相比,它们的优势在于您不需要任何虚拟函数。可以说,考虑到基于策略的设计和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"; }
    }
    
        5
  •  0
  •   none    14 年前

    你可能还想看看 poco :

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

    poco architecture http://pocoproject.org/images/poco.png

        6
  •  0
  •   Phil Rykoff    14 年前

    如果您想使用一个完整的C++框架,可用于许多平台和许可的版权保护,则使用Qt。

        7
  •  0
  •   darron    14 年前

    所以…你不想简单地使用qt吗?对于使用C++的实际工作,我非常推荐它。它是一个绝对优秀的跨平台工具包。我刚刚写了几个插件让它在Kindle上工作,现在是PalmPre。Qt让一切变得轻松有趣。完全恢复活力,甚至。好吧,直到你第一次遇到qmodelindex,但是他们应该意识到他们过度设计了它,并且正在替换它;)

    不过,作为一个学术练习,这是一个有趣的问题。作为一个轮子的发明者,我已经做过几次了。:)

    简短的回答:我会和皮条客一起去。(qt源有大量示例)

    我以前使用过基类和特定于平台的派生类,但结果通常比我想象的要混乱一些。我还使用某种程度的函数指针为平台特定的位完成了部分实现,对此我甚至不太满意。

    这两次,我都有一种强烈的感觉,那就是我过度的架构设计,迷失了方向。

    我发现在不同的文件中使用具有不同平台特定位的私有实现类(PIMPL)最容易编写 调试。然而。。。不要太害怕 #定义 或者两行,如果只是几行,并且非常清楚发生了什么。我讨厌乱七八糟的 γIFIFF 逻辑,但这里或那里的一两个可以真正帮助避免代码重复。

    有了PIMPL,当您发现需要不同平台间实现的新功能时,您就不会不断地重构您的设计了。那就是龙。

    在实现级别,隐藏在应用程序中…一些特定于平台的派生类也没有问题。如果两个平台实现定义得相当好并且几乎不共享数据成员,那么它们将是一个很好的候选者。想做就做 之后 意识到这一点,之前不是出于这样的想法,即一切都需要适合您选择的模式。

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

    然而,作为一个总的指导方针…从PIMPL开始。

        8
  •  0
  •   OneOfOne    14 年前

    还有大男孩,比如 Qt4 (完整框架+图形用户界面) GTK+ (仅限图形用户界面),以及 Boost (仅框架,没有GUI),所有3个支持大多数平台,GTK+是C,QT4/Boost是C++,并且大部分是基于模板的。