【问题标题】:What's the best way to build variants of the same C/C++ application构建相同 C/C++ 应用程序变体的最佳方法是什么
【发布时间】:2008-11-04 13:08:29
【问题描述】:

我有三个密切相关的应用程序,它们是从相同的源代码构建的 - 比如说 APP_A、APP_B 和 APP_C。 APP_C 是 APP_B 的超集,而 APP_B 又是 APP_A 的超集。

到目前为止,我一直在使用预处理器定义来指定正在构建的应用程序,它的工作原理是这样的。

// File: app_defines.h
#define APP_A 0
#define APP_B 1
#define APP_C 2

然后指定(例如)我的 IDE 构建选项

#define APPLICATION APP_B

...在源代码中,我会有类似的东西

#include "app_defines.h"

#if APPLICATION >= APP_B
// extra features for APPB and APP_C
#endif

但是,今天早上我开枪打死了自己,只是从一个文件中省略了 #include "app_defines.h" 行,这浪费了很多时间。一切编译正常,但应用程序在启动时因 AV 崩溃。

我想知道有什么更好的处理方法。以前,这通常是我认为可以使用#define 的少数几次之一(无论如何,在 C++ 中),但我仍然搞砸了,编译器没有保护我。

【问题讨论】:

    标签: c++ c-preprocessor configuration-management software-product-lines


    【解决方案1】:

    您不必总是在共享公共代码库的应用程序中强制继承关系。真的。

    有一个古老的 UNIX 技巧,您可以根据 argv[0](即应用程序名称)来定制应用程序的行为。如果我没记错的话(自从我看过它已经 20 年了),rsh 和 rlogin 是/是同一个命令。您只需根据 argv[0] 的值进行运行时配置。

    如果您想坚持构建配置,这是通常使用的模式。您的构建系统/makefile 在命令上定义了一个符号,例如,APP_CONFIG 是一个非零值,然后您就有一个带有配置螺母和螺栓的通用包含文件。

    #define APP_A 1
    #define APP_B 2
    
    #ifndef APP_CONFIG
    #error "APP_CONFIG needs to be set
    #endif
    
    #if APP_CONFIG == APP_A
    #define APP_CONFIG_DEFINED
    // other defines
    #endif
    
    #if APP_CONFIG == APP_B
    #define APP_CONFIG_DEFINED
    // other defines
    #endif
    
    #ifndef APP_CONFIG_DEFINED
    #error "Undefined configuration"
    #endif
    

    这种模式强制配置是命令行定义的并且是有效的。

    【讨论】:

    • 这有点好,但不会发现我的问题。我定义了 APP_CONFIG,但忘记包含上面显示的“通用包含文件”。
    【解决方案2】:

    您尝试做的似乎与“产品线”非常相似。 Carnigie Melon University 有一个关于该模式的优秀页面:http://www.sei.cmu.edu/productlines/

    这基本上是一种构建具有不同功能的软件的不同版本的方法。如果您想像 Quicken Home/Pro/Business 之类的东西,那么您就走上了正轨。

    虽然这可能不是您所尝试的,但这些技术应该会有所帮助。

    【讨论】:

    • > Quicken Home/Pro/Business - 就是这样。
    【解决方案3】:

    在我看来,您可能会考虑将您的代码模块化为单独编译的元素,从选择的公共模块和特定于变体的顶级(主)模块中构建变体。

    然后控制哪些部分进入构建,哪些头文件用于编译顶层,哪些 .obj 文件包含在链接器阶段。

    一开始您可能会觉得这有点困难。从长远来看,您应该有一个更可靠和可验证的构建和维护过程。您还应该能够进行更好的测试,而不必担心所有 #if 变化。

    我希望您的应用程序还不是特别大,并且解开其功能的模块化不必处理一大堆泥巴。

    在某些时候,您可能需要运行时检查以验证构建使用的组件是否与您想要的应用程序配置一致,但这可以稍后解决。您还可以实现一些编译时一致性检查,但您将通过头文件和进入特定组合的从属模块的入口点签名来获得大部分检查。

    无论您是使用 C++ 类还是在 C/C++ 通用语言级别进行操作,这都是一样的游戏。

    【讨论】:

      【解决方案4】:

      如果您使用 C++,您的 A、B 和 C 应用程序不应该继承自一个共同的祖先吗?那将是解决问题的OO方式。

      【讨论】:

      • B 和 C 的附加功能在系统中非常“深入”,因此这不是一个微不足道的更改。我认为你是对的,OO 方法可能会有所帮助(事实上,当我把我的干净的脚吹掉时,我正在重构它)
      【解决方案5】:

      问题在于,使用带有未定义名称的 #if 指令就像定义为 0 一样。这可以通过始终先执行 #ifdef 来避免,但这既麻烦又容易出错。

      更好的方法是使用命名空间和命名空间别名。

      例如

      namespace AppA {
           // application A specific
      }
      
      namespace AppB {
          // application B specific
      }
      

      并使用你的 app_defines.h 来做命名空间别名

      #if compiler_option_for_appA
           namespace Application = AppA;
      #elif compiler_option_for_appB
           namespace Application = AppB;
      #endif
      

      或者,如果组合更复杂,命名空间嵌套

      namespace Application
      {
        #if compiler_option_for_appA
           using namespace AppA;
        #elif compiler_option_for_appB
           using namespace AppB;
        #endif
      }
      

      或以上任意组合。

      优点是当您忘记标头时,您会从编译器 i.s.o 中得到未知的命名空间错误。静默失败,因为 APPLICATION 默认为 0。

      话虽如此,我也遇到过类似的情况,我选择将所有内容重构为许多库,其中绝大多数是共享代码,并让版本控制系统处理不同应用程序 i.s.o 中的内容。依赖于代码中的定义等。

      在我看来,它的效果要好一些,但我知道这恰好是特定于应用程序的,YMMV。

      【讨论】:

        【解决方案6】:

        做这样的事情:

        
        CommonApp   +-----   AppExtender                        + = containment
                              ^    ^    ^
                              |    |    |                       ^ = ineritance
                            AppA  AppB  AppC                    |
        

        将您的通用代码放在 CommonApp 类中,并在关键位置调用接口“AppExtender”。例如,AppExtender 接口将具有 afterStartup、afterConfigurationRead、beforeExit、getWindowTitle 等函数...

        然后在每个应用程序的 main 中,创建正确的扩展程序并将其传递给 CommonApp:

        
            --- main_a.cpp
        
            CommonApp application;
            AppA appA;
            application.setExtender(&appA);
            application.run();
        
            --- main_a.cpp
        
            CommonApp application;
            AppB appB;
            application.setExtender(&appB);
            application.run();
        

        【讨论】:

          【解决方案7】:

          但是,我今天早上开枪打死了自己,只是从一个文件中省略了 #include "app_defines.h" 行,这浪费了很多时间。一切编译正常,但应用程序在启动时因 AV 崩溃。

          这个问题有一个简单的解决方法,打开警告,这样如果 APP_B 没有定义,那么你的项目就不能编译(或者至少产生足够的警告,让你知道有问题)。

          【讨论】:

          • 如果我的编译器 (C++Builder) 有该警告作为选项,那将起作用。它没有...
          【解决方案8】:

          您可能希望了解支持产品线开发并以结构化方式促进显式变体管理的工具。

          其中一个工具是来自pure-systems 的 pure::variants,它能够通过特征模型进行可变性管理,并跟踪在源代码中实现某个特征的各个位置。

          您可以从特征模型中选择特定的特征子集,检查特征之间的约束,并创建产品线的具体变体,即创建一组特定的源代码文件和定义。

          【讨论】:

            【解决方案9】:

            为了解决不知道何时定义预处理器定义的特定技术问题,有一个简单但有效的技巧。

            而不是-

            #define APP_A 0
            #define APP_B 1
            #define APP_C 2
            

            使用-

            #define APP_A() 0
            #define APP_B() 1
            #define APP_C() 2
            

            并且在查询版本的地方使用-

            #if APPLICATION >= APP_B()
            // extra features for APPB and APP_C
            #endif
            

            (可能也以同样的精神用 APPLICATION 做一些事情)。

            尝试使用未定义的预处理器 function 会在大多数编译器中产生警告或错误(而未定义的预处理器 define 只会默默地计算为 0)。如果不包含标题,您会立即注意到 - 特别是如果您“将警告视为错误”。

            【讨论】:

              【解决方案10】:

              查看Alexandrescu's Modern C++ Design。他介绍了使用模板的基于策略的开发。基本上,这种方法是策略模式的扩展,不同之处在于所有选择都是在编译时做出的。我认为 Alexandrescu 的方法类似于使用 PIMPL 习语,但使用模板实现。

              您将使用公共头文件中的预处理标志来选择您想要编译的实现,并将其 typedef 为代码库中其他地方的所有模板实例化中使用的类型。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2012-11-29
                • 2013-06-27
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多