함수

함수 (Functions)

D 언어 공식 사양(D Language Reference)의 Functions 챕터를 한국어로 옮긴 문서예요. 함수 선언부터 계약(Contract), 순수 함수, 오버로딩, 함수 파라미터, 가변 인자, 중첩 함수, 함수 포인터·델리게이트, CTFE, 안전성(Safety), UFCS까지, 함수와 관련된 D 언어의 거의 모든 규칙을 다룹니다. 코드 블록은 원문 그대로 보존했으니, 문법 규칙과 함께 직접 실행해 보며 읽는 걸 추천할게요.

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

본문

함수 선언 (Function Declarations)

FuncDeclaration:
    StorageClassesopt BasicType FuncDeclarator FunctionBody
    StorageClassesopt BasicType FuncDeclarator MissingFunctionBody
    AutoFuncDeclaration

AutoFuncDeclaration:
    StorageClasses Identifier FuncDeclaratorSuffix FunctionBody

FuncDeclarator:
    TypeSuffixesopt Identifier FuncDeclaratorSuffix

FuncDeclaratorSuffix:
    Parameters MemberFunctionAttributesopt
    TemplateParameters Parameters MemberFunctionAttributesopt Constraintopt
  • 함수 본문(FunctionBody)을 가진 FuncDeclaration을 **함수 정의(function definition)**라고 불러요.
  • 함수 본문이 없는 FuncDeclaration을 **함수 프로토타입(function prototype)**이라고 불러요.
  • TemplateParameters를 가진 FuncDeclaration은 **함수 템플릿(function template)**을 정의해요.

함수 파라미터 (Function Parameters)

Parameters:
    ( ParameterListopt )

ParameterList:
    Parameter
    Parameter , ParameterListopt
    VariadicArgumentsAttributesopt ...

Parameter:
    ParameterDeclaration
    ParameterDeclaration ...
    ParameterDeclaration = AssignExpression
    ParameterDeclaration = AssignExpression ...
    ParameterAttributesopt ParameterTuplePattern
    ParameterAttributesopt ParameterTuplePattern = AssignExpression

ParameterDeclaration:
    ParameterAttributesopt BasicType Declarator
    ParameterAttributesopt Type

ParameterAttributes:
    ParameterStorageClass
    UserDefinedAttribute
    ParameterAttributes ParameterStorageClass
    ParameterAttributes UserDefinedAttribute

ParameterStorageClass:
    auto
    TypeCtor
    final
    in
    lazy
    out
    ref
    return
    scope

VariadicArgumentsAttributes:
    VariadicArgumentsAttribute
    VariadicArgumentsAttribute VariadicArgumentsAttributes

VariadicArgumentsAttribute:
    const
    immutable
    return
    scope
    shared

참고(Note): D2에서 파라미터를 final로 선언하는 것은 구문 오류(parse error)가 아니라 의미 오류(semantic error)입니다.

관련 내용: 파라미터 저장 클래스(parameter storage classes).

함수 속성 (Function Attributes)

FunctionAttributes:
    FunctionAttribute
    FunctionAttribute FunctionAttributes

FunctionAttribute:
    FunctionAttributeKwd
    Property
    AtAttribute

MemberFunctionAttributes:
    MemberFunctionAttribute
    MemberFunctionAttribute MemberFunctionAttributes

MemberFunctionAttribute:
    const
    immutable
    inout
    return refopt
    scope
    shared
    FunctionAttribute

FunctionAttributes는 선언 앞에 붙는 Attributes와 달리, FuncDeclaration이나 함수 포인터에서 함수 파라미터 뒤에 옵니다.

생성자(constructor), 멤버 함수, 델리게이트, 중첩 함수는 MemberFunctionAttributes를 가질 수 있는데, 이것이 FunctionAttributes를 확장한 형태예요.

  • 많은 함수/메서드 속성은 유효한 선언 속성(declaration attribute)이기도 해요. 다만 TypeCtor 속성은 위치에 따라 의미가 달라질 수 있다는 점을 기억하세요. 관련 내용은 '한정된 타입을 반환하는 메서드(Methods Returning a Qualified Type)'를 참고해요.
  • 일부 함수 속성은 추론(infer)될 수 있어요.
  • debug 문 안에서는 일부 함수 속성 검사가 꺼져요.

참고:

  • 함수 속성 (Function Attributes)
  • OOP 속성 (OOP Attributes)

함수 본문 (Function Bodies)

FunctionBody:
    SpecifiedFunctionBody
    ShortenedFunctionBody

SpecifiedFunctionBody:
    doopt BlockStatement
    FunctionContractsopt InOutContractExpression doopt BlockStatement
    FunctionContractsopt InOutStatement do BlockStatement

ShortenedFunctionBody:
    InOutContractExpressionsopt => AssignExpression ;

예시:

int hasSpecifiedBody() { return 1; }
int hasShortenedBody() => 1; // equivalent

ShortenedFunctionBody 형태는 return 문을 암시해요. 이 문법은 함수 리터럴에도 똑같이 적용돼요.

함수 프로토타입 (Function Prototypes)

MissingFunctionBody:
    ;
    FunctionContractsopt InOutContractExpression ;
    FunctionContractsopt InOutStatement

MissingFunctionBody를 가진 함수 선언, 예를 들면:

int foo();

abstract로 선언되지 않은 이런 선언들은 구현이 다른 어딘가에 있다고 간주하고, 그 구현은 **링크 단계(link step)**에서 채워져요. 이 덕분에 함수 구현을 사용자에게 완전히 숨길 수 있고, 구현 자체는 C나 어셈블러 같은 다른 언어로 작성할 수도 있어요. 보통 함수 프로토타입은 extern(D)가 아닌 링키지(linkage)를 갖는 경우가 많아요.

함수 프로토타입은 extern(D) 링키지를 가질 수도 있어요. D 인터페이스 파일을 작성할 때 유용하죠.

함수 계약 (Function Contracts)

FunctionContracts:
    FunctionContract
    FunctionContract FunctionContracts

FunctionContract:
    InOutContractExpression
    InOutStatement

InOutContractExpressions:
    InOutContractExpression
    InOutContractExpression InOutContractExpressions

InOutContractExpression:
    InContractExpression
    OutContractExpression

InOutStatement:
    InStatement
    OutStatement

함수 계약은 함수의 **전제 조건(precondition)**과 **사후 조건(postcondition)**을 명시해요. 이건 계약 프로그래밍(Contract Programming)에서 사용돼요.

전제 조건과 사후 조건은 함수의 타입에는 영향을 주지 않아요.

전제 조건 (Preconditions)

InContractExpression:
    in ( AssertArguments )

InStatement:
    in BlockStatement

InContractExpression은 전제 조건이에요.

  • AssertArguments의 첫 번째 AssignExpressiontrue로 평가되어야 해요. 그렇지 않으면 전제 조건이 실패한 거예요.
  • 두 번째 AssignExpression은 있다면 const(char)[] 타입으로 암시적으로 변환 가능해야 해요.

InStatement도 전제 조건이에요. InStatement 안에 나타나는 모든 AssertExpressionInContractExpression이 돼요.

전제 조건은 의미상 함수가 실행되기 전에 충족되어야 해요. 전제 조건이 실패하면 프로그램은 *유효하지 않은 상태(Invalid State)*로 들어가요.

구현 정의(Implementation Defined): 전제 조건을 실제로 실행할지 말지는 구현마다 다르고, 보통 컴파일러 스위치로 선택할 수 있어요. 전제 조건 실패 시의 동작도 보통 컴파일러 스위치로 고를 수 있어요. 그중 하나는 선택적인 두 번째 AssignExpression을 메시지로 하는 AssertError를 던지는 방식이에요.

모범 사례(Best Practices): 전제 조건은 함수가 기대하는 값을 입력 인자가 실제로 갖고 있는지 검증하는 데 사용하세요.

모범 사례(Best Practices): 전제 조건은 런타임에 실제로 검사될 수도 있고 안 될 수도 있으므로, 부작용(side effect)이 있는 전제 조건은 피하세요.

표현식 형태는 이렇게 생겼어요:

in (expression)
in (expression, "failure string")
{
    ...function body...
}

블록 문 형태는 이렇게요:

in
{
    ...contract preconditions...
}
do
{
    ...function body...
}

사후 조건 (Postconditions)

OutContractExpression:
    out ( ; AssertArguments )
    out ( Identifier ; AssertArguments )

OutStatement:
    out BlockStatement
    out ( Identifier ) BlockStatement

OutContractExpression은 사후 조건이에요.

  • AssertArguments의 첫 번째 AssignExpressiontrue로 평가되어야 해요. 그렇지 않으면 사후 조건이 실패한 거예요.
  • 두 번째 AssignExpression은 있다면 const(char)[] 타입으로 암시적으로 변환 가능해야 해요.

OutStatement도 사후 조건이에요. OutStatement 안에 나타나는 모든 AssertExpressionOutContractExpression이 돼요.

사후 조건은 의미상 함수 실행이 끝난 후에 충족되어야 해요. 사후 조건이 실패하면 프로그램은 유효하지 않은 상태로 들어가요.

구현 정의(Implementation Defined): 사후 조건을 실제로 실행할지 말지는 구현마다 다르고, 보통 컴파일러 스위치로 선택할 수 있어요. 사후 조건 실패 시의 동작도 보통 컴파일러 스위치로 고를 수 있어요. 그중 하나는 선택적인 두 번째 AssignExpression을 메시지로 하는 AssertError를 던지는 방식이에요.

모범 사례(Best Practices): 사후 조건으로 함수를 떠날 때 다음을 검증하세요:

  • 변경 가능한 입력 인자 각각이 기대하는 값/값 범위를 갖는지.
  • 반환 값이 올바른 값/값 범위를 갖는지.

모범 사례(Best Practices): 사후 조건도 런타임에 실제로 검사될 수도 있고 안 될 수도 있으므로, 부작용이 있는 사후 조건은 피하세요.

표현식 형태:

out (identifier; expression)
out (identifier; expression, "failure string")
out (; expression)
out (; expression, "failure string")
{
    ...function body...
}

블록 문 형태:

out
{
    ...contract postconditions...
}
out (identifier)
{
    ...contract postconditions...
}
do
{
    ...function body...
}

두 형태 모두에서 선택적인 식별자는 함수의 반환 값에 설정되며, 사후 조건 안에서 그 값을 접근할 수 있어요. 그 식별자는 암시적으로 const예요.

예시 (Example)

int fun(ref int a, int b)
in (a > 0)
in (b >= 0, "b cannot be negative!")
out (r; r > 0, "return must be positive")
out (; a != 0)
{
    // function body
}
int fun(ref int a, int b)
in
{
    assert(a > 0);
    assert(b >= 0, "b cannot be negative!");
}
out (r)
{
    assert(r > 0, "return must be positive");
    assert(a != 0);
}
do
{
    // function body
}

위 두 함수는 의미적으로 동일해요.

In, Out과 상속 (In, Out and Inheritance)

파생 클래스의 함수가 기반 클래스의 함수를 오버라이드하면, 전제 조건은 해당 함수와 그 오버라이드된 함수 중 하나만 충족하면 돼요. 따라서 오버라이딩은 전제 조건을 느슨하게(loosening) 만드는 과정이 돼요.

class B
{
    int i;

    int f() in (i == 5) => 5;
}
class C : B
{
    override int f() => i; // no `in` contract, `i` can have any value
}

B b = new B;
assert(b.i == 0);
//b.f(); // AssertError, `i == 5` failed

b = new C;
assert(b.f() == 0); // OK

전제 조건이 없는 함수는 전제 조건이 항상 충족된다는 뜻이에요. 그래서 상속 계층에서 어떤 함수가 전제 조건이 없다면, 그 함수를 오버라이드하는 함수의 전제 조건은 의미가 없어져요.

class C : Object
{
    int x;

    override string toString()
    in (x > 0) // Error, Object.toString has no `in` contract
    {
        return "";
    }
}

반대로, 사후 조건은 함수와 그 오버라이드된 함수의 모든 사후 조건이 충족되어야 해요. 그래서 오버라이딩은 사후 조건을 단단하게(tightening) 만드는 과정이 돼요.

class B
{
    int i;

    int f() out (; i > 5) => i;
}
class C : B
{
    override int f() out (; i < 10) => i;
}

B b = new C;
b.f(); // AssertError, `i > 5` failed
b.i = 10;
b.f(); // AssertError, `i < 10` failed

함수 반환 값 (Function Return Values)

함수가 void가 아닌 반환 타입을 명시하면, 실행 경로마다 하나의 return 문이 필요해요. 단, 다음 경우는 예외예요:

  • 경로가 무한 루프를 실행하는 경우
  • 경로가 assert(0) 표현식을 평가하는 경우
  • 경로가 noreturn 타입의 표현식을 평가하는 경우
  • 경로에 인라인 어셈블러 코드가 있는 경우

위 세 가지 중 간단한 형태는 컴파일러가 인식할 수 있지만, 전부는 아니에요.

int error(int i)
{
    if (i & 1)
        i++;
    else
        return i;

    // Error: no return statement
}
int f(int i)
{
    if (i >= 0)
        return i;

    assert(0);
    // OK, no return needed
}

ref로 표시되지 않은 함수 반환 값은 호출하는 함수 입장에서 rvalue로 취급돼요. 즉 다른 함수에 참조로 넘길 수 없다는 뜻이에요.

순수 함수 (Pure Functions)

순수 함수는 pure 속성으로 표시해요. 순수 함수는 전역(global) 또는 정적(static) 변경 가능 상태에 직접 접근할 수 없어요. 순수 함수는 순수 함수만 호출할 수 있어요.

순수 함수는 다음을 할 수 있어요:

  • 함수의 로컬 상태를 수정하기.
  • 예외를 던지기.
int x;
immutable int y;

pure int foo(int i)
{
    i++;     // ok, modifying local state
    //x = i; // error, modifying global state
    //i = x; // error, reading mutable global state
    i = y;   // ok, reading immutable global state
    throw new Exception("failed"); // ok
}

순수 함수는 순수하지 않은(impure) 함수를 오버라이드할 수 있지만, 순수하지 않은 함수가 순수 함수를 오버라이드할 수는 없어요. 즉 순수 함수는 순수하지 않은 함수에 대해 **공변(covariant)**이에요.

강한 순수성 vs 약한 순수성 (Strong vs Weak Purity)

*약하게 순수한 함수(weakly pure function)*는 변경 가능한 간접 참조(indirection)를 가진 파라미터를 갖고 있어요. 이 경우 대응하는 인자를 통해 프로그램 상태가 전이적으로(transitively) 수정될 수 있어요.

pure size_t foo(int[] arr)
{
    arr[] += 1;
    return arr.length;
}
int[] a = [1, 2, 3];
foo(a);
assert(a == [2, 3, 4]);

*강하게 순수한 함수(strongly pure function)*는 변경 가능한 간접 참조를 가진 파라미터가 없고, 함수 외부의 어떤 프로그램 상태도 수정할 수 없어요.

struct S { double x; }

pure size_t foo(immutable(int)[] arr, int num, S val)
{
    //arr[num] = 1; // compile error
    num = 2;        // has no side effect on the caller side
    val.x = 3.14;   // ditto
    return arr.length;
}

