기본 인자

기본 인자 (Default arguments)

함수를 호출할 때, 뒤쪽에 있는 인자 하나 이상을 생략해도 되게 만들어 주는 게 기본 인자(default argument)예요. 함수 선언의 매개변수 목록에서 특정 매개변수에 = 초기화식을 붙이면, 호출 쪽에서 그 자리에 인자를 안 넘겨줘도 그 값이 대신 들어가요.

출처: cppreference

본문

문법 (Syntax)

함수 선언의 매개변수 목록에서 다음 형태로 표기해요.

표기 의미
attr(선택) decl-specifier-seq declarator = initializer (1) 이름 있는 매개변수에 기본 인자 지정
attr(선택) decl-specifier-seq abstract-declarator(선택) = initializer (2) 이름 없는 매개변수에 기본 인자 지정

기본 인자는 함수 호출에서 빠진 뒤쪽 인자 자리에 대신 사용돼요.

void point(int x = 3, int y = 4);

point(1, 2); // point(1, 2) 호출
point(1);    // point(1, 4) 호출
point();     // point(3, 4) 호출

기본 인자가 붙은 매개변수 뒤의 규칙

함수 선언에서 기본 인자가 있는 매개변수 뒤에 오는 매개변수들은 전부 다음 조건을 만족해야 해요. 기본 인자가 있어야 하거나, 매개변수 팩이어야 해요.

  • 같은 스코프에 있는 이 선언이나 이전 선언에서 기본 인자가 제공돼야 해요.
int x(int = 1, int); // 오류: 기본 인자는 뒤쪽 매개변수에만 붙일 수 있음
                     //        (앞서 "x"에 대한 선언이 없다고 가정)

void f(int n, int k = 1);
void f(int n = 0, int k); // OK: "k"의 기본 인자는 같은 스코프의
                          // 이전 선언이 제공

void g(int, int = 7);

void h()
{
    void g(int = 1, int); // 오류: 같은 스코프가 아님
}
  • 예외 (C++11부터): 매개변수가 매개변수 팩(parameter pack)에서 확장됐거나 함수 매개변수 팩이라면 이 규칙이 적용되지 않아요.
template<class... T> struct C { void f(int n = 0, T...); };
C<int> c; // OK; void C::f(int n = 0, int) 선언으로 인스턴스화됨

template<class... T> void h(int i = 0, T... args); // OK
  • 줄임표(...)는 매개변수가 아니므로, 기본 인자가 있는 매개변수 뒤에 와도 돼요.
int g(int n = 0, ...); // OK

어디에 쓸 수 있고, 어디에 쓸 수 없는가

기본 인자는 함수 선언과 람다 표현식의 매개변수 목록에만 허용돼요 (C++11부터). 함수에 대한 포인터·참조의 선언이나 typedef 선언에는 쓸 수 없어요. 템플릿 매개변수 목록도 비슷한 문법으로 기본 템플릿 인자를 사용해요.

재선언과 기본 인자

템플릿이 아닌 함수의 경우, 같은 스코프에서 함수를 다시 선언하면 이미 선언된 함수에 기본 인자를 추가할 수 있어요. 함수 호출 지점에서의 기본 인자는 그 함수에 대해 보이는 모든 선언이 제공한 기본 인자의 합집합이에요. 다만 재선언은 이미 보이는 기본 인자가 있는 매개변수에 (값이 같더라도) 새 기본 인자를 도입할 수 없어요. 그리고 내부 스코프에서의 재선언은 바깥 스코프의 기본 인자를 물려받지 않아요.

void f(int, int);     // #1
void f(int, int = 7); // #2 OK: 기본 인자 추가

void h()
{
    f(3); // #1과 #2가 스코프 안: f(3,7) 호출
    void f(int = 1, int); // 오류: 두 번째 매개변수의 기본 인자는
                          // 바깥 스코프에서 물려받지 않음
}

void m()
{ // 새 스코프 시작
    void f(int, int); // 내부 스코프 선언; 기본 인자 없음
    f(4); // 오류: f(int, int) 호출에 인자가 부족
    void f(int, int = 6);
    f(4); // OK: f(4, 6) 호출
    void f(int, int = 6); // 오류: 두 번째 매개변수에 이미 기본 인자가 있음
                          // (값이 같아도 마찬가지)
}

