본문으로 건너뛰기

안동민 개발노트

본문 시작

기본 매개변수

생략한 인자를 선언의 기본값으로 채우는 규칙을 익히고 선언 위치·오른쪽 배치·오버로딩 모호성을 점검합니다.

선택적인 값만 다른 함수를 여러 번 오버로딩하면 구현과 사용법이 불필요하게 늘어날 수 있습니다.

C++의 기본 매개변수(Default Arguments)는 호출에서 생략한 뒤쪽 인자를 선언의 기본값으로 채워, 하나의 함수로 자주 쓰는 호출 형태를 간결하게 표현합니다.

이 장에서는 기본 인자가 선택되는 지점, 오른쪽 연속 제약, 선언과 정의의 관계, 오버로드 모호성을 함께 다룹니다.


기본 매개변수란 무엇인가?

기본 매개변수는 함수 선언의 매개변수에 기본값(default value)을 지정하고, 호출에서 생략된 뒤쪽 인자를 그 값으로 채우는 기능입니다. 함수 본문이 실행 중에 기본값을 고르는 것이 아니라, 컴파일러가 호출 지점에서 도달 가능한 선언을 보고 인자를 보충합니다.

C++ 기본 인자가 호출 지점의 선언에서 선택되는 과정과 오른쪽 연속 제약, 후속 선언 및 재정의 규칙

CALL SITE · TRAILING DEFAULTS

생략한 인자는 호출 지점에 보이는 선언이 채운다

기본 인자는 함수 본문 안의 fallback이 아닙니다. 컴파일러는 호출 지점에서 도달 가능한 같은 범위의 선언을 보고, 생략된 뒤쪽 인자를 보충합니다. 직접 전달한 인자는 언제나 기본값보다 우선합니다.

선언이 제공하는 호출 형태

필수 인자 뒤에 선택 인자를 연속해서 둔다

void setWindowSize(
    int x,
    int y,
    int width = 800,
    int height = 600);

setWindowSize(10, 20)width=800, height=600

명시한 값은 그대로 사용

중간 인자를 건너뛰는 위치 기반 문법은 없다

setWindowSize(50, 50, 1024);
// width=1024, height=600

setWindowSize(0, 0, 1920, 1080);
// 두 기본값 모두 사용하지 않음

따라서 한 선언에서는 기본값을 둔 매개변수 뒤의 모든 매개변수가 현재 선언 또는 앞선 같은 범위의 선언에서 기본값을 가져야 합니다.

Call-site resolution

선언에서 실제 호출까지의 세 단계

  1. 도달 가능한 선언을 모은다

    호출 지점에서 보이는 같은 host scope(선언이 속한 범위)의 선언들이 각 매개변수에 제공한 기본값을 확인합니다. 내부 범위에 새로 선언한 함수는 바깥 선언의 기본값을 자동으로 물려받지 않습니다.

  2. 빠진 trailing 인자를 확인한다

    왼쪽부터 전달된 인자를 대응시킨 뒤, 남은 오른쪽 매개변수마다 유효한 기본값이 있어야 그 호출 형태를 사용할 수 있습니다.

  3. 기본 표현식을 호출마다 평가한다

    기본 표현식의 이름은 선언 문맥에서 해석되지만, 표현식 자체는 해당 인자를 생략한 각 호출에서 평가됩니다.

기본 인자 선언 조합의 적합성
선언 조합판정이유
void place(
  int x,
  int y = 20,
  int z = 30);
적합기본값이 오른쪽 끝에서 연속합니다.
void gap(
  int x = 10,
  int y,
  int z = 30);
오류x 뒤의 y에 현재 또는 앞선 선언의 기본값이 없습니다.
void staged(int x,
            int y = 20);
void staged(int x = 10,
            int y);
적합같은 범위의 후속 선언이 아직 없던 x 기본값만 추가하고, y는 앞선 선언에서 가져옵니다.
void print(int n = 1);
void print(int n = 1) { /* … */ }
오류정의도 선언입니다. 이미 제공한 n의 기본값을 같은 값으로 반복해도 재정의입니다.
적합 · trailing defaults
void place(
  int x,
  int y = 20,
  int z = 30);

기본값이 오른쪽 끝에서 연속합니다.

오류 · 중간 공백
void gap(
  int x = 10,
  int y,
  int z = 30);

x 뒤의 y에 현재 또는 앞선 선언의 기본값이 없습니다.