강하게 순수한 함수는 약하게 순수한 함수를 호출할 수 있어요.

특별한 경우 (Special Cases)

순수 함수는:

  • 부동소수점 예외 플래그(floating point exception flags)를 읽고 쓸 수 있어요.
  • 부동소수점 모드 플래그(floating point mode flags)를 읽고 쓸 수 있어요. 단, 그 플래그들이 함수 진입 시의 초기 상태로 복원되는 경우에 한해요.

미정의 동작(Undefined Behavior): 함수 종료 시 이러한 플래그가 초기 상태로 복원되지 않으면 미정의 동작이 발생해요. 이를 보장하는 건 프로그래머의 책임입니다. @safe 코드에서는 이런 플래그 설정이 허용되지 않아요.

디버깅 (Debugging)

순수 함수는 DebugCondition에 의해 제어되는 ConditionalStatement 안의 문에서 순수하지 않은 연산을 수행할 수 있어요.

모범 사례(Best Practices): DebugCondition 안에서 순수성 검사를 완화하는 것은 오로지 프로그램 디버깅을 더 쉽게 하기 위한 것입니다.

pure int foo(int i)
{
    debug writeln("i = ", i); // ok, impure code allowed in debug statement
    ...
}

중첩 함수 (Nested Functions)

순수 함수 안의 중첩 함수는 자동으로 pure로 표시돼요.

비정적(static이 아닌) 순수 중첩 함수는 컨텍스트 포인터를 통해 데이터에 접근(그리고 때로는 수정)할 수 있어요:

pure int foo()
{
    int x = 5;

    int bar()
    // implicitly marked as pure, to be "weakly pure"
    // since hidden context pointer to foo stack context is mutable
    {
        x = 10;     // can access states in enclosing scope
                    // through the mutable context pointer
        return x;
    }
    pragma(msg, typeof(&bar));  // int delegate() pure

    bar();
    return x;
}

static assert(foo() == 10);

참고(Note): 중첩 함수를 immutable로 한정하면 컨텍스트 포인터를 통한 변경 가능 데이터 접근이 막힙니다. immutable 순수 중첩 함수는 강하게 순수합니다.

순수 팩토리 함수 (Pure Factory Functions)

*순수 팩토리 함수(pure factory function)*는 변경 가능한 간접 참조만을 가진 결과를 반환하는 강하게 순수한 함수예요. 호출이 반환한 모든 변경 가능한 메모리는 프로그램의 다른 어떤 부분에서도 참조할 수 없어요. 즉 그 함수가 새로 할당한 것이죠. 결과의 변경 가능한 참조들 역시 함수 호출 전에 존재했던 어떤 객체도 가리킬 수 없어요. 이 덕분에 결과를 어떤 타입에서든 immutable이나 const shared로 암시적으로 캐스팅할 수 있고, shared, const shared에서 (비공유) const로도 캐스팅할 수 있어요. 예를 들어:

struct List { int payload; List* next; }

pure List* make(int a, int b)
{
    auto result = new List(a, null);
    result.next = new List(b, null);
    return result;
}

void main()
{
    auto list = make(1, 2);
    pragma(msg, typeof(list));       // List*

    immutable ilist = make(1, 2);
    pragma(msg, typeof(ilist));      // immutable List*
    pragma(msg, typeof(ilist.next)); // immutable List*
}

make의 결과에 있는 모든 참조는 make가 만든 List 객체를 가리키고, 프로그램의 다른 어떤 부분도 이 객체들을 참조하지 않아요. 그래서 결과가 immutable 변수를 초기화할 수 있는 거예요.

이것은 함수에서 던져지는 Exception이나 Error에는 영향을 주지 않아요.

최적화 (Optimization)

구현 정의(Implementation Defined): 구현은 변경 가능한 간접 참조가 없거나 전혀 없는 인자로 호출되면서, 변경 가능한 간접 참조가 없는 결과를 반환하는 강하게 순수한 함수가, 동등한 인자로 호출될 때마다 항상 같은 효과를 낸다고 가정할 수 있습니다. 동등한 인자가 항상 동등한 결과를 만든다는 가정 아래 함수 결과를 메모이즈(memoize)해도 허용됩니다.

int a(int) pure; // no mutable indirections
int b(const Object) pure; // depends on argument passed
immutable(Object) c(immutable Object) pure; // always memoizable

void g();

void f(int n, const Object co, immutable Object io)
{
    const int x = a(n);
    g(); // `n` does not change between calls to `a`
    int i = a(n); // same as `i = x`

    const int y = b(co);
    // `co` may have mutable indirection
    g(); // may change fields of `co` through another reference
    i = b(co); // call is not memoizable, result may differ

    const int z = b(io);
    i = b(io); // same as `i = z`
}

이런 함수가 예를 들어 cast를 쓰거나 파라미터의 주소에 따라 동작을 바꿈으로써 메모이제이션과 일치하지 않는 동작을 보일 수도 있어요. 현재 구현은 모든 경우에 메모이제이션의 유효성을 강제할 필요는 없어요.

함수가 Exception이나 Error를 던지면, 메모이제이션과 관련된 가정은 던져진 예외까지 이어지지 않아요.

순수 소멸자(pure destructor)는 특별한 소거(elision) 혜택을 받지 않아요.

Nothrow 함수 (Nothrow Functions)

Nothrow 함수는 class Error에서 파생된 예외만 던질 수 있어요.

Nothrow 함수는 던질 수 있는(throwing) 함수에 대해 공변(covariant)이에요.

Ref 함수 (Ref Functions)

ref 함수는 값을 반환하는 대신 참조로 반환해요. ref 함수의 반환 값은 반드시 lvalue여야 해요(반면 ref가 아닌 함수의 반환 값은 rvalue일 수도 있어요). ref 함수를 호출해서 만든 표현식은 lvalue예요(ref가 아닌 함수 호출 표현식은 rvalue).

int *p;

ref int foo()
{
    p = new int(2);
    return *p;
}

void main()
{
    int i = foo();
    assert(i == 2);

    foo() = 3;     // reference returns can be lvalues
    assert(*p == 3);
}

만료된 함수 컨텍스트에 대한 참조를 반환하는 건 허용되지 않아요. 여기에는 만료된 함수 컨텍스트의 일부인 로컬 변수, 임시 값, 파라미터가 포함돼요.

ref int sun()
{
    int i;
    return i;  // error, escaping a reference to local variable i
}

ref 파라미터는 return ref가 아니면 ref로 반환할 수 없어요.

ref int moon(ref int i)
{
    return i; // error
}

Auto 함수 (Auto Functions)

Auto 함수는 반환 타입을 함수 본문의 ReturnStatements에서 추론해요.

Auto 함수는 반환 타입 없이 선언돼요. Auto 함수는 auto뿐 아니라 어떤 유효한 StorageClass든 사용할 수 있어요.

ReturnStatement가 여러 개라면 그 타입들이 공통 타입으로 암시적으로 변환 가능해야 해요. ReturnStatement가 없다면 반환 타입은 void로 추론돼요.

auto foo(int x) { return x + 3; }          // inferred to be int
pure bar(int x) { return x; return 2.5; }  // inferred to be double

참고(Note): 반환 타입 추론은 속성(attribute) 추론도 함께 촉발합니다.

Auto Ref 함수 (Auto Ref Functions)

Auto ref 함수는 auto 함수처럼 반환 타입을 추론할 수 있어요. 게다가 다음 조건이 모두 성립하면 ref 함수가 돼요:

  • 함수에서 반환되는 모든 표현식이 lvalue인 경우
  • 로컬 변수가 반환되지 않는 경우
  • 반환되는 파라미터가 참조 파라미터인 경우
  • 각 반환 표현식이 추론된 반환 타입의 lvalue로 암시적으로 변환되는 경우
auto ref f1(int x)     { return x; }  // value return
auto ref f2()          { return 3; }  // value return
auto ref f3(ref int x) { return x; }  // ref return
auto ref f4(out int x) { return x; }  // ref return
auto ref f5()
{
    static int x;
    return x; // ref return
}

함수의 ref 여부는 함수 본문의 모든 ReturnStatement에서 결정돼요:

auto ref f1(ref int x) { return 3; return x; }  // ok, value return
auto ref f2(ref int x) { return x; return 3; }  // ok, value return
auto ref f3(ref int x, ref double y)
{
    return x; return y;
    // The return type is deduced to be double, but cast(double)x is not an lvalue,
    // so f3 has a value return.
}

Auto ref 함수는 명시적인 반환 타입을 가질 수도 있어요.

auto ref int bar(ref int x) { return x; }  // ok, ref return
auto ref int foo(double x) { return x; }   // error, cannot convert double to int

Inout 함수 (Inout Functions)

자세한 내용은 inout 타입 한정자(type qualifier)를 참고하세요.

선택적 괄호 (Optional Parentheses)

함수 호출이 명시적 인자를 전달하지 않는, 즉 구문상 ()를 쓰는 경우라면, 프로퍼티 함수의 getter 호출처럼 그 괄호를 생략할 수 있어요. UFCS 호출도 빈 괄호를 생략할 수 있어요.

void foo() {}   // no arguments
void fun(int x = 10) {}
void bar(int[] arr) {}

void main()
{
    foo();      // OK
    foo;        // also OK
    fun;        // OK

    int[] arr;
    arr.bar();  // UFCS call
    arr.bar;    // also OK
}

모호함 때문에, 델리게이트나 함수 포인터를 호출할 때는 괄호가 필요해요:

void main()
{
    int function() fp;

    assert(fp == 6);    // Error, incompatible types int function() and int
    assert(*fp == 6);   // Error, incompatible types int() and int

    int delegate() dg;
    assert(dg == 6);    // Error, incompatible types int delegate() and int
}

함수가 델리게이트나 함수 포인터를 반환하면, 괄호는 결과가 아니라 함수 호출에 먼저 적용돼요. 결과를 직접 호출하려면 괄호 두 겹이 필요해요:

int getNum() { return 6; }
int function() getFunc() { return &getNum; }

void main()
{
    int function() fp;

    fp = getFunc;   // implicit call
    assert(fp() == 6);

    fp = getFunc(); // explicit call
    assert(fp() == 6);

    int x = getFunc()();
    assert(x == 6);
}
struct S
{
    int getNum() { return 6; }
    int delegate() getDel() return { return &getNum; }
}

void main()
{
    S s;
    int delegate() dg;

    dg = s.getDel;   // implicit call
    assert(dg() == 6);

    dg = s.getDel(); // explicit call
    assert(dg() == 6);

    int y = s.getDel()();
    assert(y == 6);
}

프로퍼티 함수 (Property Functions)

경고(WARNING): 프로퍼티 함수의 정의와 유용성은 아직 검토 중이며, 구현도 현재 불완전해요. 정의가 더 확실해지고 구현이 더 성숙해질 때까지는 프로퍼티 함수 사용을 권장하지 않아요.

프로퍼티는 필드나 변수처럼 구문적으로 다룰 수 있는 함수예요. 프로퍼티는 읽거나 쓸 수 있어요. 프로퍼티는 인자 없이 메서드/함수를 호출하면 읽히고, 설정할 값이 인자로 전달된 메서드/함수 호출로는 쓰여져요.

간단한 getter/setter 프로퍼티는 UFCS로 작성할 수 있어요. 여기에 @property 속성을 추가해 다음 동작들을 더할 수 있어요:

  • @property 함수는 같은 이름의 @property가 아닌 함수와 오버로드될 수 없어요.
  • @property 함수는 파라미터를 0개, 1개, 2개만 가질 수 있어요.
  • @property 함수는 가변 인자(variadic) 파라미터를 가질 수 없어요.
  • exp@property 함수인 typeof(exp) 표현식에서, 타입은 함수의 타입이 아니라 함수의 반환 타입이 돼요.
  • exp@property 함수인 __traits(compiles, exp) 표현식에서는, 함수를 실제로 호출할 수 있는지 추가로 검사해요.
  • @property는 다르게 맹글링(mangling)돼요. 즉 @property는 서로 다른 컴파일 단위에서 일관되게 사용되어야 해요.
  • ObjectiveC 인터페이스는 @property setter 함수를 특별하게 인식하고 그에 맞게 수정해요.

간단한 프로퍼티는 이렇게 생겼어요:

struct Foo
{
    @property int data() { return m_data; } // read property

    @property int data(int value) { return m_data = value; } // write property

  private:
    int m_data;
}

사용할 때:

int test()
{
    Foo f;

    f.data = 3;        // same as f.data(3);
    return f.data + 3; // same as return f.data() + 3;
}

읽기 메서드가 없으면 그 프로퍼티는 **쓰기 전용(write-only)**이에요. 쓰기 메서드가 없으면 **읽기 전용(read-only)**이에요. 쓰기 메서드는 여러 개 있을 수 있고, 일반적인 함수 오버로딩 규칙에 따라 올바른 것이 선택돼요.

그 외의 모든 면에서 이 메서드들은 다른 메서드와 동일해요. static일 수도, 다른 링키지를 가질 수도, 주소를 취할 수도 있어요.

내장 프로퍼티인 .sizeof, .alignof, .mangleof는 struct, union, class, enum에서 필드나 메서드로 선언할 수 없어요.

프로퍼티 함수가 파라미터가 없으면 getter로 동작하고, 파라미터가 정확히 하나면 setter로 동작해요.

가상 함수 (Virtual Functions)

가상 함수는 직접 호출되지 않고 함수 포인터 테이블(vtbl[])을 통해 간접적으로 호출되는 클래스 멤버 함수예요. 가상인 멤버 함수는 파생 클래스에서 오버라이드할 수 있어요:

class A
{
    void foo(int x) {}
}

class B : A
{
    override void foo(int x) {}
    //override void foo() {} // error, no foo() in A
}

void test()
{
    A a = new B();
    a.foo(1);   // calls B.foo(int)
}

함수를 오버라이드할 때는 override 속성이 필요해요. 이건 기반 클래스의 멤버 함수 파라미터가 바뀌었을 때, 모든 파생 클래스의 오버라이딩 함수를 갱신해야 하는 실수를 잡아내는 데 유용해요.

final 메서드 속성은 서브클래스가 그 메서드를 오버라이드하지 못하게 막아요.

다음은 가상이 아니에요:

  • struct와 union의 멤버 함수
  • final 멤버 함수
  • static 멤버 함수
  • private 또는 package인 멤버 함수
  • 멤버 템플릿 함수

예시:

class A
{
    int def() { ... }
    final int foo() { ... }
    final private int bar() { ... }
    private int abc() { ... }
}

class B : A
{
    override int def() { ... }  // ok, overrides A.def
    override int foo() { ... }  // error, A.foo is final
    int bar() { ... }  // ok, A.bar is final private, but not virtual
    int abc() { ... }  // ok, A.abc is not virtual, B.abc is virtual
}

void test()
{
    A a = new B;
    a.def();    // calls B.def
    a.foo();    // calls A.foo
    a.bar();    // calls A.bar
    a.abc();    // calls A.abc
}

