조건부 컴파일
조건부 컴파일 (Conditional Compilation)
이번 챕터에서는 D 언어가 어떤 코드를 컴파일에 넣고, 어떤 코드를 빼놓을지 선택하는 방법을 살펴볼게요. 같은 소스 파일 하나로 여러 버전의 프로그램(build)을 만들 수 있다는 게 이 장 전체의 핵심 아이디어예요. 제품용(release) 빌드와 디버그 빌드를 나눠야 하거나, 운영체제·플랫폼별로 다른 코드를 넣어야 할 때 이 조건부 컴파일 기능이 아주 유용하게 쓰입니다.
본문
조건부 컴파일(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)는 도입되지 않아요.
ConditionalDeclaration과 ConditionalStatement는 중첩해서 쓸 수 있습니다.
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_foo와 Windows_bar는 예약되지만, foo_ARM과 bar_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을 가진 ConditionalStatement를 DebugStatement라고 합니다. DebugStatement는 **완화된 의미 검사(relaxed semantic checks)**를 받아서, pure, @nogc, nothrow, @safe 검사가 수행되지 않아요. 또한 DebugStatement는 pure, @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
StaticIfCondition은 IfStatement와 다음 점들에서 다릅니다.
- 문(statement)뿐 아니라 선언(declaration)도 조건부로 컴파일할 수 있어요.
- 조건부로 컴파일되는 문에
{ }를 써도 새로운 스코프를 도입하지 않아요. - 만족되지 않는 조건의 경우, 조건부로 컴파일되는 코드는 구문만 올바르면 됩니다. 의미(semantically)적으로 올바를 필요는 없어요.
- 컴파일 시점에 평가 가능해야 합니다.
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 AggregateForeach는 foreach처럼 시퀀스 확장을 지원하고, ForeachTuplePatterns 풀기(unpacking)도 지원합니다.
break and continue (break와 continue)
static foreach는 반복문(loop)이 아니라 코드 생성(code generation) 구조이기 때문에, 그 안에서 break와 continue로 제어 흐름을 바꿀 수 없어요. 적절한 바깥 문을 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)
- D 언어 사양 원문 — Conditional Compilation: https://dlang.org/spec/version.html
- D 언어 사양 목차: https://dlang.org/spec/spec.html
- 관련 주제:
debug조건,version조건,static if,static foreach,static assert