void f(int = 1, int); // #3 OK, #2에 기본 인자를 추가

void n()
{ // 새 스코프 시작
    f(); // #1, #2, #3이 스코프 안: f(1, 7) 호출
}

인라인 함수가 서로 다른 번역 단위에 선언됐다면, 각 번역 단위 끝에서 누적된 기본 인자 집합이 서로 같아야 해요. (C++20부터) 인라인이 아닌 함수가 같은 네임스페이스 스코프에서 서로 다른 번역 단위에 선언됐다면, 있으면 기본 인자도 서로 같아야 해요(단 일부 기본 인자는 어떤 번역 단위에서 빠질 수 있어요).

친구(friend) 선언이 기본 인자를 지정한다면, 그 선언은 친구 함수 정의여야 하고, 그 번역 단위에서 이 함수에 대한 다른 선언은 허용되지 않아요.

using 선언과 기본 인자

using 선언은 알려진 기본 인자의 집합을 그대로 가져와요. 그리고 나중에 그 함수의 네임스페이스에 기본 인자가 더 추가되면, 그 기본 인자는 using 선언이 보이는 곳 어디서나 함께 보여요.

namespace N
{
    void f(int, int = 1);
}

using N::f;

void g()
{
    f(7); // f(7, 1) 호출
    f();  // 오류
}

namespace N
{
    void f(int = 2, int);
}

void h()
{
    f();  // f(2, 1) 호출
}

기본 인자의 이름 해석 시점

기본 인자에 쓰인 이름들은 선언하는 지점에서 이름을 찾고(lookup), 접근성을 검사하고, 바인딩돼요. 다만 그 값은 함수 호출 지점에서 평가돼요.

int a = 1;

int f(int);

int g(int x = f(a)); // f를 찾으면 ::f, a를 찾으면 ::a
                     // 이 시점의 ::a 값(1)은 사용되지 않음

void h()
{
    a = 2; // ::a의 값을 바꿈
    {
        int a = 3;
        g(); // f(2)를 호출하고, 그 결과로 g()를 호출
    }
}

멤버 함수와 기본 인자

템플릿이 아닌 클래스의 멤버 함수는 클래스 밖 정의(out-of-class definition)에서도 기본 인자를 가질 수 있고, 클래스 본문 안의 선언이 제공한 기본 인자와 합쳐져요. 만약 이런 밖에서의 기본 인자 때문에 멤버 함수가 기본 생성자나 복사/이동(C++11부터) 생성자·대입 연산자로 바뀌어서(호출이 모호해져서) 버리면, 그 프로그램은 ill-formed예요. 템플릿 클래스의 멤버 함수는 모든 기본 인자를 멤버 함수의 처음 선언에서 제공해야 해요.

class C
{
    void f(int i = 3);
    void g(int i, int j = 99);
    C(int arg); // 기본 생성자가 아님
};

void C::f(int i = 3) {}         // 오류: 클래스 스코프에서 기본 인자를
                                // 이미 지정했음

void C::g(int i = 88, int j) {} // OK: 이 번역 단위에서는
                                // C::g를 인자 없이 호출 가능

C::C(int arg = 1) {}            // 오류: 이걸 기본 생성자로 바꿔 버림

가상 함수와 기본 인자

가상 함수를 재정의(override)하는 함수는 기반 클래스 선언의 기본 인자를 물려받지 않아요. 그리고 가상 함수 호출을 할 때 기본 인자는 객체의 정적 타입(static type) 을 기준으로 결정돼요. (참고로 NVI 패턴, 즉 비가상 인터페이스 패턴으로 이 문제를 피할 수 있어요.)

struct Base
{
    virtual void f(int a = 7);
};

struct Derived : Base
{
    void f(int a) override;
};

void m()
{
    Derived d;
    Base& b = d;
    b.f(); // OK: Derived::f(7) 호출
    d.f(); // 오류: 기본 인자가 없음
}

쓸 수 없는 것들

기본 인자 안에서는 몇 가지를 쓸 수 없어요.

  • 지역 변수는 (평가되지 않는 문맥이 아니라면) 기본 인자에 쓸 수 없어요.
void f()
{
    int n = 1;
    extern void g(int x = n);        // 오류: 지역 변수는 기본 인자가 될 수 없음
    extern void h(int x = sizeof n); // CWG 2082 이후로 OK
}
  • this 포인터는 기본 인자에 쓸 수 없어요.