Objective-C 링키지를 가진 멤버 함수는 final이나 static으로 표시해도 가상이며, 오버라이드할 수 있어요.

공변성 (Covariance)

오버라이딩 함수는 오버라이드된 함수와 **공변(covariant)**일 수 있어요. 공변 함수는 오버라이드된 함수의 타입으로 암시적으로 변환 가능한 타입을 갖고 있어요.

class A { }
class B : A { }

class Foo
{
    A test() { return null; }
}

class Bar : Foo
{
    // overrides and is covariant with Foo.test()
    override B test() { return null; }
}

static assert(is(typeof(&Bar.test) : typeof(&Foo.test)));

기반 클래스 메서드 호출 (Calling Base Class Methods)

기반 클래스 Base의 멤버 함수를 직접 호출하려면 함수 이름 앞에 Base.를 쓰면 돼요. 이렇게 하면 함수 포인터를 통한 동적 디스패치를 피할 수 있어요. 예를 들어:

class B
{
    int foo() { return 1; }
}
class C : B
{
    override int foo() { return 2; }

    void test()
    {
        assert(B.foo() == 1);  // translated to this.B.foo(), and
                               // calls B.foo statically.
        assert(C.foo() == 2);  // calls C.foo statically, even if
                               // the actual instance of 'this' is D.
    }
}
class D : C
{
    override int foo() { return 3; }
}
void main()
{
    auto d = new D();
    assert(d.foo() == 3);    // calls D.foo
    assert(d.B.foo() == 1);  // calls B.foo
    assert(d.C.foo() == 2);  // calls C.foo
    d.test();
}

기반 클래스 메서드는 super 참조를 통해서도 호출할 수 있어요.

구현 정의(Implementation Defined): 보통 가상 함수 호출은 런타임에 클래스의 vtbl[]을 인덱싱해 함수 주소를 얻는 것을 의미합니다. 구현이 호출되는 가상 함수가 final처럼 정적으로 알려질 수 있다고 판단하면, 대신 직접 호출을 사용할 수 있어요.

오버로드 집합과 오버라이딩 (Overload Sets and Overriding)

오버로드 해석(overload resolution)을 할 때, 기반 클래스의 함수는 같은 오버로드 집합(Overload Set)에 없으므로 고려되지 않아요.

class A
{
    int foo(int x) { ... }
    int foo(long y) { ... }
}

class B : A
{
    override int foo(long x) { ... }
}

void test()
{
    B b = new B();
    b.foo(1);  // calls B.foo(long), since A.foo(int) is not considered

    A a = b;
    a.foo(1);  // issues runtime error (instead of calling A.foo(int))
}

오버로드 해석 과정에 기반 클래스의 함수를 포함시키려면 AliasDeclaration을 사용하세요:

class A
{
    int foo(int x) { ... }
    int foo(long y) { ... }
}

class B : A
{
    alias foo = A.foo;
    override int foo(long x) { ... }
}

void test()
{
    A a = new B();
    a.foo(1);      // calls A.foo(int)
    B b = new B();
    b.foo(1);      // calls A.foo(int)
}

이런 AliasDeclaration을 쓰지 않으면, 파생 클래스의 함수는 기반 클래스의 같은 이름을 가진 모든 함수를 완전히 오버라이드해요. 기반 클래스 함수의 파라미터 타입이 다르더라도 마찬가지예요. 그리고 기반 클래스로의 암시적 변환을 통해 그 다른 함수들이 호출되게 되는 상황은 불법이에요:

class A
{
    void set(long i) { }
    void set(int i)  { }
}
class B : A
{
    override void set(long i) { }
}

void test()
{
    A a = new B;
    a.set(3);   // error, use of A.set(int) is hidden by B
                // use 'alias set = A.set;' to introduce base class overload set
}

기본 값 (Default Values)

함수 파라미터의 기본 값은 상속되지 않아요.

class A
{
    void foo(int x = 5) { ... }
}

class B : A
{
    void foo(int x = 7) { ... }
}

class C : B
{
    void foo(int x) { ... }
}

void test()
{
    A a = new A();
    a.foo();       // calls A.foo(5)

    B b = new B();
    b.foo();       // calls B.foo(7)

    C c = new C();
    c.foo();       // error, need an argument for C.foo
}

상속되는 속성 (Inherited Attributes)

오버라이딩 함수는 오버라이드된 함수의 속성에서 명시되지 않은 FunctionAttributes를 상속해요.

class B
{
    void foo() pure nothrow @safe {}
}
class D : B
{
    override void foo() {}
}
void main()
{
    auto d = new D();
    pragma(msg, typeof(&d.foo));
    // prints "void delegate() pure nothrow @safe" in compile time
}

제약 사항 (Restrictions)

@disabledeprecated 속성은 오버라이딩 함수에 허용되지 않아요.

이유(Rationale): 컴파일이나 경고를 막기 위해, 구현이 호출 대상을 알아야 합니다. 가상 함수에서는 그 대상을 보장할 수 없어요.

class B
{
    void foo() {}
}

class D : B
{
    @disable override void foo() {}  // error, can't apply @disable to overriding function
}

인라인 함수 (Inline Functions)

함수를 인라인할지 말지는 컴파일러가 결정해요. 이 결정은 pragma(inline)으로 제어할 수 있어요.

구현 정의(Implementation Defined): 함수를 인라인할지 말지는 구현마다 다릅니다. 다만 어떤 FunctionLiteral이라도 자신이 선언된 스코프에서 사용될 때는 인라인되어야 합니다.

함수 오버로딩 (Function Overloading)

함수 오버로딩은 같은 스코프에 같은 이름의 함수가 둘 이상 있을 때 발생해요. 선택되는 함수는 인자에 가장 잘 맞는(best match) 함수예요. 매칭 등급은 다음과 같아요:

  1. 매칭 없음 (No match)
  2. 암시적 변환으로 매칭 (Match with implicit conversions)
  3. 한정자 변환으로 매칭 (Match with qualifier conversion, 인자 타입이 파라미터 타입으로 한정자 변환 가능할 때)
  4. 정확한 매칭 (Exact match)

이름 있는 인자(named argument)는 '인자를 파라미터에 매칭하기(Matching Arguments to Parameters)' 규칙에 따라 후보 함수마다 해석돼요. 만약 이게 실패하면(예: 오버로드에 이름 있는 인자와 일치하는 파라미터가 없는 경우), 등급은 매칭 없음이에요. 그 외에는 이름 있는 인자가 매칭 등급에 영향을 주지 않아요.

각 인자(this 참조 포함)는 함수의 대응하는 파라미터와 비교되어 그 인자에 대한 매칭 등급이 결정돼요. 함수의 매칭 등급은 각 인자의 매칭 등급 중 가장 나쁜(최악의) 등급이에요.

리터럴은 refout 파라미터와 매칭되지 않아요.

scope 파라미터 저장 클래스는 함수 오버로딩에 영향을 주지 않아요.

둘 이상의 함수가 같은 매칭 등급이면, 부분 순서(partial ordering) 로 최상의 매칭을 찾아 모호함을 해소해요. 부분 순서는 가장 특수화된(specialized) 함수를 찾아내요. 어느 쪽도 더 특수화되지 않았다면 모호성 오류(ambiguity error)예요. 부분 순서는 함수 fg에 대해, f의 파라미터 타입을 가져와 그 타입들의 기본 값을 인자 목록으로 만들고 이것을 g에 매칭해 봄으로써 결정돼요. 성공하면 gf만큼 특수화된 것이에요. 예를 들어:

class A { }
class B : A { }
class C : B { }
void foo(A);
void foo(B);

void test()
{
    C c;
    /* Both foo(A) and foo(B) match with implicit conversions (level 2).
     * Applying partial ordering rules,
     * foo(B) cannot be called with an A, and foo(A) can be called
     * with a B. Therefore, foo(B) is more specialized, and is selected.
     */
    foo(c); // calls foo(B)
}

가변 인자를 가진 함수는 그렇지 않은 함수보다 특수화가 덜 된 것으로 간주돼요.

정적 멤버 함수는 멤버 함수와 오버로드될 수 있어요. 정적 멤버 함수의 struct, class, union은 this 인자의 타입에서 추론돼요.

struct S {
    void eggs(int);
    static void eggs(long);
}
S s;
s.eggs(0);  // calls void eggs(int);
S.eggs(0);  // error: need `this`
s.eggs(0L); // calls static void eggs(long);
S.eggs(0L); // calls static void eggs(long);

struct T {
    void bacon(int);
    static void bacon(int);
}
T t;
t.bacon(0);  // error: ambiguous
T.bacon(0);  // error: ambiguous

이유(Rationale): this 파라미터가 필요 없는 정적 멤버 함수는 그걸 전달할 필요가 없습니다.

오버로드 집합 (Overload Sets)

*오버로드 집합(Overload Set)*은 같은 스코프에서 같은 이름으로 선언되어 오버로드 해석에 참여하는 함수들의 집합이에요. 모듈 수준에서 정의된 함수들이 오버로드 집합의 예시예요:

module A;
void foo() { }
void foo(long i) { }

A.foo()A.foo(long)은 오버로드 집합을 이뤄요. 다른 모듈도 같은 이름의 함수들로 또 다른 오버로드 집합을 정의할 수 있어요:

module B;
class C { }
void foo(C) { }
void foo(int i) { }

그리고 A와 B를 세 번째 모듈 C가 import할 수 있어요. 심볼 foo를 검색하면 A.foo 오버로드 집합과 B.foo 오버로드 집합이 모두 발견돼요. foo의 인스턴스는 정확히 하나의 오버로드 집합에서 매칭되는 것을 기준으로 선택돼요.

import A;
import B;

void bar(C c , long i)
{
    foo();    // calls A.foo()
    foo(i);  // calls A.foo(long)
    foo(c);   // calls B.foo(C)
    foo(1,2); // error, does not match any foo
    foo(1);   // error, matches A.foo(long) and B.foo(int)
    A.foo(1); // calls A.foo(long)
}

foo(1)에 대해 B.foo(int)A.foo(long)보다 더 나은 매칭이어도, 두 매칭이 서로 다른 오버로드 집합에 있기 때문에 오류예요.

오버로드 집합은 alias 선언으로 병합할 수 있어요:

import A;
import B;

alias foo = A.foo;
alias foo = B.foo;

void bar(C c)
{
    foo();    // calls A.foo()
    foo(1L);  // calls A.foo(long)
    foo(c);   // calls B.foo(C)
    foo(1,2); // error, does not match any foo
    foo(1);   // calls B.foo(int)
    A.foo(1); // calls A.foo(long)
}

함수 파라미터 (Function Parameters)

파라미터 저장 클래스 (Parameter Storage Classes)

파라미터 저장 클래스는 in, out, ref, lazy, return, scope예요. 파라미터는 또한 타입 생성자(constructor)인 const, immutable, shared, inout을 취할 수 있어요.

in, out, ref, lazy는 서로 배타적이에요. 처음 셋은 각각 입력(input), 출력(output), 입출력(input/output) 파라미터를 나타내는 데 쓰여요. 예를 들어:

int read(in char[] input, ref size_t count, out int errno);

void main()
{
    size_t a = 42;
    int b;
    int r = read("Hello World", a, b);
}

read는 세 개의 파라미터를 가져요. input은 읽기만 되며 참조가 유지되지 않아요. count는 읽고 쓸 수 있고, errno는 함수 안에서 나온 값으로 설정돼요.

인자 "Hello World"는 파라미터 input에, acount에, berrno에 바인딩돼요.

파라미터 저장 클래스와 타입 생성자 개요 (Overview)

저장 클래스 설명
none 파라미터는 인자의 변경 가능한 복사본이 됩니다.
in 파라미터는 함수에 대한 입력입니다.
out 인자는 lvalue여야 하며, 참조로 전달되고 함수 진입 시 해당 타입의 기본 값(T.init)으로 초기화됩니다.
ref 파라미터는 참조로 전달되는 입출력 파라미터입니다.
scope 파라미터는 함수 호출을 벗어나서는 안 됩니다(예: 전역 변수에 할당되는 것). 참조 타입이 아닌 파라미터에 대해서는 무시됩니다.
return 파라미터는 반환되거나 첫 번째 파라미터로 복사될 수 있지만, 그 외에는 함수 밖으로 벗어나지 않습니다. 그런 복사본은 원래 인자보다 오래 살아남지 않아야 합니다. 참조가 없는 파라미터에 대해서는 무시됩니다. Scope 파라미터를 참조하세요.
lazy 인자는 호출자(caller)가 아닌 호출된 함수(called function)가 평가합니다.
타입 생성자 설명
const 인자는 암시적으로 const 타입으로 변환됩니다.
immutable 인자는 암시적으로 immutable 타입으로 변환됩니다.
shared 인자는 암시적으로 shared 타입으로 변환됩니다.
inout 인자는 암시적으로 inout 타입으로 변환됩니다.

In 파라미터 (In Parameters)

이 파라미터는 함수에 대한 입력이에요. 입력 파라미터는 마치 const 저장 클래스를 가진 것처럼 동작해요.

참고(Note): 다음 내용은 -preview=in 스위치와 dmd v2.094.0 이상을 준수하는 컴파일러가 필요합니다.

in 파라미터는 scope 파라미터처럼도 동작해요.

const(int[]) g(in int[] a) @safe
{
    a[0]++; // Error: cannot modify const expression
    return a; // Error: scope parameter `a` may not be returned
}

입력 파라미터는 컴파일러에 의해 참조로 전달될 수도 있어요. ref 파라미터와 달리 in 파라미터는 lvalue와 rvalue(리터럴 같은) 모두에 바인딩될 수 있어요.

  • 값으로 전달하면 부작용을 일으키는 타입(복사 생성자, postblit, 파괴자가 있는 타입 같은)은 항상 참조로 전달돼요.
  • 복사할 수 없는 타입(예: 복사 생성자가 @disable로 표시된 경우)은 항상 참조로 전달돼요.
  • 동적 배열, class, 연관 배열, 함수 포인터, 델리게이트는 항상 값으로 전달돼요.

구현 정의(Implementation Defined): 파라미터 타입이 그 범주에 들지 않으면 참조로 전달될지 여부는 구현마다 다르며, 백엔드는 플랫폼의 ABI에 가장 잘 맞는 방식을 자유롭게 고를 수 있습니다.

Ref와 Out 파라미터 (Ref and Out Parameters)

기본적으로 파라미터는 rvalue 인자를 취해요. ref 파라미터는 lvalue 인자를 취하므로, 그 값에 대한 변경이 호출자의 인자에 작용해요.

void inc(ref int x)
{
    x += 1;
}

void seattle()
{
    int z = 3;
    inc(z);
    assert(z == 4);
}

ref 파라미터는 참조로도 반환될 수 있어요. 'Return Ref 파라미터(Return Ref Parameters)'를 참고하세요.

out 파라미터는 ref 파라미터와 비슷하지만, 함수 호출 시 기본 값으로 초기화된다는 점이 달라요.

void zero(out int x)
{
    assert(x == 0);
}

void two(out int x)
{
    x = 2;
}

void tacoma()
{
    int a = 3;
    zero(a);
    assert(a == 0);

    int y = 3;
    two(y);
    assert(y == 2);
}