적합 · 후속 추가
void staged(int x,
            int y = 20);
void staged(int x = 10,
            int y);

같은 범위의 후속 선언이 아직 없던 x 기본값만 추가하고, y는 앞선 선언에서 가져옵니다.

오류 · 재정의
void print(int n = 1);
void print(int n = 1) { /* … */ }

정의도 선언입니다. 이미 제공한 n의 기본값을 같은 값으로 반복해도 재정의입니다.

공개 API 원칙: 호출부가 기본값을 볼 수 있도록 헤더 선언에 두고, 소스의 정의에는 반복하지 않습니다. 후속 선언의 추가 규칙은 허용되지만 선언을 흩어 놓을 이유가 되지는 않습니다.

기본 매개변수 선언 형식
반환타입 함수이름(타입1 매개변수1, 타입2 매개변수2 = 기본값2, 타입3 매개변수3 = 기본값3);
  • 기본 인자는 상수뿐 아니라 네임스페이스 범위의 변수나 함수 호출을 포함한 표현식일 수 있습니다. 그 표현식의 이름은 선언 지점의 문맥으로 해석되고, 해당 인자를 생략한 호출마다 평가됩니다.
  • 중요: 한 선언에서 기본값이 있는 매개변수 뒤의 매개변수는 현재 선언 또는 앞선 같은 범위의 선언에서 기본값을 제공받아야 합니다. 일반적인 한 줄 선언에서는 기본값이 오른쪽 끝부터 연속해야 합니다.
잘못된 기본 매개변수 예시
// int func(int a = 10, int b); // 오류: 기본값 없는 매개변수 b가 기본값 있는 매개변수 a 뒤에 있음
// int func(int a, int b = 20, int c); // 오류: 기본값 없는 매개변수 c가 기본값 있는 매개변수 b 뒤에 있음
올바른 기본 매개변수 예시
int funcA(int a, int b = 20, int c = 30); // OK: 뒤의 두 매개변수에 기본값
int funcB(int a, int b, int c = 30);      // OK: 마지막 매개변수에 기본값
int funcC(int a = 10, int b = 20, int c = 30); // OK: 세 매개변수 모두 기본값

기본 매개변수의 필요성

  • 함수 오버로딩의 대안: 매개변수 몇 개만 다른 유사한 함수들을 여러 개 오버로딩하는 대신, 기본 매개변수를 사용하여 하나의 함수로 대체할 수 있습니다. 이는 코드 중복을 줄이고 유지보수를 용이하게 합니다.
  • 유연한 함수 호출: 함수를 호출할 때 모든 인자를 항상 제공하지 않아도 되므로, 함수 사용이 더 편리해집니다.
  • 선택적 인자 구현: 특정 매개변수가 대부분의 경우 동일한 값을 가지거나, 필수가 아닌 선택적 정보를 나타낼 때 유용합니다.

기본 매개변수 사용 예시

메시지와 함께 특정 횟수만큼 반복하는 함수를 만들어봅시다.

횟수를 지정하지 않으면 기본값(예: 1회)으로 출력하게 합니다.

기본 매개변수 예시 - 메시지 출력 함수
#include <iostream>
#include <string>

// printMessage 함수: 메시지와 반복 횟수를 받지만, count에 기본값 1을 지정
void printMessage(const std::string& message, int count = 1) {
    for (int i = 0; i < count; ++i) {
        std::cout << message << std::endl;
    }
}

int main() {
    printMessage("Hello, C++!"); // count 생략 -> 기본값 1 사용
    std::cout << "------------------" << std::endl;
    printMessage("Welcome!", 3); // count를 3으로 지정

    /* 출력:
    Hello, C++!
    ------------------
    Welcome!
    Welcome!
    Welcome!
    */

    return 0;
}

printMessage("Hello, C++!") 호출 시 count 인자를 생략했으므로, count는 기본값인 1이 됩니다.

printMessage("Welcome!", 3) 호출 시에는 count3이 명시적으로 전달되므로, 기본값은 무시되고 3이 사용됩니다.

그래픽 프로그램에서 창의 위치와 크기를 설정하는 함수를 만들어봅시다.

x, y 좌표와 width, height를 받는데, widthheight에 기본값을 줄 수 있습니다.

기본 매개변수 예시 - 창 크기 설정 함수
#include <iostream>

