조건부 컴파일

조건부 컴파일 (Conditional Compilation)

이번 챕터에서는 D 언어가 어떤 코드를 컴파일에 넣고, 어떤 코드를 빼놓을지 선택하는 방법을 살펴볼게요. 같은 소스 파일 하나로 여러 버전의 프로그램(build)을 만들 수 있다는 게 이 장 전체의 핵심 아이디어예요. 제품용(release) 빌드와 디버그 빌드를 나눠야 하거나, 운영체제·플랫폼별로 다른 코드를 넣어야 할 때 이 조건부 컴파일 기능이 아주 유용하게 쓰입니다.

출처: https://dlang.org/spec/version.html

본문

조건부 컴파일(conditional compilation)은 어떤 코드를 컴파일할지, 어떤 코드를 컴파일하지 않을지 선택하는 과정이에요.

ConditionalDeclaration:
    Condition DeclarationBlock
    Condition DeclarationBlock else DeclarationBlock
    Condition : DeclDefsopt
    Condition DeclarationBlock else : DeclDefsopt

ConditionalStatement:
    Condition NoScopeNonEmptyStatement
    Condition NoScopeNonEmptyStatement else NoScopeNonEmptyStatement

Condition(조건)이 만족되면 그 뒤에 나오는 DeclarationBlock이나 Statement가 컴파일에 포함되고, 만족되지 않으면 선택적인 else 뒤의 DeclarationBlock이나 Statement가 컴파일됩니다.

컴파일되지 않는 DeclarationBlock이나 Statement라도 구문(syntactically)은 반드시 올바르야 합니다.

{ }로 감싸여 있더라도 새로운 스코프(scope)는 도입되지 않아요.

ConditionalDeclarationConditionalStatement는 중첩해서 쓸 수 있습니다.

StaticAssert를 쓰면 조건부 컴파일에서 오류가 되는 분기(branch)에 대해 컴파일 시점에 오류를 내보낼 수 있습니다.

Condition(조건)은 다음과 같은 형태를 가집니다.

Condition:
    VersionCondition
    DebugCondition
    StaticIfCondition

Version Condition (버전 조건)

VersionCondition:
    version ( Identifier )
    version ( unittest )
    version ( assert )

버전(version) 기능을 쓰면 하나의 소스 파일로 모듈의 여러 버전을 구현할 수 있어요.

Identifier가 버전 식별자(version identifier)와 일치하면 VersionCondition이 만족됩니다.

버전 식별자는 명령줄의 -version 스위치로 설정하거나, 모듈 안에서 VersionSpecification으로 설정할 수 있고, 컴파일러가 미리 정의해 둔 것도 있어요.

버전 식별자는 **자체적으로 고유한 이름 공간(namespace)**을 가집니다. 디버그 식별자나 모듈의 다른 심볼과 충돌하지 않아요. 한 모듈에서 정의된 버전 식별자는 다른 임포트된 모듈에는 아무 영향을 주지 않습니다.

int k;
version (Demo) // compile in this code block for the demo version
{
    int i;
    int k;    // error, k already defined

    i = 3;
}
x = i;      // uses the i declared above
version (X86)
{
    ... // implement custom inline assembler version
}
else
{
    ... // use default, but slow, version
}

version (unittest)는 유닛 테스트를 활성화한 상태로 컴파일할 때만(dmd의 -unittest 옵션) 만족됩니다.

Version Specification (버전 명세)

VersionSpecification:
    version = Identifier ;

버전 명세(version specification)는 특정 버전 식별자를 정의해 줍니다. 이렇게 하면 한 무리의 기능들을 하나의 큰 버전 아래에 묶기가 아주 쉬워지는데, 예를 들면 이렇게요.

version (ProfessionalEdition)
{
    version = FeatureA;
    version = FeatureB;
    version = FeatureC;
}
version (HomeEdition)
{
    version = FeatureA;
}
...
version (FeatureB)
{
    ... implement Feature B ...
}

버전 식별자는 순방향 참조(forward reference)가 불가능합니다.

version (Foo)
{
    int x;
}
version = Foo;  // error, Foo already used