항상 참조로 전달되는 동적 배열과 class 객체 파라미터의 경우, outref는 내용이 아니라 참조 자체에만 적용돼요.

Lazy 파라미터 (Lazy Parameters)

lazy 파라미터의 인자는 함수가 호출되기 전에 평가되지 않아요. 인자는 함수 안에서 파라미터가 평가될 때에만(그리고 그때) 평가돼요. 그래서 lazy 인자는 0번 이상 실행될 수 있어요.

import std.stdio : writeln;

void main()
{
    int x;
    3.times(writeln(x++));
    writeln("-");
    writeln(x);
}

void times(int n, lazy void exp)
{
    if (n > 0)
    {
        exp();
        times(n - 1, exp);
    }
}

void 타입의 lazy 파라미터는 어떤 타입의 인자든 받아들일 수 있어요.

참고: Lazy 가변 함수(Lazy Variadic Functions).

기본 인자 (Default Arguments)

함수 파라미터 선언은 기본 값을 가질 수 있어요:

void foo(int x, int y = 3)
{
    ...
}
...
foo(4);   // same as foo(4, 3);

기본 파라미터는 함수 선언의 컨텍스트에서 해석되고 의미 검사(semantically checked)돼요.

module m;
private immutable int b;
pure void g(int a = b) {}
import m;
int b;
pure void f()
{
    g();  // ok, uses m.b
}

AssignExpression의 속성은 기본 표현식이 사용되는 곳에서 적용돼요.

module m;
int b;
pure void g(int a = b) {}
import m;
enum int b = 3;
pure void f()
{
    g();  // error, cannot access mutable global `m.b` in pure function
}

참고: 기본 값을 가진 함수 타입 alias.

Return Ref 파라미터 (Return Ref Parameters)

Return ref 파라미터는 반환된 참조가 대응하는 인자의 수명보다 오래 살아남지 못하게 하기 위해 ref 함수와 함께 사용돼요.

ref int identity(return ref int x) {
    return x; // pass-through function that does nothing
}

ref int fun() {
    int x;
    return identity(x); // Error: escaping reference to local variable x
}

ref int gun(return ref int x) {
    return identity(x); // OK
}

ref 변수의 주소를 반환하는 것도 @safe 코드에서 검사돼요.

int* pluto(ref int i) @safe
{
    return &i; // error: returning &i escapes a reference to parameter i
}

int* mars(return ref int i) @safe
{
    return &i;  // OK with -preview=dip1000
}

함수가 void를 반환하고 첫 번째 파라미터가 ref 또는 out이면, 이후의 모든 return ref 파라미터는 수명 검사(lifetime checking)를 위해 첫 번째 파라미터에 할당되는 것으로 간주돼요.

void f(ref scope int* p, return ref int i) @safe
{
    p = &i; // OK with -preview=dip1000
}

void main() @safe
{
    int i;
    int* p;
    f(p, i); // OK, lifetime of p is shorter than i
    *p = 5;
    assert(i == 5);

    int j;
    //f(p, j); // error, lifetime of p is longer than j
}

struct 비정적 멤버 함수의 this 참조 파라미터는 첫 번째 파라미터로 간주돼요.

struct S
{
    private int* p;

    void f(return ref int i) scope @safe
    {
        p = &i; // OK with -preview=dip1000
    }
}

void main() @safe
{
    int i;
    S s;
    s.f(i); // OK, lifetime of `s` is shorter than `i`
    *s.p = 2;
    assert(i == 2);
}

return ref 파라미터가 여러 개면, 반환 값의 수명은 대응하는 인자들의 수명 중 가장 짧은 것이에요.

반환 값의 수명을 결정할 때 return ref 파라미터(들)의 타입이나 반환 값의 타입은 고려되지 않아요.

반환 타입에 간접 참조가 없어도 오류가 아니에요.

int mercury(return ref int i)
{
    return i; // ok
}

템플릿 함수, auto 함수, 중첩 함수, 람다는 return 속성을 추론할 수 있어요.

@safe:

ref int templateFunction()(ref int i)
{
    return i; // ok
}

ref auto autoFunction(ref int i)
{
    return i; // ok
}

void uranus()
{
    ref int nestedFunction(ref int i)
    {
        return i; // ok
    }
    auto lambdaFunction =
        (ref int i)
        {
            return &i; // ok
        };
}

Struct 반환 메서드 (Struct Return Methods)

Struct 비정적 메서드는 반환된 참조가 struct 인스턴스보다 오래 살아남지 못하게 하기 위해 return 속성으로 표시할 수 있어요. 이는 return ref 메서드 속성과 같아요.

struct S
{
    private int x;
    ref int get() return { return x; }
}

ref int escape()
{
    S s;
    return s.get(); // Error: escaping reference to local variable s
}

숨겨진 this ref 파라미터는 그때 return ref가 돼요.

return 속성은 메서드가 ref가 아닐 때에도 반환 값의 수명을 제한하는 데 사용할 수 있어요:

struct S
{
    private int i;
    int* get() return @safe => &i;
}

void f() @safe
{
    int* p;
    {
        S s;
        int *q = s.get(); // OK, q has shorter lifetime than s
        p = s.get(); // error, p has longer lifetime
        p = (new S).get(); // OK, heap allocated S
    }
}

Scope 파라미터 (Scope Parameters)

참조 타입의 scope 파라미터는 함수 호출을 벗어나서는 안 돼요(예: 전역 변수에 할당되는 것). 참조 타입이 아닌 타입에는 효과가 없어요. scope 탈출 분석(escape analysis)은 @safe 함수에 대해서만 수행돼요. 다른 함수에서는 scope 의미를 수동으로 강제해야 해요.

참고(Note): @safe 탈출 분석은 -preview=dip1000 스위치에서만 수행됩니다.

@safe:

int* gp;
void thorin(scope int*);
void gloin(int*);
int* balin(scope int* q, int* r)
{
     gp = q; // error, q escapes to global gp
     gp = r; // ok

     thorin(q); // ok, q does not escape thorin()
     thorin(r); // ok

     gloin(q); // error, gloin() escapes q
     gloin(r); // ok that gloin() escapes r

     return q; // error, cannot return 'scope' q
     return r; // ok
}

구현 정의(Implementation Defined): scope 파라미터는 벗어날 수 없으므로, 컴파일러는 scope 파라미터에 전달되는 유일한(unique) 인자를 힙 할당하지 않을 수 있습니다. 그래서 @nogc 컨텍스트에서 배열 리터럴, 델리게이트 리터럴, NewExpression을 scope 파라미터에 전달하는 것이 허용될 수 있어요.

Return Scope 파라미터 (Return Scope Parameters)

return scope로 표시되고 간접 참조를 포함하는 파라미터는 함수의 반환 값을 통해서만 간접 참조를 벗어날 수 있어요.

@safe:

int* gp;
void thorin(scope int*);
void gloin(int*);
int* balin(return scope int* p)
{
     gp = p; // error, p escapes to global gp
     thorin(p); // ok, p does not escape thorin()
     gloin(p); // error, gloin() escapes p
     return p; // ok
}

class 참조는 scope가 적용되는 포인터로 간주돼요.

@safe:

class C { }
C gp;
void thorin(scope C);
void gloin(C);
C balin(return scope C p, scope C q, C r)
{
     gp = p; // error, p escapes to global gp
     gp = q; // error, q escapes to global gp
     gp = r; // ok

     thorin(p); // ok, p does not escape thorin()
     thorin(q); // ok
     thorin(r); // ok

     gloin(p); // error, gloin() escapes p
     gloin(q); // error, gloin() escapes q
     gloin(r); // ok that gloin() escapes r

     return p; // ok
     return q; // error, cannot return 'scope' q
     return r; // ok
}

return scope는 class와 interface 멤버 함수의 this에도 적용할 수 있어요.

class C
{
    C bofur() return scope { return this; }
}

템플릿 함수, auto 함수, 중첩 함수, 람다는 return scope 속성을 추론할 수 있어요.

Ref Return Scope 파라미터 (Ref Return Scope Parameters)

같은 파라미터에 대해 return refreturn scope 의미를 둘 다 가질 수는 없어요. 파라미터가 ref로 전달되고 returnscope 저장 클래스를 모두 가질 때, returnscope 키워드가 그 순서로 인접해 있는 경우에만 return scope 의미를 얻어요. return refscope 파라미터를 지정하면 scope 포인터에 대한 참조를 반환할 수 있게 돼요. 그 외의 모든 경우, 파라미터는 return ref 의미와 일반적인 scope 의미를 가져요.

U xerxes(       ref return scope V v) // (1) ref and return scope
U sargon(return ref        scope V v) // (2) return ref and scope

struct S
{
    // note: in struct member functions, the implicit `this` parameter
    // is passed by `ref`

    U xerxes() return scope;        // return scope
    U sargon()        scope return; // return ref, `return` comes after `scope`
    U xerxes() return const scope;  // return ref, `return` and `scope` are not adjacent
}

return scope, return ref, scope 의미의 조합 예시:

@safe:

int* globalPtr;

struct S
{
    int  val;
    int* ptr;

    this(return scope ref int* p) { ptr = p; }

    // note: `this` is passed by `ref` in structs

    int* retRefA() scope return // return-ref, scope
    {
        globalPtr = this.ptr; // disallowed, `this` is `scope`
        return &this.val; // allowed, `return` means `return ref`
    }

    ref int retRefB() scope return // return-ref, scope
    {
        globalPtr = this.ptr; // disallowed, `this` is `scope`
        return  this.val; // allowed, `return` means `return ref`
    }

    int* retScopeA() return scope // ref, return-scope
    {
        return &this.val; // disallowed, escaping a reference to `this`
        return this.ptr;  // allowed, returning a `return scope` pointer
    }

    ref int retScopeB() return scope // ref, return-scope
    {
        return this.val;  // disallowed, escaping a reference to `this`
        return *this.ptr; // allowed, returning a `return scope` pointer
    }

    ref int* retRefScopeC() scope return // return-ref, scope
    {
        return this.ptr; // allowed, returning a reference to a scope pointer
    }
}

int* retRefA(return ref scope S s)
{
    globalPtr = s.ptr; // disallowed, `s` is `scope`
    return &s.val; // allowed, returning a reference to `return ref s`
}

ref int retRefB(return ref scope S s)
{
    globalPtr = s.ptr; // disallowed, `s` is `scope`
    return s.val;
}

int* retScopeA(ref return scope S s)
{
    return &s.val; // disallowed, escaping a reference to `s`
    return s.ptr;  // allowed, returning a `return scope` pointer
}

ref int retScopeB(ref return scope S s)
{
    return s.val;  // disallowed, escaping a reference to `s`
    return *s.ptr; // allowed, returning a `return scope` pointer
}

ref int* retRefScopeC(return ref scope int* p)
{
    return p; // allowed, returning a reference to a scope pointer
}

pure 함수에서 추론된 scope 파라미터 (Inferred scope parameters in pure functions)

파라미터가 scope로 표시되거나 추론되지 않았더라도, 함수 호출에서 그 파라미터에 scope 포인터를 할당하는 것이 여전히 @safe일 수 있어요. 다음 조건이 충족되어야 해요:

  • 함수가 pure이므로, 인자를 전역 변수에 할당할 수 없어요.
  • 함수가 nothrow이므로, 인자를 던져진 Exception 객체에 할당할 수 없어요.
  • 다른 파라미터 중 변경 가능한 간접 참조가 없으므로, 인자를 더 오래 사는 변수에 할당할 수 없어요.

그러면 함수의 반환 타입에 따라 파라미터가 여전히 scope 또는 return scope로 취급돼요:

  • 함수가 ref로 반환하거나 포인터를 포함하는 반환 타입을 가지면, 인자가 반환될 수 있으므로 return scope로 취급돼요.
  • 그렇지 않으면 인자가 함수 밖으로 벗어날 수 없으므로 scope로 취급돼요.
@safe:

int dereference(int* x) pure nothrow;
int* identity(int* x) pure nothrow;
int* identityThrow(int* x) pure;
void assignToRef(int* x, ref int* escapeHatch) pure nothrow;
void assignToPtr(int* x, int** escapeHatch) pure nothrow;
void cannotAssignTo(int* x, const ref int* noEscapeHatch) pure nothrow;

int* globalPtr;

int* test(scope int* ptr)
{
    int result = dereference(ptr); // allowed, treated as `scope`
    int* ptr2 = identity(ptr); // allowed, treated as `return scope`
    int* ptr3 = identityThrow(ptr); // not allowed, can throw an `Exception`
    assignToRef(ptr, globalPtr); // not allowed, mutable second parameter
    assignToPtr(ptr, &globalPtr); // not allowed, mutable second parameter
    cannotAssignTo(ptr, globalPtr); // allowed

    return ptr2; // not allowed, ptr2 is inferred `scope` now
}

파라미터에 대한 사용자 정의 속성 (User-Defined Attributes for Parameters)

참고: 사용자 정의 속성(User-Defined Attributes).

파라미터 언패킹 (Unpacking Parameters)

ParameterTuplePattern:
    ( ParameterTupleComponents )

ParameterTupleComponents:
    ParameterTupleComponent ,
    ParameterTupleComponent , ParameterTupleComponent
    ParameterTupleComponent , ParameterTupleComponents

ParameterTupleComponent:
    ParameterAttributesopt Identifier
    ParameterAttributesopt BasicType TypeSuffixesopt Identifier
    ParameterAttributesopt ParameterTuplePattern

타입이 없는 ParameterTupleComponents를 가진 함수 리터럴 템플릿이 지원돼요. 대응하는 함수 인자는 컴포넌트 변수들로 **언패킹(unpacking)**돼요.

import std.typecons : tuple;

alias dg = ((x, y), z) => writeln(x, " ", y, " ", z);
dg(tuple(1, 2), 3); // "1 2 3\n"

참고(Note): 타입이 있는 ParameterTupleComponent는 파싱되지만, 컴파일러가 인식하는 튜플 타입이 없으면 지원할 수 없습니다.

((x, y), z) { ... }(__arg0, z) { auto (x, y) = __arg0; ... }로 낮춰집니다(lowered). 일반적으로 낮춤은 파라미터의 저장 클래스를 보존해 함수 본문의 언패킹 선언에 복사해요.

  • ref 저장 클래스는 바깥쪽으로 복사돼요. 예를 들어 ((ref a, b), c) { ... }(ref __arg0, c) { (ref a, auto b) = __arg0; ... }로 낮춰져요.
  • out은 개별 컴포넌트가 아니라 파라미터 전체에 적용될 수 있어요.
  • auto reflazy는 언패킹 파라미터에 적용할 수 없어요.

참고(Note): out 언패킹 파라미터는 현재 구현에서 지원되지 않지만, 향후 추가될 수 있습니다.

가변 함수 (Variadic Functions)

*가변 함수(Variadic Functions)*는 가변 개수의 인자를 취해요. 세 가지 형태가 있어요:

  1. C 스타일 가변 함수
  2. 타입 정보를 가진 가변 함수
  3. 타입 안전(type-safe) 가변 함수

C 스타일 가변 함수 (C-style Variadic Functions)

