C++ 기본 인자 | "Default Arguments" 가이드

C++ 기본 인자 | "Default Arguments" 가이드

이 글의 핵심

기본값은 주로 선언부에만 두고, 오른쪽부터 연속으로 지정합니다. 가상 함수에서는 기본 인자가 정적으로 결정된다는 점과 오버로딩 모호성을 함께 알아두면 실수가 줄어듭니다.

기본 인자란?

기본 인자(default arguments) 는 함수 매개변수에 기본값을 지정하여, 호출 시 인자를 생략할 수 있게 하는 기능입니다. 함수 오버로딩을 대체하여 코드를 간결하게 만들 수 있습니다.

void greet(const std::string& name = "Guest") {
    std::cout << "Hello, " << name << "!" << std::endl;
}

int main() {
    greet("Alice");  // Hello, Alice!
    greet();         // Hello, Guest!
}

왜 필요한가?:

  • 간결성: 오버로딩 없이 선택적 매개변수 구현
  • 하위 호환성: 기존 함수에 새 매개변수 추가 시 기존 코드 유지
  • 가독성: 자주 사용하는 값을 기본값으로 설정
// ❌ 오버로딩: 코드 중복
void connect(const std::string& host) {
    connect(host, 8080);
}

void connect(const std::string& host, int port) {
    // 연결 로직
}

// ✅ 기본 인자: 간결
void connect(const std::string& host, int port = 8080) {
    // 연결 로직
}

기본 사용법

// 단일 기본 인자
void func(int x = 10) {
    std::cout << x << std::endl;
}

// 여러 기본 인자
void func(int x = 10, int y = 20, int z = 30) {
    std::cout << x << ", " << y << ", " << z << std::endl;
}

int main() {
    func();           // 10, 20, 30
    func(1);          // 1, 20, 30
    func(1, 2);       // 1, 2, 30
    func(1, 2, 3);    // 1, 2, 3
}

규칙

// ✅ 오른쪽부터 기본값
void func(int x, int y = 20, int z = 30) {
    // OK
}

// ❌ 중간에 빈 자리
// void func(int x, int y = 20, int z) {  // 에러
//     // ...
// }

// ✅ 모두 기본값
void func(int x = 10, int y = 20, int z = 30) {
    // OK
}

기본 인자 규칙 상세:

  1. 오른쪽부터 연속: 기본값이 있는 매개변수는 오른쪽부터 연속되어야 합니다.
// ✅ 올바른 예시
void f1(int a, int b = 2, int c = 3);
void f2(int a = 1, int b = 2, int c = 3);

// ❌ 잘못된 예시
// void f3(int a = 1, int b, int c = 3);  // 에러: b가 중간에 기본값 없음
// void f4(int a = 1, int b, int c);      // 에러: a만 기본값
  1. 선언에만 기본값: 헤더 파일의 선언에만 기본값을 지정하고, 구현 파일에는 생략합니다.
// header.h
void func(int x, int y = 20);

// source.cpp
void func(int x, int y) {  // 기본값 없음
    std::cout << x << ", " << y << '\n';
}
  1. 재선언 시 기본값 추가 가능: 같은 매개변수에 대해 재선언 시 기본값을 추가할 수 있지만, 중복 지정은 불가합니다.
void func(int x, int y, int z);  // 선언 1

void func(int x, int y, int z = 30);  // 선언 2: z에 기본값 추가

void func(int x, int y = 20, int z);  // 선언 3: y에 기본값 추가

// 최종: func(int x, int y = 20, int z = 30)

// ❌ 중복 지정
// void func(int x, int y = 20, int z = 30);  // 에러: y, z 중복

가상 함수에서의 제약과 함정

기본 인자는 가상이 아닙니다. 가상 함수 호출에서도 어떤 기본값이 쓰이는지는 컴파일 타임에, 호출에 사용한 정적 타입으로 정해집니다.

struct Base {
    virtual void f(int x = 1) { std::cout << "B " << x << '\n'; }
};
struct Derived : Base {
    void f(int x = 2) override { std::cout << "D " << x << '\n'; }
};

int main() {
    Derived d;
    Base& b = d;
    d.f();   // D 2  — 정적 타입 Derived → Derived의 기본값
    b.f();   // D 1  — 정적 타입 Base → Base의 기본값(파생 쪽 기본값 아님!)
}