// x, y 좌표와 폭, 높이를 받는 함수
// 폭과 높이에 기본값을 설정
void setWindowSize(int x, int y, int width = 800, int height = 600) {
    std::cout << "창 설정: X=" << x << ", Y=" << y
              << ", 폭=" << width << ", 높이=" << height << std::endl;
}

int main() {
    setWindowSize(10, 20); // X=10, Y=20, 폭=800, 높이=600 (기본값 사용)
    setWindowSize(50, 50, 1024); // X=50, Y=50, 폭=1024, 높이=600 (height 기본값 사용)
    setWindowSize(0, 0, 1920, 1080); // X=0, Y=0, 폭=1920, 높이=1080 (모든 인자 명시)

    /* 출력:
    창 설정: X=10, Y=20, 폭=800, 높이=600
    창 설정: X=50, Y=50, 폭=1024, 높이=600
    창 설정: X=0, Y=0, 폭=1920, 높이=1080
    */
    return 0;
}

기본 매개변수와 함수 원형/정의

비템플릿 함수는 같은 범위의 후속 선언에서 아직 지정되지 않은 기본값을 추가할 수 있습니다. 하지만 한 매개변수에 기본값을 한 번 제공한 뒤에는, 같은 값을 쓰더라도 뒤의 선언이나 정의에서 다시 지정할 수 없습니다.

공개 함수는 호출 지점이 기본값을 볼 수 있도록 일반적으로 헤더의 함수 원형(declaration)에 기본값을 두고, 소스 파일의 정의에는 반복하지 않습니다. 다른 내부 범위에 새로 쓴 선언은 바깥 범위 선언의 기본값을 상속하지 않습니다.

올바른 방법 (함수 원형에 기본값 지정)
my_printer.h
#pragma once
#include <string>

// 함수 원형에 기본 매개변수 지정
void printData(int value, char separator = '-', int repeat = 1);
my_printer.cpp
#include "my_printer.h"
#include <iostream>

// 함수 정의에는 기본 매개변수를 다시 지정하지 않음
void printData(int value, char separator, int repeat) {
    for (int i = 0; i < repeat; ++i) {
        std::cout << value;
        if (i < repeat - 1) { // 마지막을 제외하고 구분자 출력
            std::cout << separator;
        }
    }
    std::cout << std::endl;
}
main.cpp
#include "my_printer.h" // 원형이 있는 헤더 포함

int main() {
    printData(10);         // 10 (separator='-', repeat=1)
    printData(20, '*');    // 20 (repeat=1이므로 구분자를 출력하지 않음)
    printData(30, '#', 5); // 30#30#30#30#30

    return 0;
}

기본 매개변수와 함수 오버로딩의 관계

기본 매개변수는 함수 오버로딩과 함께 사용될 때 모호성(Ambiguity)을 유발할 수 있습니다. 생략된 모든 뒤쪽 매개변수에 기본값이 있으면 그 오버로드도 viable 후보가 되지만, 기본값을 사용했다는 사실 자체는 더 좋은 후보가 되는 기준이 아닙니다.

기본 매개변수와 오버로딩의 모호성 예시
#include <iostream>
#include <string>

void greet(std::string name) { // 오버로딩된 함수
    std::cout << "안녕하세요, " << name << std::endl;
}

void greet(std::string name, std::string salutation = "안녕") { // 기본 매개변수를 가진 함수
    std::cout << salutation << ", " << name << std::endl;
}

int main() {
    // greet("홍길동"); // 컴파일 오류! 모호성 발생!
                       // greet(std::string)과 greet(std::string, std::string="안녕")
                       // 둘 다 greet("홍길동")에 매칭될 수 있기 때문
    return 0;
}

위 예시에서 greet("홍길동")을 호출하면, 컴파일러는 greet(std::string)을 호출할지, 아니면 두 번째 greet 함수에서 salutation 매개변수에 기본값을 사용하여 greet(std::string, std::string)을 호출할지 결정할 수 없습니다.

다음 흐름으로 후보의 viability와 최종 순위를 나누어 보면 충돌 원인을 빠르게 찾을 수 있습니다.

C++ 기본 인자가 오버로드 viability를 넓혀 모호성을 만드는 과정과 공개 기본값 변경이 호출부에 미치는 영향

VIABLE · RANK · API

기본 인자는 후보를 살릴 수 있지만 순위를 올려 주지 않는다

