• 首页
  • 加入
  • RSS
  • 使用 if constexpr 实现条件编译

    Thursday, March 10, 2022
    deepin 社区官方博客 #deepin-community:deepin.org


    在项目开发中,我们通常会使用条件编译对代码进行裁剪,选择性地排除不需要的代码,比如在某个平台下完全不支持某个功能,那么这个功能就不应该被编译。

    一般我们使用宏来判断代码,选择性的挑选需要编译的部分,并在构建系统中开启这样的条件。

    #ifdef XXXXXXXXXX
        std::cout << "hello world!" << std::endl;
    #else
        std::cout << "good bye!" << std::endl;
    #endif
    

    在 C 语言的项目中,这样的行为是很正常的,甚至在 C++ 项目中,我们也会选择使用宏进行条件判断。

    但是如果定义的宏多了,则很容易导致代码碎片化,并且无法直观地看到工作流程。

    例如这样的代码:

    #define KWIN_VERSION_CHECK(major, minor, patch, build) ((major<<24)|(minor<<16)|(patch<<8)|build)
    #ifdef KWIN_VERSION_STR
    #define KWIN_VERSION KWIN_VERSION_CHECK(KWIN_VERSION_MAJ, KWIN_VERSION_MIN, KWIN_VERSION_PAT, KWIN_VERSION_BUI)
    #endif
    
    #if defined(KWIN_VERSION) && KWIN_VERSION < KWIN_VERSION_CHECK(5, 21, 3, 0)
    typedef int TimeArgType;
    #else
    typedef std::chrono::milliseconds TimeArgType;
    #endif
    
    #if defined(Q_OS_LINUX) && !defined(QT_NO_DYNAMIC_LIBRARY) && !defined(QT_NO_LIBRARY)
    QT_BEGIN_NAMESPACE
    QFunctionPointer qt_linux_find_symbol_sys(const char *symbol);
    QT_END_NAMESPACE
    QFunctionPointer KWinUtils::resolve(const char *symbol)
    {
        return QT_PREPEND_NAMESPACE(qt_linux_find_symbol_sys)(symbol);
    #else
    QFunctionPointer KWinUtils::resolve(const char *symbol)
    {
        static QString lib_name = "kwin.so." + qApp->applicationVersion();
    
        return QLibrary::resolve(lib_name, symbol);
    #endif
    }
    

    从上面的例子中可以看到,代码中一旦出现大量重复的判断条件,代码非常不直观,而且被宏分割成了很多部分。

    在一次偶然的机会,我看到了一篇介绍 C++ 17 中的 if constexpr 的用法,可以在编译期进行一些计算,虽然我很早就知道了 constexpr 的用法,但是大家举的例子基本上都是数值计算,让编译器在编译期间将数值进行计算,从而减轻运行时的消耗,我也从来想到其他用法,所以一直没有在项目中使用到。

    constexpr 的作用并不是编译期计算数值,而是编译期进行的代码分析,如果代码较小且非常直观,比如大家经常举的例子,在编译期间计算斐波那契数列,这种例子即使不使用 constexpr 显式要求,编译器也会帮助我们开启优化,直接给出结果。

    但是如果代码非常复杂,编译器就不一定会为我们做这样的优化,就需要我们手动标记可以计算的位置,要求编译器在编译期间进行求值和优化。

    我设想的是,使用 cmake 在构建时,先生成一份文件,将开关的值记录下来,在需要进行判断的地方,就可以直接使用 if constexpr 进行条件判断,在编译期间,编译器会发现有一个分支确定不会被执行(相当于 if(false) {}),那么这个分支就不会进行编译,直接剔除。

    CMakeLists.txt 中需要做一些工作,将编译参数加入构建系统。

    option (ENABLE_MODULE "Enable Module" ON)
    
    if(ENABLE_MODULE)
        set(ENABLE_MODULE "1")
    else()
        set(ENABLE_MODULE "0")
    endif(ENABLE_MODULE)
    
    configure_file (
      "${CMAKE_CURRENT_SOURCE_DIR}/options/options.h.in"
      "${CMAKE_CURRENT_BINARY_DIR}/options/options.h"
    )
    

    在 options/options.h.in 文件里,按照 cmake 的要求将变量导入进文件中,进行内容替换。

    #pragma once
    
    #cmakedefine01 ENABLE_MODULE
    

    这里我仍然使用的是宏定义,也可以直接写成如下形式:

    option (ENABLE_MODULE "Enable Module" ON)
    
    if(ENABLE_MODULE)
        set(ENABLE_MODULE "true")
    else()
        set(ENABLE_MODULE "false")
    endif(ENABLE_MODULE)
    
    configure_file (
      "${CMAKE_CURRENT_SOURCE_DIR}/options/options.h.in"
      "${CMAKE_CURRENT_BINARY_DIR}/options/options.h"
    )
    
    #pragma once
    
    const bool ENABLE_MODULE{@ENABLE_MODULE@};
    

    在 main.cpp 中写一段测试代码:

    #include "options/options.h"
    
    #include <iostream>
    
    int main() {
        if constexpr (ENABLE_MODULE) {
            std::cout << "Now Enable Module" << std::endl;
        }
    
        if constexpr (!ENABLE_MODULE) {
            std::cout << "Now Disable Module" << std::endl;
        }
    
        return 0;
    }
    

    执行结果是符合预期的。

    # lxz @ lxz-MacBook-Pro in ~/Develop/constexpr-demo/build on git:master o [13:28:39]
    $ cmake ../ -G Ninja -DENABLE_MODULE=ON
    -- The CXX compiler identification is AppleClang 13.0.0.13000029
    -- Detecting CXX compiler ABI info
    -- Detecting CXX compiler ABI info - done
    -- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ - skipped
    -- Detecting CXX compile features
    -- Detecting CXX compile features - done
    -- Configuring done
    -- Generating done
    -- Build files have been written to: /Users/lxz/Develop/constexpr-demo/build
    
    # lxz @ lxz-MacBook-Pro in ~/Develop/constexpr-demo/build on git:master o [13:28:45]
    $ ninja
    [2/2] Linking CXX executable src/constexpr
    
    # lxz @ lxz-MacBook-Pro in ~/Develop/constexpr-demo/build on git:master o [13:28:48]
    $ ./src/constexpr
    Now Enable Module
    
    # lxz @ lxz-MacBook-Pro in ~/Develop/constexpr-demo/build on git:master o [13:28:52]
    $ cmake ../ -G Ninja -DENABLE_MODULE=OFF
    -- Configuring done
    -- Generating done
    -- Build files have been written to: /Users/lxz/Develop/constexpr-demo/build
    
    # lxz @ lxz-MacBook-Pro in ~/Develop/constexpr-demo/build on git:master o [13:28:58]
    $ ninja
    [2/2] Linking CXX executable src/constexpr
    
    # lxz @ lxz-MacBook-Pro in ~/Develop/constexpr-demo/build on git:master o [13:29:00]
    $ ./src/constexpr
    Now Disable Module
    

    虽然结果是符合的,但是我们其实不确定是否真的在编译期间就完成了代码剔除,所以使用命令进行汇编,查看汇编中是否包含了判断指令和两段输出的字符串。

    clang -S main.cpp -o main.s -I./
    

    main.s

    	.text
    	.file	"main.cpp"
    	.globl	main                            // -- Begin function main
    	.p2align	2
    	.type	main,@function
    main:                                   // @main
    	.cfi_startproc
    // %bb.0:
    	stp	x29, x30, [sp, #-32]!           // 16-byte Folded Spill
    	str	x19, [sp, #16]                  // 8-byte Folded Spill
    	mov	x29, sp
    	.cfi_def_cfa w29, 32
    	.cfi_offset w19, -16
    	.cfi_offset w30, -24
    	.cfi_offset w29, -32
    	adrp	x19, :got:_ZSt4cout
    	ldr	x19, [x19, :got_lo12:_ZSt4cout]
    	adrp	x1, .L.str
    	add	x1, x1, :lo12:.L.str
    	mov	w2, #18
    	mov	x0, x19
    	bl	_ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l
    	ldr	x8, [x19]
    	ldur	x8, [x8, #-24]
    	add	x8, x8, x19
    	ldr	x19, [x8, #240]
    	cbz	x19, .LBB0_5
    // %bb.1:
    	ldrb	w8, [x19, #56]
    	cbz	w8, .LBB0_3
    // %bb.2:
    	ldrb	w1, [x19, #67]
    	b	.LBB0_4
    .LBB0_3:
    	mov	x0, x19
    	bl	_ZNKSt5ctypeIcE13_M_widen_initEv
    	ldr	x8, [x19]
    	mov	w1, #10
    	mov	x0, x19
    	ldr	x8, [x8, #48]
    	blr	x8
    	mov	w1, w0
    .LBB0_4:
    	adrp	x0, :got:_ZSt4cout
    	ldr	x0, [x0, :got_lo12:_ZSt4cout]
    	bl	_ZNSo3putEc
    	bl	_ZNSo5flushEv
    	ldr	x19, [sp, #16]                  // 8-byte Folded Reload
    	mov	w0, wzr
    	ldp	x29, x30, [sp], #32             // 16-byte Folded Reload
    	ret
    .LBB0_5:
    	bl	_ZSt16__throw_bad_castv
    .Lfunc_end0:
    	.size	main, .Lfunc_end0-main
    	.cfi_endproc
                                            // -- End function
    	.section	.text.startup,"ax",@progbits
    	.p2align	2                               // -- Begin function _GLOBAL__sub_I_main.cpp
    	.type	_GLOBAL__sub_I_main.cpp,@function
    _GLOBAL__sub_I_main.cpp:                // @_GLOBAL__sub_I_main.cpp
    	.cfi_startproc
    // %bb.0:
    	stp	x29, x30, [sp, #-32]!           // 16-byte Folded Spill
    	str	x19, [sp, #16]                  // 8-byte Folded Spill
    	mov	x29, sp
    	.cfi_def_cfa w29, 32
    	.cfi_offset w19, -16
    	.cfi_offset w30, -24
    	.cfi_offset w29, -32
    	adrp	x19, _ZStL8__ioinit
    	add	x19, x19, :lo12:_ZStL8__ioinit
    	mov	x0, x19
    	bl	_ZNSt8ios_base4InitC1Ev
    	adrp	x0, :got:_ZNSt8ios_base4InitD1Ev
    	ldr	x0, [x0, :got_lo12:_ZNSt8ios_base4InitD1Ev]
    	mov	x1, x19
    	ldr	x19, [sp, #16]                  // 8-byte Folded Reload
    	adrp	x2, __dso_handle
    	add	x2, x2, :lo12:__dso_handle
    	ldp	x29, x30, [sp], #32             // 16-byte Folded Reload
    	b	__cxa_atexit
    .Lfunc_end1:
    	.size	_GLOBAL__sub_I_main.cpp, .Lfunc_end1-_GLOBAL__sub_I_main.cpp
    	.cfi_endproc
                                            // -- End function
    	.type	_ZStL8__ioinit,@object          // @_ZStL8__ioinit
    	.local	_ZStL8__ioinit
    	.comm	_ZStL8__ioinit,1,1
    	.hidden	__dso_handle
    	.type	.L.str,@object                  // @.str
    	.section	.rodata.str1.1,"aMS",@progbits,1
    .L.str:
    	.asciz	"Now Disable Module" // 关键在这里
    	.size	.L.str, 19
    
    	.section	.init_array,"aw",@init_array
    	.p2align	3
    	.xword	_GLOBAL__sub_I_main.cpp
    	.ident	"clang version 13.0.1"
    	.section	".note.GNU-stack","",@progbits
    	.addrsig
    	.addrsig_sym _GLOBAL__sub_I_main.cpp
    	.addrsig_sym _ZStL8__ioinit
    	.addrsig_sym __dso_handle
    	.addrsig_sym _ZSt4cout
    

    查看整个 main.s 汇编,发现只在 .L.str 段中有预期的文本字符串,可以得出结论,代码是在编译期间完成了剔除,符合我们的要求。

    原文链接:https://blog.justforlxz.com/2022/03/10/if-constexpr/