파생 클래스에서 override한 함수에 다른 기본값을 두는 것은 문법적으로 가능하지만, 베이스 포인터로 호출하는 사용자에게는 혼란만 줍니다. 공개 API에서는 기본값은 베이스 선언에만 두고 파생에서는 생략하거나, 아예 비가상 래퍼로 기본값을 처리하는 편이 안전합니다.

또한 기본 인자가 있는 가상 함수를 오버라이드할 때, 시그니처(이름·매개변수 목록)는 같아야 하므로 기본값 목록은 상속 규칙과 별개로 헤더 설계를 맞춰야 합니다.

함수 오버로딩과의 관계 (심화)

기본 인자는 “호출 시 인자 개수를 줄이는 문법 설탕”일 뿐, 별도의 오버로드가 생기는 것은 아닙니다. 그래서 다음이 성립합니다.

  • 재정의 집합에 “기본값만 다른” 함수를 추가하면, 일부 호출이 모호해질 수 있습니다(위 “문제 3” 참고).
  • 템플릿 부분 특수화enable_if와 섞을 때도, “이 호출이 어떤 함수로 갈지”가 한눈에 안 들어오면 기본 인자보다 명시적 오버로드가 나을 때가 많습니다.
  • 가변 인자 템플릿과는 보통 충돌하지 않지만, f(int)f(int, int = 0)가 같이 있으면 f(0)에서 모호합니다.

실전 패턴 보강

  • API 확장: 옵션 객체(struct Options)나 빌더 패턴을 쓰면 기본 인자 나열이 길어지는 것을 피할 수 있습니다. 기본 인자는 2~3개까지가 읽기 좋은 경우가 많습니다.
  • 헤더 전방 선언: 기본값은 첫 선언(보통 헤더)에 모읍니다. 구현 파일에 또 쓰면 ODR 위반입니다.
  • 람다: C++14부터 일반 람다에도 기본 인자를 둘 수 있습니다. 팀에서 쓰는 표준(C++11만 허용 등)에 따라 달라지므로 빌드 설정과 맞추세요.

흔한 실수 추가

  • 기본값 표현식이 매 호출마다 평가: 기본 인자가 get_id()처럼 부작용이 있으면, 생략 호출마다 실행됩니다. “한 번만”이 필요하면 기본값은 std::nullopt 등으로 두고 본문에서 처리하세요.
  • 정적 바인딩: 위 가상 함수 예처럼, 인터페이스 문서에 “베이스 타입으로 호출 시 기본값은 베이스 기준”을 명시하는 것이 좋습니다.

실전 예시

예시 1: 로그 함수

#include <iostream>
#include <fstream>
#include <chrono>
#include <ctime>

enum class LogLevel {
    DEBUG,
    INFO,
    WARNING,
    ERROR
};

void log(const std::string& message, 
         LogLevel level = LogLevel::INFO,
         bool timestamp = true) {
    if (timestamp) {
        auto now = std::chrono::system_clock::now();
        auto time = std::chrono::system_clock::to_time_t(now);
        std::cout << std::ctime(&time) << " ";
    }
    
    switch (level) {
        case LogLevel::DEBUG:   std::cout << "[DEBUG] "; break;
        case LogLevel::INFO:    std::cout << "[INFO] "; break;
        case LogLevel::WARNING: std::cout << "[WARNING] "; break;
        case LogLevel::ERROR:   std::cout << "[ERROR] "; break;
    }
    
    std::cout << message << std::endl;
}

int main() {
    log("서버 시작");
    log("경고 메시지", LogLevel::WARNING);
    log("에러 발생", LogLevel::ERROR, false);
}

예시 2: 파일 읽기

#include <fstream>
#include <string>

std::string readFile(const std::string& filename, 
                     size_t maxSize = 1024 * 1024,
                     bool binary = false) {
    std::ios::openmode mode = std::ios::in;
    if (binary) {
        mode |= std::ios::binary;
    }
    
    std::ifstream file(filename, mode);
    if (!file) {
        throw std::runtime_error("파일 열기 실패");
    }
    
    std::string content;
    content.reserve(maxSize);
    
    std::string line;
    while (std::getline(file, line) && content.size() < maxSize) {
        content += line + "\n";
    }
    
    return content;
}