class A
{
    void f(A* p = this) {} // 오류: this는 허용되지 않음
};
  • 비정적 클래스 멤버는 (평가되지 않아도) 기본 인자에 쓸 수 없어요. 단 멤버에 대한 포인터(pointer-to-member)를 만드는 데 쓰거나, 멤버 접근 표현식 안에 쓰거나, (C++26부터) 반영 연산자 ^^의 피연산자로 쓸 때는 예외예요.
int b;

class X
{
    int a;
    int mem1(int i = a);       // 오류: 비정적 멤버는 사용 불가
    int mem2(int i = b);       // OK: 이름을 찾으면 X::b, 정적 멤버
    int mem3(int X::* i = &X::a); // OK: 비정적 멤버를 포인터로 사용 가능
    int mem4(int i = x.a);     // OK: 멤버 접근 표현식 안
    consteval int mem5(std::meta::info r = ^^a); // C++26부터 OK: 반영으로 사용

    static X x;
    static int b;
};
  • 기본 인자는 함수를 호출할 때 해당 매개변수에 인자가 제공되지 않을 때마다 평가돼요. 함수 매개변수는 (평가되지 않는 문맥이 아니라면) 기본 인자에 쓸 수 없어요. 단 매개변수 목록에서 앞에 나오는 매개변수는 스코프 안에 있어요.
int a;

int f(int a, int b = a); // 오류: 기본 인자에 매개변수 a를 사용
int g(int a, int b = sizeof a); // CWG 2082 해결 전까지는 오류
                                // 해결 후: 평가되지 않는 문맥에서의 사용은 OK

기본 인자는 함수 타입의 일부가 아니다

기본 인자는 함수의 타입에 포함되지 않아요. 그래서 함수 포인터 타입은 기본 인자를 반영하지 않아요.

int f(int = 0);

void h()
{
    int j = f(1);
    int k = f(); // f(0) 호출
}

int (*p1)(int) = &f;
int (*p2)()    = &f; // 오류: f의 타입은 int(int)

연산자 함수

함수 호출 연산자 ()와 첨자 연산자 [](C++23부터)를 제외한 연산자 함수는 기본 인자를 가질 수 없어요.

class C
{
    int operator++(int i = 0); // ill-formed
    int operator[](int j = 0); // C++23부터 OK
    int operator()(int k = 0); // OK
};

(C++23부터) 명시적 객체 매개변수(explicit object parameter) 는 기본 인자를 가질 수 없어요.

struct S { void f(this const S& = S{}); }; // ill-formed

참고 (Note)

매개변수 이름이 없을 때, 합성 대입 토큰(compound assignment token)과 겹치지 않도록 공백이 필요할 수 있어요(maximal munch 규칙 때문).

void f1(int*=0);         // 오류: 여기서 "*="는 예상하지 못한 토큰
void g1(const int&=0);   // 오류: 여기서 "&="는 예상하지 못한 토큰
void f2(int* = 0);       // OK
void g2(const int& = 0); // OK
void h(int&&=0);         // 공백 없이도 OK, 여기서 "&&"는 하나의 토큰

결함 보고 (Defect reports)

다음의 동작을 바꾸는 결함 보고(DR)가 이전에 공표된 C++ 표준에 소급 적용됐어요.

DR 적용 대상 공표된 동작 올바른 동작
CWG 217 C++98 클래스 템플릿의 비템플릿 멤버 함수에 기본 인자를 추가할 수 있었음 금지
CWG 1344 C++98 멤버 함수의 클래스 밖 정의에서 추가된 기본 인자가 특별 멤버 함수로 바꿀 수 있었음 금지
CWG 1716 C++98 호출자가 인자를 제공해도 기본 인자가 매 호출마다 평가됨 해당 매개변수에 인자가 제공되지 않을 때만 평가
CWG 2082 C++98 기본 인자가 평가되지 않는 문맥에서 지역 변수와 앞선 매개변수 사용을 금지 평가되지 않는 문맥에서의 사용 허용
CWG 2233 C++11 기본 인자가 있는 매개변수 뒤에 매개변수 팩에서 확장된 매개변수가 올 수 없었음 허용
CWG 2683 C++98 클래스 템플릿의 중첩 클래스 멤버 함수의 클래스 밖 정의가 기본 인자를 가질 수 있었음 금지