오버로드 판정은 “호출 가능한가”와 “누가 더 좋은가”를 나눠서 봅니다. 생략한 뒤쪽 매개변수에 기본값이 있으면 후보가 viable해지지만, 기본값을 덜 사용했다는 이유만으로 우선하지는 않습니다.

후보 A · 1개 매개변수

greet(std::string)

void greet(std::string name);

greet("홍길동")의 인자 수와 맞으므로 viable 후보입니다.

후보 B · 기본 인자

greet(std::string, std::string)

void greet(
    std::string name,
    std::string salutation = "안녕");

두 번째 인자를 생략해도 기본값이 있으므로 같은 한 인자 호출에 viable합니다.

Overload resolution

greet("홍길동")이 모호해지는 흐름

  1. 이름으로 후보 집합을 만든다

    보이는 두 greet 선언이 모두 후보가 됩니다. 기본 인자는 함수 타입이나 별도 오버로드를 만들지 않습니다.

  2. 인자 수와 기본값으로 viability를 거른다

    후보 A는 매개변수가 하나라서, 후보 B는 빠진 두 번째 매개변수에 기본값이 있어서 모두 살아남습니다.

  3. 제공된 인자의 변환 순위를 비교한다

    두 후보 모두 문자열 리터럴을 첫 번째 std::string으로 바꾸는 같은 변환이 필요합니다. 유일한 최적 후보가 없으므로 호출은 ill-formed입니다.

호출 형태별 오버로드 결과
호출결과판정 근거
greet("홍길동")모호함A와 B가 모두 viable이고, 제공된 인자에 대한 변환 순위도 같습니다.
greet("A", "Hi")후보 B후보 A는 인자가 너무 많아 viable하지 않습니다.
후보 A를 제거한 뒤 한 인자 호출후보 B단 하나의 함수가 기본 인자로 공통 호출 형태를 제공합니다.
역할별 이름을 분리명확함동작 의미가 다르면 기본 인자보다 서로 다른 함수 이름이 호출 의도를 더 잘 드러냅니다.
모호함

greet("홍길동")

A와 B가 모두 viable이고, 제공된 인자에 대한 변환 순위도 같습니다.

후보 B

greet("A", "Hi")

후보 A는 인자가 너무 많아 viable하지 않습니다.

후보 B

후보 A를 제거한 한 인자 호출

단 하나의 함수가 기본 인자로 공통 호출 형태를 제공합니다.

명확함

역할별 이름을 분리

동작 의미가 다르면 서로 다른 함수 이름이 호출 의도를 더 잘 드러냅니다.

Public default is API

헤더의 기본값 변경은 재컴파일된 호출부의 동작을 바꾼다

// v1 header
void connect(int timeoutMs = 1000);

v1 헤더로 컴파일한 connect() 호출은 인자 1000을 전달하는 코드가 됩니다.

// v2 header
void connect(int timeoutMs = 3000);

v2 헤더로 다시 컴파일한 호출부는 3000을 전달합니다. 기존 오브젝트를 단순히 다시 링크하는 것만으로 그 호출 인자가 바뀌지는 않습니다.

설계 체크: 자주 쓰는 한 가지 의미에만 기본값을 주고, 같은 호출 형태의 오버로드를 만들지 않으며, 공개 기본값 변경을 소스 호환성 이상의 동작 변경으로 검토합니다.

이러한 모호성을 피하려면,
  • 기본 매개변수와 오버로드가 같은 호출 형태를 만들지 않도록 설계합니다.
  • 두 번째 인자를 명시해 후보를 하나로 좁히거나, 역할이 다른 함수라면 이름을 분리합니다.

결론: 기본 매개변수의 효율적인 활용

기본 매개변수는 대부분 같은 값을 쓰는 선택 인자를 간결하게 표현하고 유사한 오버로드의 중복을 줄이는 도구입니다. 대신 필수 인자는 왼쪽, 기본값이 있는 인자는 오른쪽에 모으고, 호출 하나가 유일한 최적 후보로 좁혀지는지 확인해야 합니다.

기본값도 공개 인터페이스의 일부입니다. 헤더의 기본 리터럴을 바꾸면 새 헤더로 다시 컴파일한 호출부가 새 값을 전달하지만, 재컴파일하지 않은 호출부의 오브젝트 코드는 이전 값을 계속 전달할 수 있습니다. 따라서 기본값 변경도 API 동작 변경으로 검토해야 합니다.