함수

함수 (Functions)

함수(function)는 재사용 가능한 연산을 정의하는 D의 핵심 구조예요. 본문 있는 정의, 프로토타입, 계약(contract), 순수 함수, 속성, 매개변수 저장 클래스, 가변 인자, 중첩 함수, 함수 포인터·delegate, 안전성(safety)을 다루는 페이지예요.

출처: Functions

본문

함수 선언 (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

함수 본문이 있는 FuncDeclaration을 함수 정의라고 불러요.

함수 본문이 없는 FuncDeclaration을 함수 프로토타입이라고 불러요.

TemplateParameters가 있는 FuncDeclaration은 함수 템플릿을 정의해요.

함수 매개변수 (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

참고: D2에서 매개변수를 final로 선언하는 것은 의미 오류(semantic error)이며, 파싱 오류는 아니에요.

참고: 매개변수 저장 클래스 참조.

함수 속성 (Function Attributes)

FunctionAttributes:
    FunctionAttribute
    FunctionAttribute FunctionAttributes

FunctionAttribute:
    FunctionAttributeKwd
    Property
    AtAttribute

MemberFunctionAttributes:
    MemberFunctionAttribute
    MemberFunctionAttribute MemberFunctionAttributes

MemberFunctionAttribute:
    const
    immutable
    inout
    return refopt
    scope
    shared
    FunctionAttribute

FunctionAttributes는 Attribute가 선언에 앞서는 것과 달리, FuncDeclaration이나 함수 포인터에서 함수 매개변수 뒤에 와요.

생성자, 멤버 함수, delegate, 중첩 함수는 FunctionAttributes를 확장한 MemberFunctionAttributes를 가질 수 있어요.

많은 함수/메서드 속성은 유효한 선언 속성이에요. 단, TypeCtor 속성은 다른 속성 위치에 따라 의미가 다르다는 점에 주의하세요. 한정된 타입을 반환하는 메서드 참조.

일부 함수 속성은 추론될 수 있어요.

일부 함수 속성 검사는 debug 문 안에서 꺼져요.

참고: 함수 속성, OOP 속성.

함수 본문 (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로 선언되지 않았다면, 구현이 다른 곳에 있으리라 기대되고 그 구현은 링크 단계에서 제공돼요. 이것은 함수의 구현을 사용자에게 완전히 숨길 수 있게 하며, 구현이 C, 어셈블러 등 다른 언어일 수 있어요. 보통 함수 프로토타입은 extern(D)가 아닌 링키지를 가져요.

함수 프로토타입은 extern(D) 링키지를 가질 수 있어요. 이것은 D 인터페이스 파일에 유용해요.

함수 계약 (Function Contracts)

FunctionContracts:
    FunctionContract
    FunctionContract FunctionContracts

FunctionContract:
    InOutContractExpression
    InOutStatement

InOutContractExpressions:
    InOutContractExpression
    InOutContractExpression InOutContractExpressions

InOutContractExpression:
    InContractExpression
    OutContractExpression

InOutStatement:
    InStatement
    OutStatement

함수 계약은 함수의 선행 조건(precondition)과 후행 조건(postcondition)을 지정해요. 계약 프로그래밍에서 쓰여요.

선행·후행 조건은 함수의 타입에 영향을 주지 않아요.

선행 조건 (Preconditions)

InContractExpression:
    in ( AssertArguments )

InStatement:
    in BlockStatement

InContractExpression은 선행 조건이에요.

AssertArguments의 첫 번째 AssignExpression은 true로 평가되어야 해요. 아니면 선행 조건이 실패한 거예요.

두 번째 AssignExpression은, 있으면, 타입 const(char)[]로 암시적으로 변환될 수 있어야 해요.

InStatement도 선행 조건이에요. InStatement에 나타나는 모든 AssertExpression은 InContractExpression이 돼요.

선행 조건은 의미상 함수 실행 시작 전에 성립해야 해요. 선행 조건이 실패하면 프로그램은 Invalid State에 들어가요.

구현 정의(Implementation Defined): 선행 조건이 실제로 실행되는지 여부는 구현 정의예요. 보통 컴파일러 스위치로 선택할 수 있어요. 선행 조건 실패 시 동작도 보통 컴파일러 스위치로 선택할 수 있어요. 한 옵션은 선택적 두 번째 AssignExpression으로 구성된 메시지와 함께 AssertError를 던지는 거예요.

Best Practices: 선행 조건을 사용해 입력 인자가 함수가 기대하는 값을 가졌는지 검증하세요.

Best Practices: 선행 조건은 런타임에 실제로 검사될 수도 있고 아닐 수도 있으므로, 부수 효과가 있는 선행 조건을 쓰지 마세요.

식 형태는:

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의 첫 번째 AssignExpression은 true로 평가되어야 해요. 아니면 후행 조건이 실패한 거예요.

두 번째 AssignExpression은, 있으면, 타입 const(char)[]로 암시적으로 변환될 수 있어야 해요.

OutStatement도 후행 조건이에요. OutStatement에 나타나는 모든 AssertExpression은 OutContractExpression이 돼요.

후행 조건은 의미상 함수 실행 완료 후에 성립해야 해요. 후행 조건이 실패하면 프로그램은 Invalid State에 들어가요.

구현 정의(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...
}

두 종류의 후행 조건에서 선택적 identifier는 함수의 반환 값으로 설정되며 후행 조건 안에서 접근할 수 있어요. 그것은 암시적으로 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)

파생 클래스의 함수가 수퍼 클래스의 함수를 오버라이드하면, 그 함수와 오버라이드된 함수들의 선행 조건 중 하나만 성립하면 돼요. 오버라이딩 함수는 곧 선행 조건을 느슨하게 만드는 과정이 돼요.

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 "";
    }
}