C 스타일 가변 함수는 마지막 함수 파라미터로 ... 파라미터를 가지고 선언돼요. extern (C) 같은 비 D 링키지를 가져요.

extern (C) void dry(int x, int y, ...); // C-style Variadic Function

void spin()
{
    dry(3, 4);      // ok, no variadic arguments
    dry(3, 4, 6.8); // ok, one variadic argument
    //dry(2);       // error, no argument for parameter y
}

가변 인자가 아닌 파라미터가 최소 하나는 선언되어야 해요.

extern (C) int def(...); // error, must have at least one parameter

C 스타일 가변 함수는 가변 함수에 대한 C 호출 규약(calling convention)을 따르며, printf 같은 C 표준 라이브러리 함수를 호출할 수 있어요.

extern (C) int printf(const(char)*, ...);

void main()
{
    printf("hello world\n");
}

C 스타일 가변 함수는 @safe로 표시할 수 없어요.

가변 인자에 접근하려면 표준 라이브러리 모듈 core.stdc.stdarg를 import 하세요.

import core.stdc.stdarg;
import std.stdio;

extern (C) void rinse(int x, int y, ...)
{
    va_list args;
    va_start(args, y); // y is the last named parameter
    int z;
    va_arg(args, z);   // z is set to 5
    va_end(args);
    writeln(z);
}

void main()
{
    rinse(3, 4, 5);   // first variadic argument is 5
}

D 스타일 가변 함수 (D-style Variadic Functions)

D 스타일 가변 함수는 D 링키지를 가지고 마지막 파라미터로 ...를 가져요.

...는 유일한 파라미터일 수 있어요.

... 파라미터 앞에 파라미터가 있다면, ...쉼표로 구분되어야 해요.

참고(Note): 쉼표를 생략하면 타입 안전 가변 함수(Type-safe Variadic Function)가 됩니다.

int abc(char c, ...);   // one required parameter: c
int def(...);           // no required parameters
int ghi(int i ...);     // a typesafe variadic function
//int boo(, ...);       // error

함수에는 두 개의 숨겨진 인자가 전달돼요:

  • _argptr
  • TypeInfo[] _arguments

_argptr는 가변 인자 중 첫 번째를 가리키는 포인터예요. 가변 인자에 접근하려면 core.vararg를 import 하세요. va_arg와 함께 _argptr을 사용해요:

import core.vararg;
import std.stdio;

@system void foo(int x, int y, ...)
{
    int z = va_arg!int(_argptr); // z is set to 5 and _argptr is advanced
                                 // to the next argument
    writeln(z); // 5
}

void main()
{
    foo(3, 4, 5);   // first variadic argument is 5
}

_arguments는 인자의 개수와 각각의 typeid를 제공해서, 런타임에 타입 안전성을 검사할 수 있게 해줘요.

import std.stdio;

void main()
{
    Foo f = new Foo();
    Bar b = new Bar();

    writefln("%s", f);
    printargs(1, 2, 3L, 4.5, f, b);
}

class Foo { int x = 3; }
class Bar { long y = 4; }

import core.vararg;

@system void printargs(int x, ...)
{
    writefln("%d arguments", _arguments.length);
    for (int i = 0; i < _arguments.length; i++)
    {
        writeln(_arguments[i]);

        if (_arguments[i] == typeid(int))
        {
            int j = va_arg!(int)(_argptr);
            writefln("\t%d", j);
        }
        else if (_arguments[i] == typeid(long))
        {
            long j = va_arg!(long)(_argptr);
            writefln("\t%d", j);
        }
        else if (_arguments[i] == typeid(double))
        {
            double d = va_arg!(double)(_argptr);
            writefln("\t%g", d);
        }
        else if (_arguments[i] == typeid(Foo))
        {
            Foo f = va_arg!(Foo)(_argptr);
            writefln("\t%d", f.x);
        }
        else if (_arguments[i] == typeid(Bar))
        {
            Bar b = va_arg!(Bar)(_argptr);
            writefln("\t%d", b.y);
        }
        else
            assert(0);
    }
}

이 출력은 이렇게 나와요:

0x00870FE0
5 arguments
int
        2
long
        3
double
        4.5
Foo
        3
Bar
        4

D 스타일 가변 함수는(va_arg를 호출하면) @safe로 표시할 수 없어요.

타입 안전 가변 함수 (Typesafe Variadic Functions)

타입 안전 가변 함수는 D 링키지를 가지며, 보통 배열 뒤에 ...가 오는 가변 파라미터를 가져요. 가변 파라미터는 마지막이어야 해요. 컴포넌트 인자(예: 배열 요소)를 전달하면, 가변 파라미터는 주어진 인자들로 암시적으로 구성돼요.

동적 배열 가변 파라미터는 동적 배열 인자 또는 각각이 배열 요소 타입으로 암시적으로 변환되는 임의 개수의 인자를 받아들여요.

int sum(int[] ar ...) // typesafe variadic function
{
    int s;
    foreach (int x; ar)
        s += x;
    return s;
}

void main()
{
    assert(sum(1, 2, 3) == 6);
    assert(sum() == 0);

    int[3] ii = [4, 5, 6];
    assert(sum(ii) == 15);
}

정적 배열 가변 파라미터는 정적 배열 인자 또는 배열 차원과 일치하는 고정 개수의 인자를 받아들여요.

int sum(int[3] ar ...) // typesafe variadic function
{
    int s;
    foreach (int x; ar)
        s += x;
    return s;
}

void main()
{
    int i;
    //i = sum(2, 3);    // error, need 3 values for array
    i = sum(1, 2, 3);
    assert(i == 6);

    int[3] ii = [4, 5, 6];
    i = sum(ii);
    assert(i == 15);

    int[] jj = ii;
    //i = sum(jj); // error, type mismatch
}

더 이상 권장하지 않음(Deprecated): class 객체에 대해서:

int tesla(int x, C c ...)
{
    return x + c.x;
}

class C
{
    int x;
    string s;

    this(int x, string s)
    {
        this.x = x;
        this.s = s;
    }
}

void edison()
{
    C g = new C(3, "abc");
    tesla(1, c);         // ok, since c is an instance of C
    tesla(1, 4, "def");  // ok
    tesla(1, 5);         // error, no matching constructor for C
}

가변 class 객체나 배열 인스턴스의 수명은 함수가 끝날 때 끝나요.

C orville(C c ...)
{
    return c;   // error, c instance contents invalid after return
}

int[] wilbur(int[] a ...)
{
    return a;       // error, array contents invalid after return
    return a[0..1]; // error, array contents invalid after return
    return a.dup;   // ok, since copy is made
}

구현 정의(Implementation Defined): 가변 객체나 배열 인스턴스는 스택에 구성될 수 있습니다.

다른 타입의 경우, 가변 파라미터는 정확히 하나의 인자와 매칭되며 값으로 전달돼요.

int neil(int i ...)
{
    return i;
}

void buzz()
{
    neil();     // error, missing argument
    neil(3);    // returns 3
    neil(3, 4); // error, too many arguments
    int[] x;
    neil(x);    // error, type mismatch
}

Lazy 가변 함수 (Lazy Variadic Functions)

함수의 가변 파라미터가 파라미터 없는 델리게이트의 배열이면, 그 델리게이트 타입과 일치하지 않는 타입의 각 인자는 그 타입의 델리게이트로 변환돼요.

void hal(scope int delegate()[] dgs ...);

void dave()
{
    int delegate() dg;
    hal(1, 3+x, dg, cast(int delegate())null);   // (1)
    hal( { return 1; }, { return 3+x; }, dg, null ); // same as (1)
}

가변 델리게이트 배열은 lazy 가변 배열과 달라요. 후자는 배열 요소에 접근할 때마다 모든 배열 요소를 평가해요. 전자는 접근되는 요소만 평가해요.

import std.stdio;

void main()
{
    int x;
    ming(++x, ++x);

    int y;
    flash(++y, ++y);
}

// lazy variadic array
void ming(lazy int[] arr...)
{
    writeln(arr[0]); // 1
    writeln(arr[1]); // 4
}

// variadic delegate array
void flash(scope int delegate()[] arr ...)
{
    writeln(arr[0]()); // 1
    writeln(arr[1]()); // 2
}

모범 사례(Best Practices): 델리게이트 배열 파라미터를 선언할 때 scope를 사용하세요. scope는 델리게이트가 함수 밖으로 벗어나지 않는다는 뜻이므로, 델리게이트에 대한 클로저(closure)가 생성되는 것을 막아줍니다.

숨겨진 파라미터 (Hidden Parameters)

  • 비정적 멤버 함수는 모두 this 참조라는 숨겨진 파라미터를 가져요. 이는 함수가 호출된 그 객체를 가리켜요.
  • D 스타일 가변 함수는 숨겨진 파라미터를 가져요.
  • 중첩 함수는 숨겨진 컨텍스트 포인터 파라미터를 가져요.
  • Objective-C 링키지를 가진 멤버 함수는 숨겨진 파라미터를 가져요.

Ref Scope 반환 경우 (Ref Scope Return Cases)

정의 (Definitions)

용어 설명
I 간접 참조를 포함하지 않는 타입
P 간접 참조를 포함하는 타입
X 간접 참조를 포함할 수도 있고 아닐 수도 있는 타입
p P 타입의 파라미터
i I 타입의 파라미터
ref ref 또는 out 파라미터
returned return 문을 통해 반환됨
escaped 전역 변수나 함수의 스택 프레임이 아닌 다른 메모리에 저장됨

분류 (Classification)

파라미터는 다음 상태 중 하나여야 해요:

용어 설명
None p는 반환되거나 벗어날 수 있습니다.
ReturnScope p는 반환될 수 있지만 벗어날 수 없습니다.
Scope p는 반환될 수도 벗어날 수도 없습니다.
Ref p는 반환되거나 벗어날 수 있고, ref는 반환되거나 벗어날 수 없습니다.
ReturnRef p는 반환되거나 벗어날 수 있고, ref는 반환될 수 있지만 벗어날 수 없습니다.
RefScope p는 반환될 수도 벗어날 수도 없고, ref도 반환되거나 벗어날 수 없습니다.
ReturnRef-Scope p는 반환될 수도 벗어날 수도 없고, ref는 반환될 수 있지만 벗어날 수 없습니다.
Ref-ReturnScope p는 반환될 수 있지만 벗어날 수 없고, ref는 반환되거나 벗어날 수 없습니다.
ReturnRef-ReturnScope p는 반환될 수 있지만 벗어날 수 없고, ref도 반환될 수 있지만 벗어날 수 없습니다. 이는 현재 구문으로는 표현할 수 없어 허용되지 않습니다.

분류에 대한 구문 매핑 (Mapping Syntax Onto Classification)

returnscope 바로 앞에 인접해 있는 것은 ReturnScope를 의미해요. 그 외에 어떤 위치에서든 returnref가 있으면 ReturnRef를 의미해요.

예시 분류 주석
X foo(P p) None
X foo(scope P p) Scope
P foo(return scope P p) ReturnScope
I foo(return scope P p) Scope 반환 타입 I가 포인터를 포함하지 않아 return이 버려집니다.
P foo(return P p) ReturnScope scope 없이 return만 있는 건 말이 안 됩니다.
I foo(return P p) Scope 반환 타입 I가 포인터를 포함하지 않아 return이 버려집니다.
X foo(ref P p) Ref
X foo(ref scope P p) RefScope
P foo(ref return scope P p) Ref-ReturnScope
P foo(return ref scope P p) ReturnRef-Scope
I foo(ref return scope P p) RefScope
P foo(ref return P p) ReturnRef
I foo(ref return P p) Ref
ref X foo(P p) None
ref X foo(scope P p) Scope
ref X foo(return scope P p) ReturnScope
ref X foo(return P p) ReturnScope scope 없이 return만 있는 건 말이 안 됩니다.
ref X foo(ref P p) Ref
ref X foo(ref scope P p) RefScope
ref X foo(ref return scope P p) Ref-ReturnScope
ref X foo(return ref scope P p) ReturnRef-Scope
ref X foo(ref return P p) ReturnRef
X foo(I i) None
X foo(scope I i) None
X foo(return scope I i) None
X foo(return I i) None
X foo(ref I i) Ref
X foo(ref scope I i) Ref
X foo(ref return scope I i) Ref
P foo(ref return I i) ReturnRef
I foo(ref return I i) Ref
ref X foo(I i) None
ref X foo(scope I i) None
ref X foo(return scope I i) None
ref X foo(return I i) None
ref X foo(ref I i) Ref
ref X foo(ref scope I i) Ref
ref X foo(ref return scope I i) ReturnRef
ref X foo(ref return I i) ReturnRef

멤버 함수 (Member Functions)

멤버 함수는 this 파라미터를 멤버가 아닌 함수의 첫 번째 파라미터인 것처럼 다시 쓴 것으로 간주돼요.

struct S {
    X foo();
}

는 다음과 같이 취급돼요:

X foo(ref S);

그리고:

class C {
    X foo()
}

는 다음과 같이 취급돼요:

X foo(P)

P와 ref (P and ref)

규칙은 ref와 P 사이의 전환을 다뤄요. 예를 들어:

int* foo(return ref int i) { return &i; }
ref int foo(int* p) { return *p; }

반변성 (Contravariance)

반변성(contravariance)은 파라미터에 제약이 있는 함수를, 대응하는 파라미터의 제약이 더 적은 함수로 오버라이드할 수 있다는 것을 뜻해요. 이는 각 상태의 설명에서 추론할 수 있어요. 다음 표들은 어떤 속성들을 느슨하게 할 수 있는지 보여줘요.

참고(Note): ref 타입과 비-ref 타입 사이에는 변성(variance)이 없으므로, 표를 단순화하기 위해 그 항목들은 생략했습니다.

비-Ref 반변성 (Non-Ref Contravariance)

From\To None ReturnScope Scope
None
ReturnScope
Scope

Ref 반변성 (Ref Contravariance)

From\To Ref ReturnRef RefScope ReturnRef-Scope Ref-ReturnScope
Ref
ReturnRef
RefScope
ReturnRef-Scope
Ref-ReturnScope

예를 들어, scope는 모든 비-ref 파라미터로 오버라이드될 수 있고, ref scope는 모든 ref 파라미터로 오버라이드될 수 있어요:

// cannot escape parameter
void f1(scope int*);
// can escape, but doesn't have to
void function(int*) fp1 = &f1;

void f2(ref scope int*);
void function(ref int*) fp2 = &f2;

// cannot return parameter
ref int f3(ref int);
// can return parameter, but doesn't have to
ref int function(ref return int) fp3 = &f3;

로컬 변수 (Local Variables)

로컬 변수는 함수의 스코프 안에서 선언돼요. 함수 파라미터도 여기에 포함돼요.

로컬 변수는 먼저 값을 할당하지 않고는 읽을 수 없어요.

구현 정의(Implementation Defined): 구현이 항상 이러한 경우를 감지할 수 있는 것은 아닙니다.

로컬 비정적 변수의 주소나 참조는 함수에서 반환할 수 없어요.

같은 함수 안에서 로컬 변수와 레이블은 같은 이름을 가질 수 없어요.