int main() {
    auto text = readFile("test.txt");
    auto data = readFile("data.bin", 2048, true);
}

예시 3: 그리기 함수

struct Color {
    int r, g, b;
};

void drawRectangle(int x, int y, 
                   int width = 100, 
                   int height = 100,
                   Color color = {0, 0, 0}) {
    std::cout << "사각형 그리기:" << std::endl;
    std::cout << "  위치: (" << x << ", " << y << ")" << std::endl;
    std::cout << "  크기: " << width << "x" << height << std::endl;
    std::cout << "  색상: RGB(" << color.r << ", " 
              << color.g << ", " << color.b << ")" << std::endl;
}

int main() {
    drawRectangle(10, 20);
    drawRectangle(10, 20, 200);
    drawRectangle(10, 20, 200, 150);
    drawRectangle(10, 20, 200, 150, {255, 0, 0});
}

예시 4: HTTP 요청

#include <string>
#include <map>

class HttpClient {
public:
    std::string request(const std::string& url,
                       const std::string& method = "GET",
                       const std::map<std::string, std::string>& headers = {},
                       const std::string& body = "",
                       int timeout = 30) {
        std::cout << method << " " << url << std::endl;
        std::cout << "Timeout: " << timeout << "s" << std::endl;
        
        for (const auto& [key, value] : headers) {
            std::cout << key << ": " << value << std::endl;
        }
        
        if (!body.empty()) {
            std::cout << "Body: " << body << std::endl;
        }
        
        return "Response";
    }
};

int main() {
    HttpClient client;
    
    client.request("https://api.example.com/users");
    
    client.request("https://api.example.com/users", "POST",
                  {{"Content-Type", "application/json"}},
                  R"({"name": "Alice", "age": 30})");
}

헤더와 구현 분리

// myclass.h
class MyClass {
public:
    void func(int x, int y = 20);  // 기본값은 선언에만
};

// myclass.cpp
void MyClass::func(int x, int y) {  // 정의에는 기본값 없음
    std::cout << x << ", " << y << std::endl;
}

자주 발생하는 문제

문제 1: 순서 위반

// ❌ 중간에 기본값 없음
// void func(int x = 10, int y, int z = 30) {  // 에러
//     // ...
// }

// ✅ 오른쪽부터 기본값
void func(int x, int y = 20, int z = 30) {
    // OK
}

문제 2: 재선언 시 기본값

// ❌ 재선언 시 기본값 중복
void func(int x = 10);

// void func(int x = 10) {  // 에러: 기본값 중복
//     // ...
// }

// ✅ 정의에는 기본값 없음
void func(int x) {
    // OK
}

문제 3: 오버로딩과 충돌

// ❌ 모호함
void func(int x) {
    std::cout << "1개 인자" << std::endl;
}

void func(int x, int y = 0) {
    std::cout << "2개 인자" << std::endl;
}

int main() {
    // func(10);  // 에러: 모호함
    func(10, 20);  // OK
}

문제 4: 포인터 기본값

// ❌ 지역 변수 주소
void func(int* ptr = &localVar) {  // 에러
    // ...
}

// ✅ nullptr 또는 전역 변수
void func(int* ptr = nullptr) {
    if (ptr) {
        // ...
    }
}

기본 인자 vs 오버로딩

// 기본 인자
void func(int x, int y = 20, int z = 30) {
    std::cout << x << ", " << y << ", " << z << std::endl;
}

// 오버로딩
void func(int x) {
    func(x, 20, 30);
}

void func(int x, int y) {
    func(x, y, 30);
}

void func(int x, int y, int z) {
    std::cout << x << ", " << y << ", " << z << std::endl;
}

비교표:

특징기본 인자오버로딩
코드 간결성✅ 간결 (1개 함수)❌ 복잡 (여러 함수)
구현 차이❌ 불가 (같은 구현)✅ 가능 (다른 구현)
타입 차이❌ 불가 (같은 타입)✅ 가능 (다른 타입)
유지보수✅ 쉬움⚠️ 중복 코드
바이너리 크기✅ 작음⚠️ 큼 (여러 함수)

언제 무엇을 사용할까?:

// ✅ 기본 인자 사용
// - 같은 로직, 선택적 매개변수
void log(const std::string& msg, LogLevel level = INFO);