반대로, 그 함수와 오버라이드된 함수들의 모든 후행 조건이 성립해야 해요. 오버라이딩 함수는 곧 후행 조건을 조이는 과정이 돼요.

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의 식을 평가함
  • 경로가 인라인 어셈블러 코드를 포함함

위 첫 3가지 경우의 단순한 형태는 컴파일러가 인식할 수 있지만, 전부는 아니에요.

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 속성으로 주석 처리돼요. 순수 함수는 전역 또는 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
}

순수 함수는 비순수 함수를 오버라이드할 수 있지만, 비순수 함수에 의해 오버라이드될 수는 없어요. 즉 비순수 함수와 공변(covariant)이에요.

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

약하게 순수한 함수는 변경 가능한 간접 참조(mutable indirections)를 가진 매개변수를 가져요. 프로그램 상태는 그 일치하는 인자를 통해 전이적으로 수정될 수 있어요.

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

강하게 순수한 함수는 변경 가능한 간접 참조를 가진 매개변수가 없고 함수 외부의 프로그램 상태를 수정할 수 없어요.

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)

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

  • 부동소수점 예외 플래그를 읽고 쓰기
  • 부동소수점 모드 플래그를 읽고 쓰기(단, 그 플래그가 함수 진입 시 초기 상태로 복원되는 한)

미정의 동작(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 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);

참고: 중첩 함수를 한정하는 것은 immutable로, 그 컨텍스트 포인터를 통한 변경 가능 데이터 접근을 막아요. immutable 순수 중첩 함수는 강하게 순수해요.

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