로컬 변수는 같은 함수 안의 다른 로컬 변수를 숨길 수 없어요.

이유(Rationale): 이렇게 하는 것은 종종 버그이거나, 적어도 버그처럼 보입니다.

ref double func(int x)
{
    int x;       // error, hides previous definition of x
    double y;
    {
        char y;  // error, hides previous definition of y
        int z;
    }
    {
        wchar z; // Ok, previous z is out of scope
    }
  z:             // error, z is a local variable and a label
    return y;    // error, returning ref to local
}

로컬 정적 변수 (Local Static Variables)

static, shared static, __gshared로 선언된 로컬 변수는 스택에 할당되지 않고 정적으로 할당돼요. __gsharedshared static 변수의 수명은 함수가 처음 실행될 때 시작해 프로그램이 끝날 때 끝나요. static 변수의 수명은 스레드 안에서 함수가 처음 실행될 때 시작해 그 스레드가 종료될 때 끝나요.

import std.stdio : writeln;

void foo()
{
    static int n;
    if (++n == 100)
        writeln("called 100 times");
}

정적 변수의 초기화자(initializer)는 컴파일 타임에 평가할 수 있어야 해요. 로컬 정적 변수에는 정적 생성자나 정적 소멸자가 없어요.

정적 변수의 이름 가시성은 일반적인 스코프 규칙을 따르지만, 그 이름들은 특정 함수 안에서 고유해야 해요.

void main()
{
    { static int x; }
    { static int x; } // error
    { int i; }
    { int i; } // ok
}

중첩 함수 (Nested Functions)

함수는 다른 함수 안에 중첩될 수 있어요:

int bar(int a)
{
    int foo(int b)
    {
        int abc() { return 1; }

        return b + abc();
    }
    return foo(a);
}

void test()
{
    int i = bar(3); // i is assigned 4
}

중첩 함수는 그 이름이 스코프 안에 있을 때만 접근할 수 있어요.

void foo()
{
    void A()
    {
        B(); // error, B() is forward referenced
        C(); // error, C undefined
    }
    void B()
    {
        A(); // ok, in scope
        void C()
        {
            void D()
            {
                A();      // ok
                B();      // ok
                C();      // ok
                D();      // ok
            }
        }
    }
    A(); // ok
    B(); // ok
    C(); // error, C undefined
}

그리고:

int bar(int a)
{
    int foo(int b) { return b + 1; }
    int abc(int b) { return foo(b); }   // ok
    return foo(a);
}

void test()
{
    int i = bar(3);     // ok
    int j = bar.foo(3); // error, bar.foo not visible
}

중첩 함수는 어휘적으로 둘러싼(enclosing) 함수가 정의한 변수와 다른 심볼들에 접근할 수 있어요. 이 접근에는 읽기와 쓰기 능력이 모두 포함돼요.

int bar(int a)
{
    int c = 3;

    int foo(int b)
    {
        b += c;       // 4 is added to b
        c++;          // bar.c is now 5
        return b + c; // 12 is returned
    }
    c = 4;
    int i = foo(a); // i is set to 12
    return i + c;   // returns 17
}

void test()
{
    int i = bar(3); // i is assigned 17
}

이 접근은 여러 중첩 레벨에 걸칠 수 있어요:

int bar(int a)
{
    int c = 3;

    int foo(int b)
    {
        int abc()
        {
            return c;   // access bar.c
        }
        return b + c + abc();
    }
    return foo(3);
}

정적(static) 중첩 함수는 어휘적으로 둘러싼 함수의 스택 변수에 접근할 수 없지만, 정적 변수에는 접근할 수 있어요. 이는 정적 멤버 함수가 동작하는 방식과 유사해요.

int bar(int a)
{
    int c;
    static int d;

    static int foo(int b)
    {
        b = d;          // ok
        b = c;          // error, foo() cannot access frame of bar()
        return b + 1;
    }
    return foo(a);
}

함수는 멤버 함수 안에 중첩될 수도 있어요:

struct Foo
{
    int a;

    int bar()
    {
        int c;

        int foo()
        {
            return c + a;
        }
        return 0;
    }
}

중첩 함수는 항상 D 함수 링키지 타입을 가져요.

메서드 속성을 가진 중첩 함수 (Nested Functions with Method Attributes)

비정적 중첩 함수는 MemberFunctionAttributes를 가질 수 있어요. 메서드 속성(일반 FunctionAttributes 외의 것)은 컨텍스트 포인터에 영향을 줘요:

void test(int x, immutable int y)
{
    int foo() // hidden context pointer is mutable
    {
        x = 10;     // can access data in enclosing scope
                    // through the mutable context pointer
        return x;
    }

    int bar() immutable // hidden context pointer is immutable
    {
        //return x; // error, cannot access nested data
                    // through the immutable context pointer
        return y;   // OK, `y` is immutable
    }
}

선언 순서 (Declaration Order)

모듈 수준 선언과 달리, 함수 스코프 안의 선언은 순서대로 처리돼요. 그래서 두 중첩 함수는 서로를 상호 호출할 수 없어요:

void test()
{
    void foo() { bar(); } // error, bar not defined
    void bar() { foo(); } // ok
}

이 제약에는 몇 가지 해결 방법이 있어요:

  • 함수들을 중첩 struct의 정적 멤버로 선언하기:
void test()
{
    static struct S
    {
        static void foo() { bar(); } // ok
        static void bar() { foo(); } // ok
    }

    S.foo();  // compiles (but note the infinite runtime loop)
}
  • 함수 중 하나 이상을, 특정 템플릿 인자를 취하지 않더라도 함수 템플릿으로 선언하기:
void test()
{
    void foo()() { bar(); } // ok (foo is a function template)
    void bar()   { foo(); } // ok
}
  • 함수들을 mixin 템플릿 안에 선언하기:
mixin template T()
{
    void foo() { bar(); } // ok
    void bar() { foo(); } // ok
}

void main()
{
    mixin T!();
}
  • 델리게이트 사용하기:
void test()
{
    void delegate() fp;
    void foo() { fp(); }
    void bar() { foo(); }
    fp = &bar;
}

중첩 함수는 오버로드될 수 없어요.

함수 포인터, 델리게이트와 클로저 (Function Pointers, Delegates and Closures)

함수 포인터 (Function Pointers)

함수 포인터는 function 키워드로 선언해요:

void f(int);
void function(int) fp = &f; // fp is a pointer to a function taking an int

함수 포인터는 정적 중첩 함수를 가리킬 수 있어요:

int function() fp;  // fp is a pointer to a function returning an int

void test()
{
    static int a = 7;
    static int foo() { return a + 3; }

    fp = &foo;
}

void main()
{
    assert(!fp);
    test();
    int i = fp();
    assert(i == 10);
}

구현 정의(Implementation Defined): 본문이 동일한 두 함수, 또는 동일한 어셈블리 코드로 컴파일되는 두 함수가 서로 다른 함수 포인터 값을 가질 것이라고 보장되지는 않습니다. 구현은 동일한 코드로 컴파일되면 함수 본문들을 하나로 병합할 수 있어요.

int abc(int x)   { return x + 1; }
uint def(uint y) { return y + 1; }

int function(int)   fp1 = &abc;
uint function(uint) fp2 = &def;
// Do not rely on fp1 and fp2 being different values; the compiler may merge
// them.

델리게이트와 클로저 (Delegates & Closures)

델리게이트는 비정적 중첩 함수로 설정할 수 있어요:

int delegate() dg;

void test()
{
    int a = 7;
    int foo() { return a + 3; }

    dg = &foo;
    int i = dg(); // i is set to 10
}

void main()
{
    test();
    int i = dg(); // ok, test.a is in a closure and still exists
    assert(i == 10);
}

중첩 함수가 참조하는 스택 변수들은 함수가 끝난 뒤에도 여전히 유효해요(D 1.0과는 다르다는 점을 주의하세요). 이런 환경과 함수의 결합을 **동적 클로저(dynamic closure)**라고 불러요.

클로저를 구성하는 그 참조된 스택 변수들은 GC 힙에 할당돼요. 단, 다음 경우는 예외예요:

  • 클로저가 scope 파라미터로 전달되는 경우.
  • 클로저가 scope 변수의 초기화자인 경우.
  • 클로저가 scope 변수에 할당되는 경우.
@nogc:
void f(scope int delegate());
void g(int delegate());

void main()
{
    int i;
    int h() { return i; }
    h(); // OK
    scope x = &h; // OK
    x(); // OK
    //auto y = &h; // error, can't allocate closure in @nogc function
    f(&h); // OK
    //g(&h); // error

    // delegate literals
    f(() => i); // OK
    scope d = () => i; // OK
    d = () => i + 1; // OK
    f(d);
    //g(() => i); // error, can't allocate closure in @nogc function
}

참고(Note): 그러나 스택 변수의 주소를 반환하는 것은 클로저가 아니며 오류입니다.

메서드 델리게이트 (Method Delegates)

비정적 중첩 함수에 대한 델리게이트는 두 가지 데이터를 포함해요: 어휘적으로 둘러싼 함수의 스택 프레임에 대한 포인터(컨텍스트 포인터(context pointer)라고 불러요)와 함수의 주소. 이는 this 포인터와 멤버 함수 주소로 구성된 struct/class 비정적 멤버 함수 델리게이트와 유사해요. 두 형태의 델리게이트는 구분할 수 없으며 같은 타입이에요.

델리게이트는 &obj.method로 특정 객체와 메서드에 설정할 수 있어요:

struct Foo
{
    int a;
    int get() { return a; }
}

int add1(int delegate() dg)
{
    return dg() + 1;
}

void main()
{
    Foo f = {7};
    int delegate() dg = &f.get; // bind to an instance of Foo and a method
    int i = add1(dg);
    assert(i == 8);

    int x = 27;
    int abc() { return x; }

    i = add1(&abc);
    assert(i == 28);
}

속성 (Attributes)

함수 포인터나 델리게이트는 각각 FunctionAttributesMemberFunctionAttributes를 가질 수 있어요. 함수 포인터/델리게이트 타입은 더 느슨하게 한정된 타입으로 암시적으로 변환돼요(즉 원래 타입이 목적지 타입과 공변일 때).

void function() pure pfp;
void function() fp = pfp; // OK, impure `fp` can call pure
pfp = fp; // Error, pure `pfp` cannot call impure

초기화 (Initialization)

함수 포인터는 기본적으로 0으로 초기화돼요. 어떤 함수(함수 리터럴 포함)의 주소로 초기화할 수 있어요. 컨텍스트 포인터가 필요한 함수의 주소로 초기화하는 것은 @safe 함수에서 허용되지 않아요.

미정의 동작(Undefined Behavior): 컨텍스트 포인터가 필요한 함수를 가리키도록 설정된 함수 포인터를 호출하는 것.

struct S
{
    static int sfunc() @safe;
    int member();   // has hidden `this` reference parameter
}

@safe void sun()
{
    int function() @safe fp = &S.sfunc;
    fp(); // Ok
    fp = &S.member; // error
}

@system void moon()
{
    int function() fp = &S.member; // Ok because @system
    fp(); // undefined behavior
}

델리게이트는 기본적으로 0으로 초기화돼요. 비정적 멤버 함수의 주소를 취해 초기화할 수 있지만, 컨텍스트 포인터가 공급되어야 해요. 비정적 중첩 함수나 함수 리터럴의 주소를 취해 초기화할 수 있는데, 이때 컨텍스트 포인터는 스택 프레임, 클로저, 또는 null을 가리키도록 설정돼요.

델리게이트는 전역 함수, 정적 멤버 함수, 정적 중첩 함수의 주소를 취해서는 초기화할 수 없어요.

struct S
{
    static int sfunc();
    int member() { return 1; }
}

void main()
{
    S s;
    int delegate() dg = &s.member; // Ok, s supplies context pointer
    assert(dg() == 1);

    //dg = &S.sfunc;  // error
    //dg = &S.member; // error

    int moon() { return 2; }
    dg = &moon;     // Ok
    assert(dg() == 2);

    static int mars() { return 3; }
    //dg = &mars;     // error

    dg = () { return 4; }; // Ok
    assert(dg() == 4);
}

마지막 할당은 FunctionLiteral을 사용하며, 이는 델리게이트로 추론돼요.

참고(Note): 함수 포인터는 std.functional.toDelegate 템플릿을 통해 델리게이트 인자를 취하는 함수에 전달할 수 있습니다. 이 템플릿은 어떤 callable이든 null 컨텍스트 포인터를 가진 델리게이트로 변환해요.

익명 함수와 익명 델리게이트 (Anonymous Functions and Anonymous Delegates)

FunctionLiteral을 참고하세요.

main() 함수 (The main() Function)

콘솔 프로그램에서는 main()이 진입점(entry point) 역할을 해요. 모든 모듈 초기화자(module initializer)가 실행된 후, 그리고 유닛 테스트가 실행된 후에 호출돼요. 반환된 후에는 모든 모듈 소멸자(module destructor)가 실행돼요. main()은 다음과 같이 선언되어야 해요:

MainFunction:
    MainReturnDecl main() FunctionBody
    MainReturnDecl main(string[] Identifier) FunctionBody

MainReturnDecl:
    void
    int
    noreturn
    auto
  • mainvoid를 반환하면, OS는 성공 시 0 값을 받아요.
  • mainvoidnoreturn을 반환하면, OS는 비정상 종료(잡히지 않은 예외 같은) 시 0이 아닌 값을 받아요.
  • mainauto로 선언되면, 추론된 반환 타입은 void, int, noreturn 중 하나여야 해요.

string[] 파라미터가 선언되면, 그 파라미터는 OS가 프로그램에 전달한 인자들을 보관해요. 첫 번째 인자는 보통 실행 파일 이름이고, 그다음에 명령줄 인자들이 와요.

참고(Note): 런타임은 --DRT- 앞에 붙은 인자들을 제거할 수 있습니다.

참고(Note): 앞서 언급한 반환/파라미터 타입들은 const, immutable로 주석을 달 수 있습니다. 일치하는 기본 타입을 가진 enum으로 대체될 수도 있습니다.

main 함수는 D 링키지를 가져야 해요.

필요에 따라 @safe, @nogc, nothrow 등 속성을 추가할 수 있어요.

extern(C) main() 함수

프로그램은 표준 진입점의 대안으로 extern(C) main 함수를 정의할 수 있어요. 이 형태는 BetterC 프로그램에 필요해요.

C main 함수는 다음과 같이 선언되어야 해요:

CMainFunction:
    extern (C) MainReturnDecl main(CmainParametersopt) BlockStatement

CmainParameters:
    int Identifier, char** Identifier
    int Identifier, char** Identifier, char** Identifier

정의되면, 처음 두 파라미터는 OS가 프로그램에 전달한 인자들을 담는 C 스타일 배열(길이 + 포인터)을 나타내요. 세 번째 파라미터는 environ이라고 하는 POSIX 확장이며 현재 환경 변수에 대한 정보를 담아요.

참고(Note): D main 함수에 대해 정의된 저장 클래스/enum 면제는 C main 함수에도 적용됩니다.