VersionSpecification모듈 스코프에서만 나타날 수 있고, MixinExpression으로 생성할 수 없습니다.

debug 조건과 version 조건은 겉으로 보기엔 비슷하게 동작하지만, 용도가 아주 다릅니다. 디버그 문(debug statement)은 릴리스 버전에서 제거할 디버그 코드를 넣기 위한 것이고, 버전 문(version statement)은 이식성(portability)과 여러 릴리스 버전을 지원하기 위한 것이에요.

여기 정식 버전(full version)과 데모 버전(demo version)을 구분하는 예를 볼게요.

class Foo
{
    int a, b;

    version (full)
    {
        int extrafunctionality()
        {
            ...
            return 1;  // extra functionality is supported
        }
    }
    else // demo
    {
        int extrafunctionality()
        {
            return 0;  // extra functionality is not supported
        }
    }
}

다양한 버전 빌드는 version에 매개변수를 줘서 만들 수 있어요.

version (identifier) // add in version code if version
                         // keyword is identifier
{
    ... version code ...
}

이건 보통 명령줄에서 -version=identifier로 설정합니다.

Predefined Versions (미리 정의된 버전)

일관된 사용을 위해 여러 환경 관련 버전 식별자와 식별자 이름 공간이 미리 정의되어 있어요. 버전 식별자는 코드 안의 다른 식별자와 충돌하지 않으며, 별도의 이름 공간에 있습니다. 미리 정의된 버전 식별자는 **전역(global)**입니다. 즉 컴파일되고 임포트되는 모든 모듈에 적용됩니다.