순수 팩토리 함수는 변경 가능한 간접 참조만 가진 결과를 반환하는 강하게 순수한 함수예요. 그 호출로 반환된 모든 변경 가능 메모리는 프로그램의 다른 어떤 부분에서도 참조될 수 없어요. 즉 그 함수가 새로 할당한 것이에요. 결과의 변경 가능 참조들도 마찬가지로 함수 호출 전에 존재했던 어떤 객체도 참조할 수 없어요. 이것은 결과를 어떤 것이든 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): 구현은 immutable 간접 참조만(또는 없이) 가진 인자로 호출되고 변경 가능 간접 참조 없이 결과를 반환하는 강하게 순수한 함수가, 동등한 인자로 호출될 때 모든 호출에 같은 효과가 있을 것이라 가정할 수 있어요. 동등한 인자가 항상 동등한 결과를 만든다는 가정 아래 함수의 결과를 메모이즈(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`
}

그런 함수는 예를 들어 캐스트를 사용하거나 매개변수의 주소에 따라 동작을 바꾸는 등 메모이제이션과 일치하지 않는 동작을 할 수 있어요. 현재 구현이 모든 경우에 메모이제이션의 유효성을 강제해야 하는 것은 아니에요.

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

순수 소멸자는 특별한 elision 혜택이 없어요.

Nothrow 함수 (Nothrow Functions)

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

Nothrow 함수는 던지는 함수와 공변이에요.

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 함수는 함수 본문의 어떤 ReturnStatement로부터 반환 타입이 추론돼요.

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

참고: 반환 타입 추론은 속성 추론도 촉발해요.

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-ness는 함수 본문의 모든 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 타입 한정자 참조.

선택적 괄호 (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
}

모호성 때문에 delegate나 함수 포인터를 호출하려면 괄호가 필요해요:

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
}

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

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): 속성 함수의 정의와 유용성은 검토 중이며 구현이 현재 불완전해요. 정의가 더 확실해지고 구현이 더 성숙해질 때까지 속성 함수를 쓰는 것은 권장되지 않아요.

속성(property)은 필드나 변수인 것처럼 문법적으로 취급될 수 있는 함수예요. 속성은 읽거나 쓸 수 있어요. 속성은 인자가 없는 메서드/함수를 호출하여 읽고, 값으로 설정되는 인자를 가진 메서드/함수를 호출하여 씁니다.

간단한 getter·setter 속성은 UFCS로 쓸 수 있어요. 이것들은 함수에 @property 속성을 추가하여 강화될 수 있는데, 다음 동작이 추가돼요:

  • @property 함수는 같은 이름의 비-@property 함수와 오버로드될 수 없어요.
  • @property 함수는 0개, 1개 또는 2개의 매개변수만 가질 수 있어요.
  • @property 함수는 가변 매개변수를 가질 수 없어요.
  • exp가 @property 함수인 typeof(exp) 식에 대해, 타입은 함수의 타입이 아니라 함수의 반환 타입이에요.
  • exp가 @property 함수인 __traits(compiles, exp) 식에 대해, 함수가 호출될 수 있는지 추가 검사가 이루어져요.
  • @property는 다르게 맹글링(mangle)되므로 @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;
}

읽기 메서드가 없으면 속성은 쓰기 전용이에요. 쓰기 메서드가 없으면 속성은 읽기 전용이에요. 여러 쓰기 메서드가 존재할 수 있고, 올바른 것은 보통의 함수 오버로딩 규칙으로 선택돼요.

다른 모든 면에서 이 메서드들은 다른 메서드와 같아요. 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)

오버라이딩 함수는 오버라이드된 함수와 공변일 수 있어요. 공변 함수는 오버라이드된 함수의 타입으로 암시적으로 변환될 수 있는 타입을 가져요.

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)

오버로드 해석을 할 때, 기본 클래스의 함수들은 같은 오버로드 집합에 없으므로 고려되지 않아요:

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): 컴파일을 중단하거나 폐기 메시지를 출력하려면 구현이 호출 대상(target)을 판단할 수 있어야 하는데, 가상일 때는 그것을 보장할 수 없어요.

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)

함수 오버로딩은 같은 스코프의 둘 이상의 함수가 같은 이름을 가질 때 발생해요. 선택되는 함수는 인자에 가장 잘 맞는 것이에요. 일치 수준은:

  • 일치 없음
  • 암시적 변환이 있는 일치
  • 한정자 변환이 있는 일치(인자 타입이 매개변수 타입으로 한정자 변환 가능할 때)
  • 정확한 일치

명명된 인자는 후보에 대해 인자를 매개변수에 일치시키기에 따라 해결돼요. 이것이 실패하면(예: 오버로드에 명명된 인자와 일치하는 매개변수가 없기 때문에) 수준은 일치 없음이에요. 그 외에는 명명된 인자가 일치 수준에 영향을 주지 않아요.

각 인자(모든 this 참조 포함)는 함수의 해당 매개변수와 비교되어 그 인자의 일치 수준을 결정해요. 함수의 일치 수준은 각 인자의 최악의 일치 수준이에요.

리터럴은 ref나 out 매개변수와 일치하지 않아요.

scope 매개변수 저장 클래스는 함수 오버로딩에 영향을 주지 않아요.

둘 이상의 함수가 같은 일치 수준이면 부분 순서(partial ordering)로 모호함을 해소해 가장 좋은 일치를 찾아요. 부분 순서는 가장 특수화된 함수를 찾아요. 어떤 함수도 다른 쪽보다 더 특수화되지 않으면 모호성 오류예요. 부분 순서는 함수 f와 g에 대해 f의 매개변수 타입을 취하고, 그 타입들의 기본 값으로 인자 목록을 구성한 다음, 그것들을 g에 대해 일치시켜 보는 것으로 결정돼요. 성공하면 g는 f만큼 이상 특수화된 거예요. 예:

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)
}

가변 인자를 가진 함수는 가변 인자가 없는 함수보다 덜 특수화된 것으로 간주돼요.

static 멤버 함수는 멤버 함수와 오버로드될 수 있어요. static 멤버 함수의 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 매개변수가 필요 없는 static 멤버 함수는 그것을 전달할 필요가 없어요.

오버로드 집합 (Overload Sets)

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

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예요. 매개변수는 타입 생성자 const, immutable, shared, inout도 가질 수 있어요.

in, out, ref, lazy는 상호 배타적이에요. 처음 세 개는 각각 입력, 출력, 입출력 매개변수를 나타내는 데 쓰여요. 예:

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에 바인딩돼요.

매개변수 저장 클래스 및 타입 생성자 개요:

Storage Class Description
none The parameter will be a mutable copy of its argument.
in The parameter is an input to the function.
out The argument must be an lvalue, which will be passed by reference and initialized upon function entry with the default value (T.init) of its type.
ref The parameter is an input/output parameter, passed by reference.
scope The parameter must not escape the function call (e.g. by being assigned to a global variable). Ignored for any parameter that is not a reference type.
return Parameter may be returned or copied to the first parameter, but otherwise does not escape from the function. Such copies are required not to outlive the argument(s) they were derived from. Ignored for parameters with no references. See scope parameters.
lazy argument is evaluated by the called function and not by the caller
Type Constructor Description
const argument is implicitly converted to a const type
immutable argument is implicitly converted to an immutable type
shared argument is implicitly converted to a shared type
inout argument is implicitly converted to an inout type

in 매개변수 (In Parameters)

매개변수는 함수에 대한 입력이에요. 입력 매개변수는 const 저장 클래스를 가진 것처럼 동작해요.

참고: 다음은 -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로 표시된 경우)은 항상 참조로 전달돼요.

동적 배열, 클래스, 연관 배열, 함수 포인터, delegate는 항상 값으로 전달돼요.

구현 정의(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 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);
}

항상 참조로 전달되는 동적 배열과 클래스 객체 매개변수에 대해, out과 ref는 내용이 아니라 참조에만 적용돼요.

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)
{
    while (n--)
        exp();
}

콘솔에 이렇게 출력해요:

0
1
2
−
3

lazy 매개변수는 lvalue일 수 없어요.

lazy 매개변수의 기본 delegate는 & 연산자로 추출될 수 있어요:

void test(lazy int dg)
{
    int delegate() dg_ = &dg;
    assert(dg_() == 7);
    assert(dg == dg_());
}

void main()
{
    int a = 7;
    test(a);
}

타입 void의 lazy 매개변수는 어떤 타입의 인자도 받을 수 있어요.

참고: Lazy 가변 함수.

기본 인자 (Default Arguments)

함수 매개변수 선언은 기본 값을 가질 수 있어요:

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

기본 매개변수는 함수 선언의 문맥에서 해결되고 의미적으로 검사돼요.

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 매개변수는 수명 검사를 위해 첫 번째 매개변수에 배정된 것으로 간주돼요.

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 비정적 메서드는 return 속성으로 표시되어 반환된 참조가 struct 인스턴스보다 오래 살지 않도록 보장할 수 있어요. 이것은 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 이스케이프 분석은 @safe 함수에 대해서만 수행돼요. 다른 함수의 경우 scope 의미를 수동으로 강제해야 해요.

참고: @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 매개변수에 대한 고유한 인자를 힙 할당하는 것을 피할 수 있어요. 이 때문에 배열 리터럴, delegate 리터럴 또는 NewExpression을 scope 매개변수에 전달하는 것이 @nogc 문맥에서 허용될 수 있어요.

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
}

클래스 참조는 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는 클래스·인터페이스 멤버 함수의 this에 적용될 수 있어요.

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

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

Ref Return Scope 매개변수 (Ref Return Scope Parameters)

같은 매개변수에 return ref와 return scope 의미를 모두 가질 수는 없어요. 매개변수가 ref로 전달되고 return과 scope 저장 클래스를 모두 가질 때, return과 scope 키워드가 그 순서로 인접하게 나타나면 return scope 의미를 얻어요. return ref와 scope 매개변수를 지정하면 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
}

순수 함수의 추론된 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)

참고: 사용자 정의 속성.

매개변수 언패킹 (Unpacking Parameters)

ParameterTuplePattern:
    ( ParameterTupleComponents )

ParameterTupleComponents:
    ParameterTupleComponent ,
    ParameterTupleComponent , ParameterTupleComponent
    ParameterTupleComponent , ParameterTupleComponents

ParameterTupleComponent:
    ParameterAttributesopt Identifier
    ParameterAttributesopt BasicType TypeSuffixesopt Identifier
    ParameterAttributesopt ParameterTuplePattern

타입 없는 ParameterTupleComponents를 가진 함수 리터럴 템플릿이 지원돼요. 해당 함수 인자는 구성 요소 변수들로 언패킹돼요:

import std.typecons : tuple;

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

참고: 타입 있는 ParameterTupleComponent는 파싱되지만, 컴파일러가 인식하는 튜플 타입이 없으면 지원될 수 없어요.

((x, y), z) { ... }(__arg0, z) { auto (x, y) = __arg0; ... }로 낮춰져요. 일반적으로 낮춤은 매개변수의 저장 클래스를 함수 본문의 언패킹 선언으로 보존·복사해요.

ref 저장 클래스는 바깥쪽으로 복사돼요. 예: ((ref a, b), c) { ... }(ref __arg0, c) { (ref a, auto b) = __arg0; ... }로 낮춰져요.

out은 전체 매개변수에 적용될 수 있지만 개별 구성 요소에는 적용될 수 없어요.

auto reflazy는 언패킹 매개변수에 적용될 수 없어요.

참고: out 언패킹 매개변수는 현재 구현에서 지원되지 않지만, 미래에 추가될 수 있어요.

가변 인자 함수 (Variadic Functions)

가변 인자 함수는 가변적인 개수의 인자를 취해요. 세 가지 형태가 있어요:

C-style 가변 인자 함수 (C-style Variadic Functions)

C-style 가변 인자 함수는 마지막 함수 매개변수로 매개변수 ...와 함께 선언돼요. 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-style 가변 인자 함수는 가변 인자 함수의 C 호출 규약과 일치하며, printf 같은 C 표준 라이브러리 함수를 호출할 수 있어요.

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

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

C-style 가변 인자 함수는 @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-style 가변 인자 함수 (D-style Variadic Functions)

D-style 가변 인자 함수는 D 링키지와 마지막 매개변수 ...를 가져요.

...는 유일한 매개변수일 수 있어요.

... 매개변수 앞에 매개변수가 있으면 그것들과 ...를 콤마로 구분해야 해요.

참고: 콤마가 생략되면 타입 안전 가변 인자 함수예요.

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하세요. _argptrva_arg와 함께 사용하세요:

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-style 가변 인자 함수는 (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);
}

static 배열 가변 매개변수는 static 배열 인자 또는 배열 차원과 일치하는 고정 개수의 인자를 받아요:

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: 클래스 객체에 대해:

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
}

가변 클래스 객체나 배열 인스턴스의 수명은 함수의 끝에서 끝나요.

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)

함수의 가변 매개변수가 매개변수 없는 delegate의 배열이면, 그 delegate의 타입과 일치하지 않는 각 인자는 그 타입의 delegate로 변환돼요.

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)
}

가변 delegate 배열은 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: delegate 배열 매개변수를 선언할 때 scope를 사용하세요. scope는 delegate가 함수를 벗어나지 않음을 의미하므로 delegate에 대한 클로저가 생성되는 것을 막아요.

숨은 매개변수 (Hidden Parameters)

비정적 멤버 함수는 모두 this 참조라는 숨은 매개변수를 가지는데, 그것은 그 함수가 호출된 객체를 참조해요.

D-style 가변 인자 함수는 숨은 매개변수를 가져요.

중첩 함수는 숨은 컨텍스트 포인터 매개변수를 가져요.

Objective-C 링키지를 가진 멤버 함수는 숨은 매개변수를 가져요.

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

정의 (Definitions)

Term Description
I type that contains no indirections
P type that contains indirections
X type that may or may not contain indirections
p parameter of type P
i parameter of type I
ref ref or out parameter
returned returned via the return statement
escaped stored in a global or other memory not in the function’s stack frame

분류 (Classification)

매개변수는 다음 상태 중 하나에 있어야 해요:

Term Description
None p may be returned or escaped
ReturnScope p may be returned but not escaped
Scope p may be neither returned nor escaped
Ref p may be returned or escaped,
ref may not be returned nor escaped
ReturnRef p may be returned or escaped,
ref may be returned but not escaped
RefScope p may be neither returned nor escaped,
ref may not be returned nor escaped
ReturnRef-Scope p may be neither returned nor escaped,
ref may be returned but not escaped
Ref-ReturnScope p may be returned but not escaped,
ref may not be returned nor escaped
ReturnRef-ReturnScope p may be returned but not escaped,
ref may be returned but not escaped.
This isn't expressible with the current syntax
and so is not allowed.

문법을 분류에 매핑하기 (Mapping Syntax Onto Classification)

returnscope 바로 앞에 나란히 붙는 것은 ReturnScope를 의미해요. 그렇지 않으면 어떤 위치의 returnref는 ReturnRef를 의미해요.

Example Classification Comments
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 The return is dropped because the return type I contains no pointers.
P foo(return P p) ReturnScope Makes no sense to have return without scope.
I foo(return P p) Scope The return is dropped because the return type I contains no pointers.
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 Makes no sense to have return without scope.
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)

반공변성은 매개변수 제한이 있는 함수가 제한이 더 적은 해당 매개변수를 가진 함수로 오버라이드될 수 있다는 것을 의미해요. 이것은 각 상태의 설명에서 추론할 수 있어요. 다음 표는 어떤 속성이 완화될 수 있는지 보여줘요.

참고: 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
}

지역 static 변수 (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");
}

static 변수의 초기화자는 컴파일 타임에 평가될 수 있어야 해요. static 지역 변수에 대한 static 생성자나 static 소멸자는 없어요.

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

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
}

중첩 함수는 어휘적으로 둘러싸는 함수가 정의한 변수와 다른 심볼에 접근할 수 있어요. 이 접근은 그것들을 읽고 쓰는 능력을 모두 포함해요.

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 중첩 함수는 어휘적으로 둘러싸는 어떤 함수의 스택 변수에도 접근할 수 없지만, static 변수에는 접근할 수 있어요. 이것은 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의 static 멤버로 선언:
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!();
}
  • delegate 사용:
void test()
{
    void delegate() fp;
    void foo() { fp(); }
    void bar() { foo(); }
    fp = &bar;
}

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

함수 포인터, Delegate와 클로저 (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

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

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.

Delegate와 클로저 (Delegates & Closures)

delegate는 비정적 중첩 함수로 설정될 수 있어요:

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 힙에 할당돼요:

@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
}

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

메서드 Delegate (Method Delegates)

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

delegate는 &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)

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

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
}

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

delegate는 전역 함수, static 멤버 함수, 또는 static 중첩 함수의 주소를 취해 초기화될 수 없어요.

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을 사용하는데, 그것은 delegate로 추론돼요.

참고: 함수 포인터는 std.functional.toDelegate 템플릿을 통해 전달해 delegate 인자를 취하는 함수에 넘길 수 있는데, 이 템플릿은 어떤 호출 가능한 것을 null 컨텍스트 포인터를 가진 delegate로 변환해요.

익명 함수와 익명 Delegate (Anonymous Functions and Anonymous Delegates)

FunctionLiteral 참조.

main() 함수 (main() Function)

콘솔 프로그램에서 main()은 진입점으로 기능해요. 모든 모듈 초기화자가 실행된 후, 그리고 unittest가 실행된 후 호출돼요. 그것이 반환된 후 모든 모듈 소멸자가 실행돼요. main()은 다음과 같이 선언되어야 해요:

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

MainReturnDecl:
    void
    int
    noreturn
    auto
  • main이 void를 반환하면 OS는 성공 시 0 값을 받아요.
  • main이 void 또는 noreturn을 반환하면 OS는 잡히지 않은 예외 같은 비정상 종료 시 0이 아닌 값을 받아요.
  • main이 auto로 선언되면 추론된 반환 타입은 void, int, noreturn 중 하나여야 해요.
  • string[] 매개변수가 선언되면 그 매개변수는 OS가 프로그램에 전달한 인자를 담아요. 첫 번째 인자는 보통 실행 파일 이름이고, 그 뒤에 명령줄 인자들이 와요.
  • 참고: 런타임은 --DRT- 접두가 붙은 인자를 제거할 수 있어요.
  • 참고: 앞서 언급한 반환/매개변수 타입은 const, immutable로 주석 처리될 수 있어요. 일치하는 기본 타입을 가진 enum으로 대체될 수도 있어요.

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

필요에 따라 속성이 추가될 수 있어요(예: @safe, @nogc, nothrow 등).

extern(C) main() 함수 (extern(C) main() Function)

프로그램은 표준 진입점의 대안으로 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-style 배열(길이 + 포인터)을 나타내요. 세 번째 매개변수는 environ이라 불리는 POSIX 확장이며 현재 환경 변수에 대한 정보를 담아요.

참고: D main 함수에 대해 정의된 저장 클래스/enum 면제도 C main 함수에 적용돼요.

이 함수는 C main 함수의 자리를 차지하고, D main 함수와 연관된 어떤 설정·해체도 없이 즉시 실행돼요. 모듈 생성자, 모듈 소멸자, unittest에 의존하는 프로그램은 적절한 런타임 함수로 (역)초기화를 수동으로 수행해야 해요.

구현 정의(Implementation Defined): WinMain과 DllMain 같은 다른 시스템 특정 진입점이 존재할 수 있어요.

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

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

함수 템플릿 (Function Templates)

함수는 템플릿 형태의 컴파일 타임 인자를 가질 수 있어요. 함수 템플릿 참조.

컴파일 타임 함수 실행 (CTFE)

컴파일 타임 값이 필요한 문맥에서 함수는 그 값을 계산하는 데 쓰일 수 있어요. 이것은 컴파일 타임 함수 실행(Compile Time Function Execution, CTFE)이라고 불러요.

이 문맥들은:

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는 다음 제한을 받아요:

  • 식은 변경 가능한 static 변수를 참조할 수 없음
  • AsmStatements는 허용되지 않음
  • 이식 불가능한 캐스트(예: int[]에서 float[]로), 엔디언에 의존하는 캐스트 포함은 허용되지 않음. 부호 있는 타입과 부호 없는 타입 사이의 캐스트는 허용됨
  • union에서 겹치는 필드를 재해석하는 것은 허용되지 않음
  • 미정의 동작이 발생해서는 안 됨

CTFE에서 포인터는 안전하게 사용될 때 허용돼요:

  • 포인터 산술은 static 또는 동적 배열 요소를 가리키는 포인터에만 허용됨. 포인터는 배열의 첫 번째 요소 너머를 가리킬 수도 있지만, 그런 포인터는 역참조될 수 없음. null이거나 배열이 아닌 것을 가리키는 포인터에 대한 포인터 산술은 허용되지 않음
  • 두 포인터 사이의 순서 비교(<, <=, >, >=)는 두 포인터가 같은 배열을 가리키거나 적어도 하나가 null일 때 허용됨
  • 인접하지 않은 메모리 블록 사이의 포인터 비교는 불법이지만, 두 그런 비교가 &&나 ||로 결합되어 메모리 블록의 순서와 무관한 결과를 산출하는 경우는 예외. 각 비교는 <, <=, >, >=로 비교되는 두 포인터 식으로 구성되어야 하고 선택적으로 !로 부정될 수 있음. 예를 들어 p1, p2가 메모리 블록 P에 대한 포인터를 산출하는 식이고 q1, q2가 메모리 블록 Q에 대한 포인터를 산출하는 식일 때, (p1 > q1 && p2 <= q2)는 P와 Q가 무관한 메모리 블록이어도 허용됨. [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로 평가돼요.

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

복구할 수 없는 오류(assert 실패 같은)는 불법이에요.

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

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

문자열 Mixin과 컴파일 타임 함수 실행 (String Mixins and CTFE)

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 함수에서는 다음 연산이 허용되지 않아요:

@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가 아니면 클로저를 쓸 수 없어요. Delegates & Closures 참조.

@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는 추론될 수 있어요.

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

안전 함수에서 컴파일러가 강제하는 제한은:

  • 포인터 타입 T에서 포인터를 가진 어떤 타입 U로의 캐스팅 금지. 단, 다음 경우는 예외:
    • T가 U로 암시적으로 변환됨
    • U가 extern(D)인 T의 클래스나 인터페이스를 구현
    • T.opCast!U가 @safe임
    • 두 타입 모두 동적 배열이거나 두 타입 모두 원시 포인터이고, 다음을 만족함:
      • 목적 요소 타입이 포인터 타입이 아님
      • 소스 요소 타입이 포인터 타입일 때 목적 요소 타입이 변경 가능하지 않음
      • 목적 요소 타입이 동적 배열이거나 목적 요소 타입이 소스 요소 타입보다 크지 않음
      • 어떤 소스 요소 타입 한정자도 목적 요소 타입 한정자로 암시적으로 변환됨
      • 어떤 요소 타입도 함수 타입이 아님
      • 소스 타입이 void[]일 때 목적 요소 타입이 변경 가능하지 않음
      • 목적 요소 타입이 안전 값만 저장함
      • 목적 요소 타입이 변경 가능할 때 소스 요소 타입이 안전 값만 저장함
      • 어떤 요소 타입도 불투명(opaque)하지 않음
  • 비포인터 타입에서 포인터 타입으로의 캐스팅 금지
  • 포인터 산술(포인터 인덱싱·슬라이싱 포함) 없음
  • 배열 .ptr 속성 접근 불가
  • void 배열로 복사 불가
  • 다음 union 필드 접근 불가:
    • 다른 타입과 겹치는 포인터나 참조를 가진 필드
    • 다른 타입과 겹치는 invariant를 가진 필드
    • 안전하지 않은 값이 될 수 있는 필드와 겹치는 필드
  • 시스템 함수 호출 금지
  • class Exception에서 파생되지 않은 예외 포착 금지
  • 인라인 어셈블러는 @trusted로 표시되어야 함
  • 다음의 명시적 캐스팅 금지(일치하는 @safe opCast가 있는 경우 제외):
    • 변경 가능 객체를 immutable로(기본 데이터 타입 제외)
    • immutable 객체를 변경 가능으로(기본 데이터 타입 제외)
    • 스레드 지역 객체를 shared로
    • shared 객체를 스레드 지역으로
  • @system 또는 __gshared 변수 접근 불가
  • 다음을 포함하는 타입에 대한 void 초기화자 사용 불가:
    • 포인터/참조 타입
    • invariant가 있는 타입
    • 안전하지 않은 값
  • RvalueExpression 사용 불가

참고: 배열을 인덱싱하거나 슬라이싱할 때, 범위를 벗어난 접근은 런타임 오류를 일으켜요.

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

안전 함수는 신뢰되거나 시스템 함수와 공변이에요.

Best Practices: 가능한 한 많은 함수를 @safe로 표시하세요.

안전 외부 함수 (Safe External Functions)

외부 함수는 컴파일러에 보이는 함수 본문이 없어요:

@safe extern (C) void play();

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

Best Practices: 기본 설정에 의존하지 말고 외부 함수에 대해 명시적으로 속성을 설정하세요.

신뢰 함수 (Trusted Functions)

신뢰 함수는 @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.
}

신뢰 함수는 안전, 신뢰, 시스템 함수를 호출할 수 있어요.

신뢰 함수는 안전 또는 시스템 함수와 반공변(contravariant)이에요.

Best Practices: 신뢰 함수는 수동 검증을 더 쉽게 하도록 작게 유지해야 해요.

시스템 함수 (System Functions)

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

시스템 함수는 안전·신뢰 함수를 호출할 수 있어요.

시스템 함수는 신뢰·안전 함수와 공변이 아니에요.

Best Practices: 의심스러울 때, extern(C)와 extern(C++) 함수의 구현이 D에 없으면 @system으로 표시하세요. D 컴파일러가 그것들을 검사할 수 없기 때문이에요. 대부분은 @safe지만 수동으로 확인해야 해요.

Best Practices: 시스템 함수의 수와 크기는 최소화해야 해요. 이것은 안전성 수동 검사에 필요한 작업을 최소화해요.

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

함수 호출의 인자, 문맥, 접근 가능한 전역이 각각 안전 값안전한 앨리어싱을 가질 때, 그 함수는 다음을 만족하면 안전한 인터페이스를 가져요:

  • 미정의 동작을 나타낼 수 없음, 그리고
  • @safe 코드에서 접근 가능한 안전하지 않은 값을 만들 수 없음(예: 반환 값, 전역 변수, ref 매개변수를 통해), 그리고
  • @safe 코드에서 접근 가능한 안전하지 않은 앨리어싱을 도입할 수 없음

이 요구를 충족하는 함수는 @safe 또는 @trusted일 수 있어요. 충족하지 못하는 함수는 @system만 될 수 있어요.

예:

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

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

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

검증되지 않은 가정에 기반해 포인터를 순회하기 때문이에요(strlen은 s가 0으로 종결된다고 가정하고, memcpy는 dst와 src가 가리키는 메모리 객체가 적어도 nbytes라고 가정함). 인자로 전달된 C 문자열을 순회하는 어떤 함수든 @system만 될 수 있어요. 배열 경계를 위해 별도의 매개변수를 신뢰하는 어떤 함수든 @system만 될 수 있어요.

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

어떤 입력에 대해서도 미정의 동작을 나타내지 않아요. 유효한 포인터(안전함) 또는 null(역시 안전함)을 반환해요. 새 할당에 대한 포인터를 반환하므로 안전하지 않은 앨리어싱을 도입할 수 없어요. 참고: 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만 안전한 값이에요.

다른 모든 기본 데이터 타입에 대해 모든 가능한 비트 패턴이 안전해요.

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

  • null — 아래 참조
  • 그것이 살아 있는 메모리 객체를 가리키고, 그 메모리 객체의 가리켜진 값이 안전함

예:

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. */

동적 배열은 다음을 만족할 때 안전해요:

  • 그 포인터가 안전함, 그리고
  • 그 길이가 해당 메모리 객체의 경계 안에 있음, 그리고
  • 모든 요소가 안전함

예:

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. */
}

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

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

struct/union 인스턴스는 다음을 만족할 때 안전해요:

  • 접근 가능한 필드의 값이 안전함, 그리고
  • 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. */
}

클래스 참조는 null이거나 다음을 만족할 때 안전해요:

  • 클래스 타입 또는 그 클래스 타입에서 파생된 타입의 유효한 클래스 인스턴스를 참조함, 그리고
  • 인스턴스의 접근 가능한 필드의 값이 안전함, 그리고
  • union으로 안전하지 않은 앨리어싱을 도입하지 않음

함수 포인터는 null이거나 delegate 타입과 같거나 공변인 시그니처를 가진 유효한 함수를 참조할 때 안전해요.

delegate는 다음을 만족할 때 안전해요:

  • .funcptr 속성이 null이거나 delegate 타입과 일치하거나 공변인 함수를 참조함, 그리고
  • .ptr 속성이 null이거나 함수가 기대하는 형태의 메모리 객체를 참조함

Null 역참조 (Null Dereferences)

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

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

안전한 앨리어싱 (Safe Aliasing)

한 메모리 위치가 두 가지 다른 타입으로 접근 가능할 때, 그 앨리어싱은 다음 경우에 안전한 것으로 간주돼요:

  • 두 타입 모두 const 또는 immutable이거나;
  • 한 타입이 변경 가능하고 다른 타입이 const로 한정된 기본 데이터 타입이거나;
  • 두 타입 모두 변경 가능한 기본 데이터 타입이거나;
  • 한 타입이 길이가 0인 static 배열 타입이거나;
  • 한 타입이 길이가 0이 아닌 static 배열 타입이고, 배열 요소 타입과 다른 타입의 앨리어싱이 안전하거나;
  • 두 타입 모두 포인터 타입이고, 목적 타입의 앨리어싱이 안전하며, 목적 타입의 크기가 같음

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

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

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

예:

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)

FunctionLiteral, Auto 함수, Auto Ref 함수, 중첩 함수, 함수 템플릿은 그 함수 본문이 항상 존재하므로, 명시적으로 오버라이드하지 않으면 다음 속성을 추론해요:

마찬가지로 다음 매개변수 속성도 추론돼요:

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

추론은 함수 본문이 특정 속성의 규칙을 따르는지 판단하여 수행돼요.

순환 함수(즉 직간접적으로 자기 자신을 호출하게 되는 함수)는 비순수, 던지는, @system으로 추론돼요.

함수가 그 속성들에 대해 스스로를 검사하려 하면, 그 함수는 그 속성들을 가지지 않는 것으로 추론돼요.

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

균일 함수 호출 문법 (UFCS)

다음 두 가지가 모두 성립할 때 자유 함수가 멤버 함수처럼 호출될 수 있어요:

  • 그 객체 식에 대해 멤버 함수가 존재하지 않거나(존재할 수 없거나) 존재할 수 없음
  • 자유 함수의 첫 번째 매개변수 타입이 객체 식과 일치함

객체 식은 어떤 타입이든 될 수 있어요. 이것을 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): 이것은 객체의 private 상태를 처리하는 데 필요한 필수적인 것들만으로 클래스의 함수 수를 최소화하면서, 클래스에 외부 함수를 마치 public final 멤버 함수인 것처럼 추가하는 방법을 제공해요. 이것은 함수 체이닝과 컴포넌트 프로그래밍도 가능하게 해요.

이 일반적인 개념은 다른 프로그래밍 언어에서 다른 이름을 가져요. 다음은 간단한 목록이에요:

  • 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[])
    }
}

더 알아보기