이 함수는 C main 함수를 대신하며, D main 함수와 관련된 어떤 설정/정리(setup/teardown) 없이 즉시 실행돼요. 모듈 생성자, 모듈 소멸자, 유닛 테스트에 의존하는 프로그램은 적절한 런타임 함수를 사용해 (역)초기화를 수동으로 수행해야 해요.

구현 정의(Implementation Defined): Windows 시스템의 WinMain, DllMain 같은 다른 시스템별 진입점이 존재할 수 있습니다.

참고(Note): main에 대해 다른 시그니처를 요구하는 플랫폼을 대상으로 하는 프로그램은 명시적 맹글링을 가진 함수를 사용할 수 있습니다:

pragma(mangle, "main")
int myMain(int a, int b, int c)
{
    return 0;
}

함수 템플릿 (Function Templates)

함수는 템플릿의 형태로 컴파일 타임 인자를 가질 수 있어요. 함수 템플릿(function templates)을 참고하세요.

컴파일 타임 함수 실행 (Compile Time Function Execution, CTFE)

컴파일 타임 값이 필요한 컨텍스트에서는 함수를 사용해 그 값들을 계산할 수 있어요. 이를 컴파일 타임 함수 실행 또는 CTFE라고 불러요.

이런 컨텍스트들은:

  • 정적 변수나 매니페스트 상수(manifest constant)의 초기화
  • struct/class 멤버의 정적 초기화자
  • 정적 배열의 차원(dimension)
  • 템플릿 값 파라미터의 인자
  • static if
  • static foreach
  • static assert
  • mixin
  • pragma 인자
  • __traits 인자
enum eval(alias arg) = arg;

int square(int i)
{
    return i * i;
}

void main()
{
    import std.stdio;

    static j = square(3);      // CTFE
    writeln(j);
    assert(square(4) == 16);       // run time
    static assert(square(3) == 9); // CTFE
    writeln(eval!(square(5))); // CTFE
}

함수는 FunctionBody를 가져야 해요.

CTFE는 다음 제약을 받아요:

  1. 표현식은 어떤 변경 가능한 정적 변수도 참조할 수 없어요.
  2. AsmStatements는 허용되지 않아요.
  3. 비이식성(non-portable) 캐스트(예: int[]에서 float[]로)는 허용되지 않아요. 여기에는 엔디언에 의존하는 캐스트가 포함돼요. 부호 있는/부호 없는 타입 사이의 캐스트는 허용돼요.
  4. union에서 겹치는 필드의 재해석(reinterpretation)은 허용되지 않아요.
  5. 미정의 동작이 발생해서는 안 돼요.

포인터는 안전하게 사용된다면 CTFE에서 허용돼요:

  • 포인터 산술은 정적 또는 동적 배열 요소를 가리키는 포인터에만 허용돼요. 포인터는 배열의 마지막 요소 다음 첫 요소를 가리킬 수도 있지만, 그런 포인터는 역참조할 수 없어요. null이거나 배열이 아닌 것을 가리키는 포인터에 대한 포인터 산술은 허용되지 않아요.
  • 두 포인터 사이의 순서 비교(<, <=, >, >=)는 두 포인터가 같은 배열을 가리키거나, 적어도 하나의 포인터가 null일 때 허용돼요.
  • 불연속(discontiguous) 메모리 블록 사이의 포인터 비교는 불법이에요. 단, 두 개의 그런 비교를 &&||로 결합해 메모리 블록의 순서와 무관한 결과를 만드는 경우는 예외예요. 각 비교는 <, <=, >, >=로 비교되는 두 포인터 표현식으로 구성되어야 하고, 선택적으로 !로 부정될 수 있어요. 예를 들어 표현식 (p1 > q1 && p2 <= q2)p1, p2가 메모리 블록 P를 가리키는 표현식이고 q1, q2가 메모리 블록 Q를 가리키는 표현식일 때 허용돼요. PQ가 무관한 메모리 블록일 때조차요. [p1..p2][q1..q2] 안에 있으면 true, 그렇지 않으면 false를 반환해요. 비슷하게 (p1 < q1 || p2 > q2)[p1..p2][q1..q2] 밖에 있으면 true, 그렇지 않으면 false예요.
  • 모든 포인터 사이의 동등 비교(==, !=, is, !is)는 제약 없이 허용돼요.
  • 어떤 포인터든 void*로, 그리고 void*에서 원래 타입으로 캐스팅할 수 있어요. 포인터 타입과 비-포인터 타입 사이의 캐스팅은 불법이에요.

위 제약들은 실제로 평가되는 표현식에만 적용돼요. 예를 들어:

static int y = 0;

int countTen(int x)
{
    if (x > 10)
        ++y;    // access static variable
    return x;
}

static assert(countTen(6) == 6);    // OK
static assert(countTen(12) == 12);  // invalid, modifies y.

__ctfe 불리언 의사 변수는 CTFE 동안 true, 그 외에는 false로 평가돼요.

참고(Note): __ctfe는 CTFE에서 금지된 연산을 피하는 대체 실행 경로를 제공하는 데 사용할 수 있습니다. __ctfe의 모든 사용은 정적으로 평가되며 런타임 비용이 없습니다.

복구 불가능한 오류(assert 실패 같은)는 불법이에요.

구현 정의(Implementation Defined): CTFE를 통해 함수를 실행하는 것은 런타임에 실행하는 것보다 상당히 오래 걸릴 수 있습니다. 함수가 무한 루프에 빠지면 컴파일러가 멈출 수 있어요.

구현 정의(Implementation Defined): CTFE로 실행된 함수는 구현 정의 동작이 발생할 때 런타임과 다른 결과를 줄 수 있습니다.

문자열 Mixin과 컴파일 타임 함수 실행 (String Mixins and Compile Time Function Execution)

CTFE에서 실행되는 모든 함수는 런타임에도 실행 가능해야 해요. 함수의 컴파일 타임 평가는 함수를 런타임에 실행하는 것과 동등한 작업을 해요. 함수의 의미는 함수의 컴파일 타임 값에 의존할 수 없어요. 예를 들어:

int foo(string s)
{
    return mixin(s);
}

const int x = foo("1");

이것은 foo의 런타임 코드를 생성할 수 없기 때문에 불법이에요.

모범 사례(Best Practices): s가 템플릿 인자인 함수 템플릿이 이런 종류의 것을 구현하는 적절한 방법일 것입니다.

No-GC 함수 (No-GC Functions)

No-GC 함수는 @nogc 속성으로 표시된 함수예요. 이런 함수는 GC 힙에 메모리를 할당하지 않아요. No-GC 함수에서 허용되지 않는 연산은:

  1. 힙에 배열 구성하기
  2. 배열의 .length 프로퍼티에 쓰기로 배열 크기 조정하기
  3. 배열 연결(concatenation)
  4. 배열 추가(appending)
  5. 연관 배열 구성하기
  6. 연관 배열 인덱싱

이유(Rationale): 인덱싱은 지정된 키가 없으면 RangeError를 던질 수 있습니다.

  1. new로 힙에 객체 할당하기

참고(Note): 함수 스코프에서 class 타입의 new 선언은 scope 변수를 초기화하는 데 사용되면 @nogc와 호환됩니다. 그 경우 스택에 할당되기 때문입니다.

  1. @nogc가 아닌 함수 호출하기. 단, DebugCondition에 의해 제어되는 ConditionalStatement 안에서의 호출은 예외.
@nogc void foo()
{
    auto a = ['a'];    // (1) error, allocates
    a.length = 1;      // (2) error, array resizing allocates
    a = a ~ a;         // (3) error, arrays concatenation allocates
    a ~= 'c';          // (4) error, appending to arrays allocates

    auto aa = ["x":1]; // (5) error, allocates
    aa["abc"];         // (6) error, indexing may allocate and throws

    auto p = new int;  // (7) error, operator new allocates
    scope auto p = new GenericClass(); // (7) Ok
    bar();             // (8) error, bar() may allocate
    debug bar();       // (8) Ok
}
void bar() { }

No-GC 함수는 scope인 경우에만 클로저를 사용할 수 있어요. '델리게이트와 클로저'를 참고하세요.

@nogc int delegate() foo()
{
    int n;              // error, variable n cannot be allocated on heap
    return (){ return n; } // since `n` escapes `foo()`, a closure is required
}

@nogc는 함수의 타입에 영향을 줘요. @nogc 함수는 @nogc가 아닌 함수와 공변이에요.

void function() fp;
void function() @nogc gp;  // pointer to @nogc function

void foo();
@nogc void bar();

void test()
{
    fp = &foo; // same type
    fp = &bar; // ok, it's covariant
    //gp = &foo; // error, not covariant
    gp = &bar; // same type
}

모범 사례(Best Practices): @nogc로 표시된 함수는 GC 할당을 하지 않으므로 GC 컬렉션도 실행하지 않을 것입니다. 그러나 다른 스레드가 여전히 GC로 할당해 컬렉션을 촉발할 수 있어요. GC 컬렉션이 실행되는 것을 막는 권장 방법은 core.memory.GC.disable()을 호출하는 것입니다. 그러면 대응하는 core.memory.GC.enable() 호출이 있기 전까지 어떤 스레드에서도 컬렉션이 실행되지 않게 막습니다. GC.disable()이 적용되는 동안에도 GC 할당은 여전히 수행될 수 있어요.

함수 안전성 (Function Safety)

안전 함수 (Safe Functions)

안전 함수는 @safe 속성으로 표시돼요. @safe는 추론될 수 있어요. '함수 속성 추론(Function Attribute Inference)'을 참고하세요.

안전 함수는 안전한 인터페이스(safe interface)를 가져요. 구현은 @trusted 함수 호출을 제외하고 함수 본문을 안전하다고 알려진 연산으로 제한함으로써 이를 강제해야 해요.

다음 제약들이 안전 함수 안에서 컴파일러에 의해 강제돼요:

  • 포인터 타입 T에서 포인터를 가진 어떤 타입 U로의 캐스팅 없음. 다음 경우는 예외:
    • TU로 암시적으로 변환되는 경우
    • UT의 class나 interface를 구현하며, 두 타입 모두 extern(D)인 경우
    • T.opCast!U@safe인 경우
    • 두 타입 모두 동적 배열이거나, 두 타입 모두 원시 포인터이며:
      • 대상 요소 타입이 포인터 타입이 아닌 경우
      • 원본 요소 타입이 포인터 타입일 때 대상 요소 타입이 변경 가능(mutable)하지 않은 경우
      • 대상 요소 타입이 동적 배열이거나, 대상 요소 타입이 원본 요소 타입보다 크지 않은 경우
      • 어떤 원본 요소 타입 한정자(qualifier)든 대상 요소 타입 한정자로 암시적으로 변환되는 경우
      • 어느 요소 타입도 함수 타입이 아닌 경우
      • 원본 타입이 void[]일 때 대상 요소 타입이 변경 가능하지 않은 경우
      • 대상 요소 타입이 안전한 값만 저장하는 경우
      • 대상 요소 타입이 변경 가능할 때 원본 요소 타입이 안전한 값만 저장하는 경우
      • 어느 요소 타입도 불투명(opaque)하지 않은 경우
  1. 어떤 비-포인터 타입에서 포인터 타입으로의 캐스팅 없음.
  2. 포인터 산술 없음(포인터 인덱싱과 슬라이싱 포함).
  3. 배열 .ptr 프로퍼티 접근 불가.
  4. void 배열로 복사 불가.
  5. 다음에 해당하는 union 필드 접근 불가:
    • 다른 타입과 겹치는 포인터나 참조가 있는 필드
    • 다른 타입과 겹치는 불변식(invariant)이 있는 필드
    • 안전하지 않은 값이 될 수 있는 필드와 겹치는 필드
  6. 어떤 시스템 함수(System Function)도 호출 금지.
  7. class Exception에서 파생되지 않은 예외 잡기 금지.
  8. 인라인 어셈블러는 @trusted로 표시되어야 해요.
  9. (일치하는 @safe opCast를 제외하고) 다음의 명시적 캐스팅 없음:
    • 변경 가능 객체를 immutable로(기본 데이터 타입 제외)
    • immutable 객체를 변경 가능으로(기본 데이터 타입 제외)
    • 스레드 로컬 객체를 shared로
    • shared 객체를 스레드 로컬로
  10. @system 또는 __gshared 변수 접근 불가.
  11. 다음을 포함하는 타입에 void 초기화자 사용 불가:
    • 포인터/참조 타입
    • 불변식이 있는 타입
    • 안전하지 않은 값
  12. RvalueExpression 사용 불가.

참고(Note): 배열을 인덱싱하거나 슬라이싱할 때, 범위를 벗어난(bounds) 접근은 런타임 오류를 일으킵니다.

안전 함수 안에 중첩된 함수는 기본적으로 안전 함수예요.

안전 함수는 trusted 함수나 system 함수와 공변이에요.

모범 사례(Best Practices): 가능한 한 많은 함수를 @safe로 표시하세요.

안전한 외부 함수 (Safe External Functions)

외부 함수는 컴파일러가 볼 수 있는 함수 본문이 없어요:

@safe extern (C) void play();

따라서 안전성을 자동으로 검증할 수 없어요.

모범 사례(Best Practices): 기본 설정에 의존하지 말고 외부 함수에 속성을 명시적으로 설정하세요.

Trusted 함수 (Trusted Functions)

Trusted 함수는 @trusted 속성으로 표시돼요.

안전 함수처럼, trusted 함수는 안전한 인터페이스를 가져요. 안전 함수와 달리, 이는 함수 본문에 대한 제약으로 강제되지 않아요. 대신 trusted 함수의 인터페이스가 안전하다는 것을 보장하는 건 프로그래머의 책임이에요.

예시:

immutable(int)* f(int* p) @trusted
{
    version (none) p[2] = 13;
    // Invalid. p[2] is out of bounds. This line would exhibit undefined
    // behavior.

    version (none) p[1] = 13;
    // Invalid. In this program, p[1] happens to be in-bounds, so the
    // line would not exhibit undefined behavior, but a trusted function
    // is not allowed to rely on this.

    version (none) return cast(immutable) p;
    // Invalid. @safe code still has mutable access and could trigger
    // undefined behavior by overwriting the value later on.

    int* p2 = new int;
    *p2 = 42;
    return cast(immutable) p2;
    // Valid. After f returns, no mutable aliases of p2 can exist.
}

void main() @safe
{
    int[2] a = [10, 20];
    int* mp = &a[0];
    immutable(int)* ip = f(mp);
    assert(a[1] == 20); // Guaranteed. f cannot access a[1].
    assert(ip !is mp); // Guaranteed. f cannot introduce unsafe aliasing.
}
  • Trusted 함수는 safe, trusted, system 함수를 호출할 수 있어요.
  • Trusted 함수는 safe 또는 system 함수와 **반변(contravariant)**이에요.

모범 사례(Best Practices): Trusted 함수는 수동 검증이 더 쉽도록 작게 유지해야 합니다.

시스템 함수 (System Functions)