버전 식별자 설명
Host Compiler
DigitalMars DMD (Digital Mars D)
GNU GDC (GNU D Compiler)
LDC LDC (LLVM D Compiler)
SDC SDC (Snazzy D Compiler)
Target Operating System
Windows Microsoft Windows 시스템 Win32과 Win64
Win32 Microsoft 32비트 Windows 시스템
Win64 Microsoft 64비트 Windows 시스템
linux 모든 Linux 시스템
Apple Apple 시스템 OSX, iOS, TVOS, WatchOS 그리고 VisionOS
OSX macOS
iOS iOS
TVOS tvOS
WatchOS watchOS
VisionOS visionOS
FreeBSD FreeBSD
OpenBSD OpenBSD
NetBSD NetBSD
DragonFlyBSD DragonFlyBSD
BSD 그 밖의 모든 BSD
Solaris Solaris
Posix 모든 POSIX 시스템 (Linux, FreeBSD, OS X, Solaris 등을 포함)
AIX IBM Advanced Interactive eXecutive OS
Haiku Haiku 운영체제
SkyOS SkyOS 운영체제
SysV3 System V Release 3
SysV4 System V Release 4
Hurd GNU Hurd
Android Android 플랫폼
Emscripten Emscripten 플랫폼
PlayStation PlayStation 플랫폼
PlayStation4 PlayStation 4 플랫폼
WASI WebAssembly System Interface의 모든 버전
WASIp1 WebAssembly System Interface의 Preview 1
WASIp2 WebAssembly System Interface의 Preview 2
WASIp3 WebAssembly System Interface의 Preview 3
FreeStanding 운영체제가 없는 환경 (Bare-metal 타깃 등)
Target Environment
Cygwin Cygwin 환경
MinGW MinGW 환경
CRuntime_Bionic Bionic C 런타임
CRuntime_DigitalMars DigitalMars C 런타임
CRuntime_Glibc Glibc C 런타임
CRuntime_Microsoft Microsoft C 런타임
CRuntime_Musl musl C 런타임
CRuntime_Newlib newlib C 런타임
CRuntime_UClibc uClibc C 런타임
CRuntime_WASI WASI C 런타임
CppRuntime_Clang 사용 중단(deprecated), CppRuntime_LLVM을 사용하세요
CppRuntime_DigitalMars DigitalMars C++ 런타임
CppRuntime_Gcc 사용 중단(deprecated), CppRuntime_GNU를 사용하세요
CppRuntime_LLVM LLVM libc++ C++ 런타임
CppRuntime_GNU GNU libstdc++ C++ 런타임
CppRuntime_Microsoft Microsoft C++ 런타임
CppRuntime_Sun Sun C++ 런타임
Target CPU and Instruction Set
X86 Intel과 AMD 32비트 프로세서
X86_64 Intel과 AMD 64비트 프로세서
ARM ARM 아키텍처 (32비트) (AArch32 등)
ARM_Thumb 모든 Thumb 모드의 ARM
ARM_SoftFloat ARM 소프트 부동소수점 ABI
ARM_SoftFP ARM softfp 부동소수점 ABI
ARM_HardFloat ARM hardfp 부동소수점 ABI
AArch64 Advanced RISC Machine 아키텍처 (64비트)
AsmJS asm.js 중간 프로그래밍 언어
AVR 8비트 Atmel AVR 마이크로컨트롤러
Epiphany Epiphany 아키텍처
PPC PowerPC 아키텍처, 32비트
PPC_SoftFloat PowerPC 소프트 플로트 ABI
PPC_HardFloat PowerPC 하드 플로트 ABI
PPC64 PowerPC 아키텍처, 64비트
IA64 Itanium 아키텍처 (64비트)
MIPS32 MIPS 아키텍처, 32비트
MIPS64 MIPS 아키텍처, 64비트
MIPS_O32 MIPS O32 ABI
MIPS_N32 MIPS N32 ABI
MIPS_O64 MIPS O64 ABI
MIPS_N64 MIPS N64 ABI
MIPS_EABI MIPS EABI
MIPS_SoftFloat MIPS 소프트 플로트 ABI
MIPS_HardFloat MIPS 하드 플로트 ABI
MSP430 MSP430 아키텍처
NVPTX Nvidia Parallel Thread Execution (PTX) 아키텍처, 32비트
NVPTX64 Nvidia Parallel Thread Execution (PTX) 아키텍처, 64비트
RISCV32 RISC-V 아키텍처, 32비트
RISCV64 RISC-V 아키텍처, 64비트
SPARC SPARC 아키텍처, 32비트
SPARC_V8Plus SPARC v8+ ABI
SPARC_SoftFloat SPARC 소프트 플로트 ABI
SPARC_HardFloat SPARC 하드 플로트 ABI
SPARC64 SPARC 아키텍처, 64비트
S390 System/390 아키텍처, 32비트
SystemZ System Z 아키텍처, 64비트
HPPA HP PA-RISC 아키텍처, 32비트
HPPA64 HP PA-RISC 아키텍처, 64비트
SH SuperH 아키텍처, 32비트
WebAssembly WebAssembly 가상 ISA (명령어 집합 아키텍처), 32비트
Xtensa Xtensa 아키텍처, 32비트
Alpha Alpha 아키텍처
Alpha_SoftFloat Alpha 소프트 플로트 ABI
Alpha_HardFloat Alpha 하드 플로트 ABI
Byte Order (endianess)
LittleEndian 바이트 순서, 최하위 바이트 우선
BigEndian 바이트 순서, 최상위 바이트 우선
Executable and Link Format
ELFv1 Elf 버전 1
ELFv2 Elf 버전 2
Miscellaneous
D_BetterC D를 Better C 코드(명령줄 스위치 -betterC)로 생성 중
D_Exceptions 예외 처리가 지원됨. -betterC 스위치로 컴파일하면 false로 평가됨
D_ModuleInfo ModuleInfo가 지원됨. -betterC 스위치로 컴파일하면 false로 평가됨
D_TypeInfo 런타임 타입 정보(a.k.a TypeInfo)가 지원됨. -betterC 스위치로 컴파일하면 false로 평가됨
D_Coverage 코드 커버리지 분석 계측(명령줄 스위치 -cov)을 생성 중
D_Ddoc Ddoc 문서(명령줄 스위치 -D)를 생성 중
D_InlineAsm_X86 X86용 인라인 어셈블러가 구현됨
D_InlineAsm_X86_64 X86-64용 인라인 어셈블러가 구현됨
D_LP64 포인터가 64비트(명령줄 스위치 -m64). (C의 LP64 모델과 혼동하지 마세요)
D_X32 포인터는 32비트지만 워드는 64비트(x32 ABI) (X86_64와 병렬로 정의될 수 있음)
D_HardFloat 타깃 하드웨어에 부동소수점 유닛이 있음
D_SoftFloat 타깃 하드웨어에 부동소수점 유닛이 없음
D_PIC Position Independent Code(명령줄 스위치 -fPIC)를 생성 중
D_PIE Position Independent Executable(명령줄 스위치 -fPIE)를 생성 중
D_SIMD 벡터 확장(__simd를 통해)이 지원됨
D_AVX AVX 벡터 명령어가 지원됨
D_AVX2 AVX2 벡터 명령어가 지원됨
D_Version2 D 버전 2 컴파일러임
D_NoBoundsChecks 배열 경계 검사가 비활성화됨(명령줄 스위치 -boundscheck=off)
D_ObjectiveC 타깃이 Objective-C와의 인터페이스를 지원함
D_ProfileGC GC 할당을 프로파일링 중(명령줄 스위치 -profile=gc)
D_Optimized 최적화를 활성화하고 컴파일 중(명령줄 스위치 -O)
Core 표준 런타임을 빌드할 때 정의됨
Std 표준 라이브러리를 빌드할 때 정의됨
unittest 유닛 테스트가 활성화됨(명령줄 스위치 -unittest)
assert AssertExpression에 대한 검사가 생성 중
D_PreConditions in 계약(contract)에 대한 검사가 생성 중
D_PostConditions out 계약에 대한 검사가 생성 중
D_Invariants 클래스/구조체 불변식(invariant)에 대한 검사가 생성 중
Special Cases
none 절대 정의되지 않음; 코드 구간을 그냥 비활성화하는 데 사용
all 항상 정의됨; none의 반대로 사용