// ✅ 오버로딩 사용
// - 다른 타입, 다른 로직
void print(int x) { std::cout << x; }
void print(const std::string& s) { std::cout << s; }

// ✅ 오버로딩 사용
// - 다른 구현 필요
void process(int x) { /* 정수 처리 */ }
void process(const std::vector<int>& vec) { /* 벡터 처리 */ }

사용 권장사항

// ✅ 기본 인자 사용 권장
// 1. 선택적 매개변수
void connect(const std::string& host, int port = 8080);

// 2. 설정 옵션
void render(bool antialiasing = true, int quality = 100);

// 3. 하위 호환성
void func(int x, int y = 0);  // 기존 코드 호환

// ❌ 기본 인자 지양
// 1. 복잡한 기본값
// 2. 순서가 중요한 경우
// 3. 여러 조합 필요 (오버로딩 사용)

실무 패턴

패턴 1: 설정 함수

class Server {
public:
    void start(const std::string& host = "0.0.0.0",
               int port = 8080,
               int maxConnections = 100,
               bool enableLogging = true) {
        std::cout << "서버 시작: " << host << ":" << port << '\n';
        std::cout << "최대 연결: " << maxConnections << '\n';
        std::cout << "로깅: " << (enableLogging ? "활성" : "비활성") << '\n';
    }
};

// 사용
Server server;
server.start();  // 모두 기본값
server.start("localhost");  // host만 변경
server.start("localhost", 3000);  // host, port 변경

패턴 2: 빌더 대체

class HttpRequest {
public:
    std::string send(const std::string& url,
                     const std::string& method = "GET",
                     const std::string& body = "",
                     int timeout = 30,
                     bool followRedirects = true) {
        // HTTP 요청 로직
        return "Response";
    }
};

// 사용
HttpRequest req;
req.send("https://api.example.com/users");
req.send("https://api.example.com/users", "POST", R"({"name":"Alice"})");

패턴 3: 하위 호환성

// 버전 1.0
void processData(const std::vector<int>& data) {
    // 처리 로직
}

// 버전 2.0: 새 매개변수 추가 (하위 호환 유지)
void processData(const std::vector<int>& data, 
                 bool parallel = false) {
    if (parallel) {
        // 병렬 처리
    } else {
        // 기존 처리
    }
}

// 기존 코드는 그대로 작동
processData(myData);  // OK

FAQ

Q1: 기본 인자는 언제 사용하나요?

A:

  • 선택적 매개변수가 필요할 때
  • 설정 옵션을 제공할 때
  • 하위 호환성을 유지하며 함수를 확장할 때

Q2: 기본 인자 vs 오버로딩?

A:

  • 기본 인자: 같은 로직, 선택적 매개변수 (간단)
  • 오버로딩: 다른 타입 또는 다른 로직 (복잡)

Q3: 기본값은 어디에 지정하나요?

A: 함수 선언에만 지정합니다. 정의(구현)에는 기본값을 생략합니다.

Q4: 기본값 순서는?

A: 오른쪽부터 연속적으로 지정해야 합니다. 중간에 기본값이 없는 매개변수가 있으면 안 됩니다.

Q5: 성능 영향은?

A: 없습니다. 기본 인자는 컴파일 타임에 처리되므로 런타임 오버헤드가 없습니다.

Q6: 기본값으로 함수 호출 결과를 사용할 수 있나요?

A: 가능하지만, 매번 호출됩니다.

int getDefault() {
    std::cout << "getDefault 호출\n";
    return 10;
}

void func(int x = getDefault()) {
    std::cout << "x: " << x << '\n';
}

func();  // "getDefault 호출", "x: 10"
func();  // "getDefault 호출", "x: 10" (매번 호출)

Q7: 기본 인자 학습 리소스는?

A:

관련 글: Function Overloading, Function Basics.

한 줄 요약: 기본 인자는 함수 매개변수에 기본값을 지정하여 선택적 매개변수를 간결하게 구현합니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ 함수 오버로딩 | “Function Overloading” 가이드
  • C++ 함수 | “처음 배우는” 함수 만들기 완벽 가이드 [예제 10개]
  • C++ Type Erasure | “타입 지우기” 패턴 가이드

관련 글

  • C++ 함수 |
  • C++ 함수 오버로딩 |
  • C++ std::function vs 함수 포인터 |
  • C++ 이름 은닉 |
  • C++ inline 함수 |