시스템 함수는 @safe@trusted로 표시되지 않고 @safe 함수 안에 중첩되지 않은 함수예요. 시스템 함수는 @system 속성으로 표시될 수 있어요. 함수가 시스템 함수라는 것이 그 함수가 실제로 안전하지 않다는 뜻은 아니고, 안전성을 수동으로 검증해야 한다는 뜻이에요.

  • 시스템 함수는 safe와 trusted 함수를 호출할 수 있어요.
  • 시스템 함수는 trusted 또는 safe 함수와 공변이 아닙니다.

모범 사례(Best Practices): 의심스러울 때, 구현이 D가 아닌 extern (C)extern (C++) 함수를 @system으로 표시하세요. D 컴파일러가 그것들을 검사할 수 없기 때문입니다. 대부분은 @safe지만 수동으로 검사해야 할 것입니다.

모범 사례(Best Practices): 시스템 함수의 수와 크기는 최소화해야 합니다. 이는 안전성을 수동 검사하는 데 필요한 작업을 최소화합니다.

안전한 인터페이스 (Safe Interfaces)

함수 호출의 인자, 어떤 컨텍스트, 그리고 접근 가능한 전역 변수들이 각각 안전한 값(safe value)과 안전한 앨리어싱(safe aliasing)을 가질 때, 다음 조건이라면 그 함수는 안전한 인터페이스를 가져요:

  1. 미정의 동작을 나타낼 수 없고, 그리고
  2. @safe 코드에서 접근할 수 있는 안전하지 않은 값을 만들 수 없고(예: 반환 값, 전역 변수, ref 파라미터를 통해), 그리고
  3. @safe 코드에서 접근할 수 있는 안전하지 않은 앨리어싱을 도입할 수 없어야 해요.

이 요구사항을 충족하는 함수는 @safe 또는 @trusted일 수 있어요. 충족하지 못하는 함수는 @system일 수밖에 없어요.

예시:

  • C의 free는 안전한 인터페이스가 없어요:
extern (C) @system void free(void* ptr);

free(p)p를 무효화해 그 값을 안전하지 않게 만들기 때문이에요. free@system일 수밖에 없어요.

  • C의 strlenmemcpy는 안전한 인터페이스가 없어요:
extern (C) @system size_t strlen(char* s);
extern (C) @system void* memcpy(void* dst, void* src, size_t nbytes);

검증되지 않은 가정에 기반해 포인터를 순회하기 때문이에요(strlens가 0으로 끝난다고 가정하고, memcpydstsrc가 가리키는 메모리 객체가 적어도 nbytes만큼 크다고 가정). 인자로 전달된 C 문자열을 순회하는 어떤 함수든 @system일 수밖에 없어요. 배열 경계를 위해 별도의 파라미터를 신뢰하는 어떤 함수든 @system일 수밖에 없어요.

  • C의 malloc은 안전한 인터페이스를 가져요:
extern (C) @trusted void* malloc(size_t sz);

어떤 입력에 대해서도 미정의 동작을 나타내지 않아요. 유효한 포인터(안전) 또는 null(역시 안전)을 반환해요. 새 할당에 대한 포인터를 반환하므로 어떤 안전하지 않은 앨리어싱도 도입할 수 없어요.

참고(Note): malloc의 구현은 대부분 @system 코드일 것입니다.

  • D 버전의 memcpy는 안전한 인터페이스를 가질 수 있어요:
@safe void memcpy(E)(E[] src, E[] dst)
{
    import std.math : min;
    foreach (i; 0 .. min(src.length, dst.length))
    {
        dst[i] = src[i];
    }
}

안전한 값에 대한 규칙이 배열의 길이가 올바르다는 것을 보장하기 때문이에요.

안전한 값 (Safe Values)

@system으로 표시된 변수나 필드는 타입에 관계없이 안전한 값을 보유하지 않아요.

bool의 경우 0과 1만 안전한 값이에요.

다른 모든 기본 데이터 타입의 경우, 모든 가능한 비트 패턴이 안전해요.

포인터는 다음 중 하나일 때 안전한 값이에요:

  1. null - 아래 참고
  2. 살아있는(live) 메모리 객체를 가리키고, 그 메모리 객체 안의 가리켜진 값이 안전한 경우

예시:

int* n = null; /* n is safe because dereferencing null must either crash
    or abort. */
int* x = cast(int*) 0xDEADBEEF; /* x is (most likely) unsafe because it
    is not a valid pointer and cannot be dereferenced. */

import core.stdc.stdlib: malloc, free;
int* p1 = cast(int*) malloc(int.sizeof); /* p1 is safe because the
    pointer is valid and *p1 is safe regardless of its actual value. */
free(p1); /* This makes p1 unsafe. */
int** p2 = &p1; /* While it can be dereferenced, p2 is unsafe because p1
    is unsafe. */
p1 = null; /* This makes p1 and p2 safe. */

동적 배열은 다음일 때 안전해요:

  1. 그 포인터가 안전하고,
  2. 그 길이가 대응하는 메모리 객체와 관련해 경계 안(in-bounds)에 있고,
  3. 모든 요소가 안전한 경우

예시:

int[] f() @system
{
    bool b = true; /* b is initialized safe */
    *(cast(ubyte*) &b) = 0xAA; /* b is now unsafe because it's not 0 or 1 */
    int[3] a;
    int[] d1 = a[0 .. 3]; /* d1 is safe. */
    int[] d2 = a.ptr[0 .. 4]; /* d2 is unsafe because it goes beyond a's
        bounds. */
    int*[] d3 = [cast(int*) 0xDEADBEEF]; /* d3 is unsafe because the
        element is unsafe. */
    return d1; /* Up to here, d1 was safe, but its pointer becomes
        invalid when the function returns, so the returned dynamic array
        is unsafe. */
}

정적 배열은 모든 요소가 안전할 때 안전해요. 요소 타입과 무관하게, 길이가 0인 정적 배열은 항상 안전해요.

연관 배열은 모든 키와 요소가 안전할 때 안전해요.

struct/union 인스턴스는 다음일 때 안전해요:

  1. 접근 가능한 필드의 값들이 안전하고,
  2. union과 안전하지 않은 앨리어싱을 도입하지 않는 경우

예시:

void fun()
{
    struct S { int* p; }
    S s1 = S(new int); /* s1 is safe. */
    S s2 = S(cast(int*) 0xDEADBEEF); /* s2 is unsafe, because s2.p is
        unsafe. */

    union U { int* p; size_t x; }
    U u = U(new int); /* Even though both u.p and u.x are safe, u is unsafe
        because of unsafe aliasing. */
}

class 참조는 null이거나 다음일 때 안전해요:

  1. 클래스 타입 또는 클래스 타입에서 파생된 타입의 유효한 class 인스턴스를 가리키고,
  2. 인스턴스의 접근 가능한 필드의 값들이 안전하고,
  3. union과 안전하지 않은 앨리어싱을 도입하지 않는 경우

함수 포인터는 null이거나, 같은 시그니처 또는 공변 시그니처의 유효한 함수를 가리킬 때 안전해요.

delegate는 다음일 때 안전해요:

  1. .funcptr 프로퍼티가 null이거나 델리게이트 타입과 일치하거나 공변인 함수를 가리키고,
  2. .ptr 프로퍼티가 null이거나 함수가 기대하는 형태의 메모리 객체를 가리키는 경우

Null 역참조 (Null Dereferences)

@safe 코드를 생성할 때, 준수하는 구현은:

  • null 역참조가 발생하지 않을 것이라고 가정해서는 안 됩니다. 그 가정을 요구하는 최적화는 @safe 함수에 사용될 수 없어요.
  • 다음을 감지하고 실행을 중단(abort)하는 코드를 생성해야 해요:
    • 기본적으로 메모리 주소의 첫 페이지에 대한 접근을 보호하지 않는 시스템에서 null 역참조가 발생할 때. (예를 들어 mprotect를 호출하는 것은 @system이라는 점에 주의하세요.)
    • 모든 시스템에서 null을 가리키는 포인터를 인덱싱하는 표현식이 시스템이 막지 않는 메모리 접근을 일으킬 때. 경고: dmd는 아직 이를 구현하지 않았어요.

안전한 앨리어싱 (Safe Aliasing)

하나의 메모리 위치가 두 개의 서로 다른 타입으로 접근 가능할 때, 다음 경우 그 앨리어싱은 안전하다고 간주돼요:

  1. 두 타입 모두 const 또는 immutable인 경우; 또는
  2. 한 타입이 변경 가능하고 다른 타입이 const-한정된 기본 데이터 타입인 경우; 또는
  3. 두 타입 모두 변경 가능한 기본 데이터 타입인 경우; 또는
  4. 한 타입이 길이가 0인 정적 배열 타입인 경우; 또는
  5. 한 타입이 0이 아닌 길이의 정적 배열 타입이고, 배열의 요소 타입과 다른 타입의 앨리어싱이 안전한 경우; 또는
  6. 두 타입 모두 포인터 타입이고, 대상 타입들의 앨리어싱이 안전하며 대상 타입들의 크기가 같은 경우

그 외의 모든 앨리어싱 경우는 안전하지 않은 것으로 간주돼요.

참고(Note): 안전한 앨리어싱은 보장된 안전성에 영향을 주지 않고 안전한 인터페이스를 가진 함수에 노출될 수 있습니다. 안전하지 않은 앨리어싱은 안전성을 보장하지 않습니다.

참고(Note): 안전한 앨리어싱이 데이터의 모든 앨리어싱된 뷰가 안전한 값을 가진다는 것을 의미하지는 않습니다. 그것들은 안전성에 대해 별도로 검사되어야 해요.

예시:

void f1(ref ubyte x, ref float y) @safe { x = 0; y = float.init; }
union U1 { ubyte x; float y; } // safe aliasing

void test1()
{
    U1 u1;
    f1(u1.x, u1.y); // Ok
}

void f2(ref int* x, ref int y) @trusted { x = new int; y = 0xDEADBEEF; }
union U2 { int* x; int y; } // unsafe aliasing

void test2()
{
    U2 u2;
    version (none) f1(u2.x, u2.y); // not safe
}

함수 속성 추론 (Function Attribute Inference)

FunctionLiterals, Auto 함수, Auto Ref 함수, 중첩 함수, 함수 템플릿은 함수 본문이 항상 존재하므로, 특별히 오버라이드되지 않으면 다음 속성들을 추론해요:

  • pure
  • nothrow
  • @safe
  • @nogc

마찬가지로 다음 파라미터 속성들이 추론돼요:

  • return ref
  • scope
  • return scope
  • ref return scope

속성 추론은 함수 본문이 존재하더라도 다른 함수에는 수행되지 않아요.

추론은 함수 본문이 특정 속성의 규칙을 따르는지 결정함으로써 이루어져요.

순환 함수(즉 직접 또는 간접적으로 자기 자신을 호출하는 함수)는 impure, throwing, @system으로 추론돼요.

함수가 자기 자신에 대해 그 속성들을 시험하려 하면, 그 함수는 그 속성들을 갖지 않는 것으로 추론돼요.

이유(Rationale): 함수 속성 추론은 특히 템플릿에서 사용자가 함수에 속성을 추가할 필요를 크게 줄여줍니다.

균일 함수 호출 구문 (Uniform Function Call Syntax, UFCS)

자유 함수(free function)는 다음 두 조건이 모두 성립할 때 멤버 함수처럼 호출될 수 있어요:

  • 그 객체 표현식에 대해 멤버 함수가 존재하지 않거나 존재할 수 없는 경우
  • 자유 함수의 첫 번째 파라미터 타입이 객체 표현식과 일치하는 경우

객체 표현식은 어떤 타입이든 될 수 있어요. 이를 UFCS 함수 호출이라고 불러요.

void sun(T, int);

void moon(T t)
{
    t.sun(1);
    // If `T` does not have a member function `sun`,
    // `t.sun(1)` is interpreted as if it were written `sun(t, 1)`
}

이유(Rationale): 이것은 클래스에 외부 함수를 마치 public final 멤버 함수인 것처럼 추가하는 방법을 제공합니다. 이를 통해 클래스의 함수 수를 객체의 private 상태를 다루는 데 필요한 본질적인 것만으로 최소화할 수 있고, 부엌 싱크대(kitchen-sink)만큼 많은 멤버 함수를 추가하고 싶은 유혹을 피할 수 있어요. 또한 함수 체이닝(function chaining)과 컴포넌트 프로그래밍을 가능하게 해줍니다.

이 일반적인 아이디어는 다른 프로그래밍 언어에서 다른 이름을 가져요. 짧은 목록은 다음과 같아요:

  • C#: 확장 메서드(Extension methods)
  • Kotlin: 확장 함수(Extension functions)
  • Zig: 점 표기법(Dot notation)

더 복잡한 예시:

stdin.byLine(KeepTerminator.yes)
    .map!(a => a.idup)
    .array
    .sort
    .copy(stdout.lockingTextWriter());

은 다음의 동등한 표현이에요:

copy(sort(array(map!(a => a.idup)(byLine(stdin, KeepTerminator.yes)))), lockingTextWriter(stdout));

UFCS는 @property 함수와 함께 동작해요:

@property prop(X thisObj);
@property prop(X thisObj, int value);

X obj;
obj.prop;      // if X does not have member prop, reinterpret as prop(obj);
obj.prop = 1;  // similarly, reinterpret as prop(obj, 1);

로컬 스코프에서 선언된 함수는 일치하는 UFCS 함수를 검색할 때 발견되지 않아요. 다른 로컬 심볼들도 마찬가지지만, 로컬 import는 검색돼요:

module a;

void foo(X);
alias boo = foo;

void main()
{
    void bar(X);     // bar declared in local scope
    import b : baz;  // void baz(X);

    X obj;
    obj.foo();    // OK, calls a.foo;
    //obj.bar();  // NG, UFCS does not see nested functions
    obj.baz();    // OK, calls b.baz, because it is declared at the
                  // top level scope of module b

    import b : boo = baz;
    obj.boo();    // OK, calls aliased b.baz instead of a.boo (== a.foo),
                  // because the declared alias name 'boo' in local scope
                  // overrides module scope name
}

멤버 함수도 일치하는 UFCS 함수를 검색할 때 발견되지 않아요.

class C
{
    void mfoo(X);           // member function
    static void sbar(X);    // static member function
    import b : ibaz = baz;  // void baz(X);

    void test()
    {
        X obj;
        //obj.mfoo();  // NG, UFCS does not see member functions
        //obj.sbar();  // NG, UFCS does not see static member functions
        obj.ibaz();    // OK, ibaz is an alias of baz which is declared at
                       //     the top level scope of module b
    }
}

그 외에는 UFCS 함수 검색이 보통대로 진행돼요.

이유(Rationale): 예상치 못한 이름 충돌을 피하기 위해 UFCS는 로컬 함수 심볼을 고려하지 않습니다. 아래의 문제 예시를 참고하세요.

int front(int[] arr) { return arr[0]; }

void main()
{
    int[] a = [1,2,3];
    auto x = a.front();   // call .front by UFCS

    auto front = x;       // front is now a variable
    auto y = a.front();   // Error, front is not a function
}

class C
{
    int[] arr;
    int front()
    {
        return arr.front(); // Error, C.front is not callable
                            // using argument types (int[])
    }
}

더 알아보기 (Learn more)