다음 식별자들은 정의되긴 하지만 **사용 중단(deprecated)**된 것들이에요.

버전 식별자 설명
darwin Darwin 운영체제; 대신 OSX를 사용하세요
Thumb Thumb 모드의 ARM; 대신 ARM_Thumb를 사용하세요
S390X System/390X 아키텍처, 64비트; 대신 SystemZ를 사용하세요

의미가 맞고 새로운 구현이 나타나면 다른 것들도 추가될 거예요.

언어의 미래 확장을 위해, "D_"로 시작하는 버전 식별자 이름 공간은 D 언어 사양이나 신규 기능 준수 여부를 나타내는 식별자로 예약되어 있습니다. 또한 위에 나열한 식별자들에 어떤 문자든 붙여서 파생된 모든 식별자도 예약됩니다. 즉 예를 들어 ARM_fooWindows_bar는 예약되지만, foo_ARMbar_Windows는 예약되지 않아요.

이 목록에 있는 미리 정의된 버전 식별자는 명령줄이나 버전 문으로 설정할 수 없습니다. (이 덕분에 Windows와 linux가 동시에 설정되는 일 같은 게 막히는 거예요.)

컴파일러 벤더별 버전은 벤더의 상표 식별자를 앞에 붙이면 미리 정의할 수 있습니다. 예를 들면 이렇게요.

version (DigitalMars_funky_extension)
{
    ...
}

목적에 맞는 올바른 버전 식별자를 쓰는 게 중요해요. 예를 들어 벤더 고유 기능을 쓸 때는 벤더 식별자를, 운영체제 고유 기능을 쓸 때는 운영체제 식별자를 사용하는 식이죠.

Debug Condition (디버그 조건)

DebugCondition:
    debug
    debug ( Identifier )

프로그램을 빌드할 때 흔히 릴리스 빌드와 디버그 빌드 두 종류를 만듭니다. 디버그 빌드에는 추가 오류 검사 코드, 테스트 하니스(harness), 이쁘게 출력하는 코드 등이 포함됩니다. 디버그 문은 자기 문 본문(statement body)을 조건부로 컴파일해 줍니다. C에서 #ifdef DEBUG / #endif 쌍으로 하던 일을 D에서는 이렇게 하는 거예요.

-debug 스위치가 컴파일러에 전달되면 디버그 조건이 만족됩니다.

debug ( Identifier ) 조건은 디버그 식별자가 Identifier와 일치할 때 만족됩니다.

class Foo
{
    int a, b;
debug:
    int flag;
}

Debug Statement (디버그 문)

DebugCondition을 가진 ConditionalStatementDebugStatement라고 합니다. DebugStatement는 **완화된 의미 검사(relaxed semantic checks)**를 받아서, pure, @nogc, nothrow, @safe 검사가 수행되지 않아요. 또한 DebugStatementpure, @nogc, nothrow, @safe 속성의 추론에도 영향을 주지 않습니다.

Debug Specification (디버그 명세)

DebugSpecification:
    debug = Identifier ;

디버그 식별자는 명령줄 스위치 -debug=identifier 또는 DebugSpecification으로 설정됩니다.

디버그 명세는 그 모듈에만 영향을 줍니다. 임포트된 다른 모듈에는 영향을 미치지 않아요. 디버그 식별자는 버전 식별자나 다른 심볼과 분리된 자체 이름 공간에 있습니다.

디버그 명세를 순방향 참조하는 것은 불법입니다.

debug (foo) writeln("Foo");
debug = foo;    // error, foo used before set

DebugSpecification모듈 스코프에서만 나타날 수 있고, MixinExpression으로 생성할 수 없습니다.

다양한 디버그 빌드는 debug에 매개변수를 줘서 만들 수 있어요.

debug (identifier) { } // add in debug code if debug keyword is identifier

이것들은 보통 명령줄에서 설정합니다. 예를 들어 -debug=identifier처럼요.

Static If Condition (정적 if 조건)

StaticIfCondition:
    static if ( AssignExpression )

AssignExpression불리언 타입으로 암시적으로 변환되고, 컴파일 시점에 평가됩니다. true로 평가되면 조건이 만족되고, false로 평가되면 만족되지 않아요.

AssignExpression을 불리언 타입으로 암시적으로 변환할 수 없거나, 컴파일 시점에 평가할 수 없으면 오류입니다.

StaticIfCondition은 모듈, 클래스, 템플릿, 구조체, 유니언, 함수 스코프에서 나타날 수 있어요. 함수 스코프에서는 AssignExpression이 참조하는 심볼이 그 지점에서 표현식이 평소에 참조할 수 있는 어떤 것이든 될 수 있습니다.

const int i = 3;
int j = 4;

static if (i == 3)    // ok, at module scope
    int x;

class C
{
    const int k = 5;

    static if (i == 3) // ok
        int x;
    else
        long x;

    static if (j == 3) // error, j is not a constant
        int y;

    static if (k == 5) // ok, k is in current scope
        int z;
}
template Int(int i)
{
    static if (i == 32)
        alias Int = int;
    else static if (i == 16)
        alias Int = short;
    else
        static assert(0); // not supported
}

Int!(32) a;  // a is an int
Int!(16) b;  // b is a short
Int!(17) c;  // error, static assert trips

StaticIfConditionIfStatement와 다음 점들에서 다릅니다.

  1. 문(statement)뿐 아니라 선언(declaration)도 조건부로 컴파일할 수 있어요.
  2. 조건부로 컴파일되는 문에 { }를 써도 새로운 스코프를 도입하지 않아요.
  3. 만족되지 않는 조건의 경우, 조건부로 컴파일되는 코드는 구문만 올바르면 됩니다. 의미(semantically)적으로 올바를 필요는 없어요.
  4. 컴파일 시점에 평가 가능해야 합니다.

Static Foreach (정적 foreach)

StaticForeachDeclaration:
    StaticForeach DeclarationBlock
    StaticForeach : DeclDefsopt

StaticForeachStatement:
    StaticForeach NoScopeNonEmptyStatement

StaticForeach:
    static AggregateForeach
    static RangeForeach

static foreach는 자신의 DeclarationBlock이나 DeclDefs일련의 선언들로 확장하는데, 각 선언은 선언된 ForeachType 심볼을 참조할 수 있습니다.

  • 집합(aggregate)/범위(range) 경계는 컴파일 시점에 평가되어, 해당 코드를 컴파일 시점의 ForeachStatement/ForeachRangeStatement로 평가함으로써 컴파일 시점 엔티티들의 시퀀스로 바뀝니다.
  • static foreach의 본문은 시퀀스 요소 수만큼 그 횟수만큼 복사됩니다.
  • i번째 복사본 안에서 static foreach 요소 '변수'의 이름은 시퀀스의 i번째 항목에 묶이는데, 상수(constants)의 경우엔 enum 변수 선언으로, 심볼(symbols)의 경우엔 alias 선언으로 묶입니다. (특히 static foreach 변수는 절대 런타임 변수가 아닙니다.)
static foreach (i; [0, 1, 2, 3])
{
    pragma(msg, i);
}

static foreach는 대응하는 foreach 문이 다중 ForeachType 변수를 지원하는 경우 그걸 지원합니다. (이 경우 static foreach는 컴파일 시점 튜플의 시퀀스를 생성하고, 튜플들은 반복 중에 차례로 풀립니다.)

static foreach (i, v; ['a', 'b', 'c', 'd'])
{
    static assert(i + 'a' == v);
}

ConditionalDeclaration의 본문처럼, static foreach 본문도 새로운 스코프를 도입하지 않아요. 따라서 기존 스코프에 선언을 추가하는 데 쓸 수 있습니다.

import std.range : iota;

static foreach (i; iota(0, 3))
{
    mixin(`enum x`, i, ` = i;`);
}

pragma(msg, x0, " ", x1," ", x2); // 0 1 2

함수 안에서 매 확장마다 새 스코프가 필요하다면, 중괄호를 한 번 더 감싸면 됩니다.

void fun()
{
    static foreach (s; ["hi", "hey", "hello"])
    {{
        enum len = s.length;    // local to each iteration
        static assert(len <= 5);
    }}

    static assert(!is(typeof(len)));
}

static AggregateForeachforeach처럼 시퀀스 확장을 지원하고, ForeachTuplePatterns 풀기(unpacking)도 지원합니다.

break and continue (break와 continue)

static foreach는 반복문(loop)이 아니라 코드 생성(code generation) 구조이기 때문에, 그 안에서 breakcontinue로 제어 흐름을 바꿀 수 없어요. 적절한 바깥 문을 break/continue하는 대신, 그런 사용은 오류를 냅니다. (오해를 막기 위해서예요.)

int test(int x)
{
    int r = -1;
    switch(x)
    {
        static foreach (i; 0 .. 5)
        {
            case i:
                r = i;
                break; // error
        }
        default: break;
    }
    return r;
}

static foreach (i; 0 .. 10)
{
    static assert(test(i) == (i < 5 ? i : -1));
}

명시적인 break/continue 라벨을 쓰면 이 제약을 피할 수 있습니다. (static foreach 자체는 라벨을 붙이더라도 break/continue할 수 없다는 점에 유의하세요.)

int test(int x)
{
    int r = -1;
    Lswitch: switch(x)
    {
        static foreach (i; 0 .. 5)
        {
            case i:
                r = i;
                break Lswitch;
        }
        default: break;
    }
    return r;
}

static foreach (i; 0 .. 10)
{
    static assert(test(i) == (i < 5 ? i : -1));
}

Static Assert (정적 assert)

StaticAssert:
    static assert ( ArgumentList ) ;

첫 번째 AssignExpression컴파일 시점에 평가되어 불리언 값으로 변환됩니다. 값이 true면 static assert는 무시되고, false면 오류 진단이 발생하고 컴파일이 실패합니다.

실패할 때, 그 뒤에 나오는 AssignExpression들은 각각 문자열로 변환된 뒤 이어붙여집니다. 결과 문자열은 오류 진단과 함께 출력됩니다.

AssertExpression과 달리 StaticAssert는 만족되지 않은 조건부 안에 있지 않는 한 항상 컴파일러가 검사하고 평가합니다.

void foo()
{
    if (0)
    {
        assert(0);  // never trips
        static assert(0); // always trips
    }
    version (BAR)
    {
    }
    else
    {
        static assert(0); // trips when version BAR is not defined
    }
}

StaticAssert는 코드에서 지원하지 않는 조건부 구성(configuration)에 주의를 끌어내는 데 유용한 도구입니다.

더 알아보기 (Learn more)