C++ 기본 인자 규칙: 헤더에만 두는 이유와 가상 함수에서 생기는 함정

이 글의 핵심

기본 인자는 함수 하나로 여러 호출 형태를 지원할 수 있어 편리하지만, 기본값이 호출 지점에서 채워지기 때문에 가상 함수에서는 파생 클래스에 지정한 기본값이 무시될 수 있고, 포인터 기본값이나 오버로딩과 섞이면 호출이 모호해집니다. 오버로딩과 빌더 패턴 중 무엇이 나은지, 하위 호환성을 유지하며 매개변수를 추가하는 방법을 함께 다룹니다.

기본 인자란?

기본 인자(default arguments)는 함수 매개변수에 기본값을 지정하여, 호출 시 인자를 생략할 수 있게 하는 기능입니다. 인자 개수만 다른 오버로드를 여러 개 만드는 대신 함수 하나로 같은 효과를 낼 수 있습니다.

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

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

기본 인자를 이해하는 핵심은 기본값이 호출하는 쪽에서 채워진다는 점입니다. 컴파일러는 greet()를 보면 그 자리에서 greet("Guest")로 바꿔 컴파일하고, greet 함수 자체는 항상 인자 하나를 받는 평범한 함수로 남습니다. 이 사실에서 이 글의 거의 모든 규칙과 함정이 나옵니다. 호출하는 쪽이 기본값을 알아야 하므로 기본값은 호출자가 보는 선언(헤더)에 있어야 하고, 가상 함수에서도 호출 코드가 보는 정적 타입의 기본값이 쓰이며, 기본값을 바꾸면 그 헤더를 포함한 모든 파일을 다시 컴파일해야 바뀐 값이 반영됩니다.

기본 인자를 쓰는 이유는 세 가지 정도입니다. 오버로드를 여러 개 만들지 않고도 선택적 매개변수를 표현할 수 있고, 기존 함수에 매개변수를 추가하면서 기존 호출 코드를 그대로 둘 수 있으며, 자주 쓰는 값을 기본값으로 정해 두면 호출부가 짧아집니다.

// 오버로딩: 전달용 함수가 하나 더 필요
void connect(const std::string& host, int port) {
    // 연결 로직
}

void connect(const std::string& host) {
    connect(host, 8080);
}

// 기본 인자: 함수 하나로 두 호출 형태를 지원
// (위 오버로드와 같은 파일에 두면 connect("h")가 모호해지므로 둘 중 하나만 씁니다)
// void connect(const std::string& host, int port = 8080);

기본 사용법

기본 인자 호출 실행 결과

그림: 실제 실행 결과 — 인자를 하나씩 더 주면 기본값이 앞에서부터 대체된다.

#include <iostream>
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
}

인자는 항상 왼쪽부터 채워지므로, y는 기본값으로 두고 z만 지정하는 호출은 할 수 없습니다. z를 바꾸려면 y도 함께 넘겨야 합니다.

규칙

오른쪽부터 연속으로 지정

기본값이 있는 매개변수는 오른쪽 끝부터 연속되어야 합니다. 기본값이 있는 매개변수 오른쪽에 기본값이 없는 매개변수가 오면 에러입니다.

// 올바른 예시
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);  // 에러: 기본값 있는 a 오른쪽에 기본값 없는 b
// void f4(int a = 1, int b, int c);      // 에러: a만 기본값

기본값은 선언에 한 번만

헤더 파일의 선언에 기본값을 지정하고, 구현 파일의 정의에서는 생략합니다.

// header.h
void func(int x, int y = 20);

// source.cpp
#include "header.h"
void func(int x, int y) {  // 기본값 없음
    std::cout << x << ", " << y << '\n';
}

재선언에서 기본값 추가

같은 스코프의 재선언에서 아직 기본값이 없는 매개변수에 기본값을 추가할 수는 있지만, 이미 기본값이 있는 매개변수에 다시 지정할 수는 없습니다(값이 같아도 에러입니다).

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에 기본값 추가 (z는 이미 기본값이 있으므로 OK)

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

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

실무에서 이 기능을 일부러 쓸 일은 드뭅니다. 기본값이 여러 선언에 흩어지면 호출 지점마다 어떤 기본값이 보이는지가 달라지기 때문에, 기본값은 첫 선언 한 곳에 모으는 편이 안전합니다.

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

기본 인자는 가상으로 동작하지 않습니다. 가상 함수를 호출할 때도 어떤 기본값이 쓰이는지는 컴파일 타임에, 호출에 사용한 정적 타입으로 정해집니다.

#include <iostream>
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의 기본값(파생 쪽 기본값 아님)
}

b.f()의 결과가 “D 1”인 이유는 두 결정이 서로 다른 시점에 이뤄지기 때문입니다. 어떤 함수 본문을 실행할지는 런타임에 vtable로 정해져 Derived::f가 선택되지만, 생략된 인자에 무엇을 넣을지는 컴파일 타임에 b의 선언 타입 Base&를 보고 정해져 1이 들어갑니다. 결과적으로 파생 클래스의 코드가 베이스 클래스의 기본값으로 실행되는, 어느 쪽 작성자도 의도하지 않은 조합이 됩니다. Scott Meyers의 “Effective C++” 항목 37이 “상속받은 기본 인자 값은 절대로 재정의하지 말라”고 권하는 이유이며, clang-tidy의 google-default-arguments 검사는 가상 함수에 기본 인자를 두는 것 자체를 경고합니다.

그래서 공개 API에서는 기본값을 베이스 선언에만 두고 파생 클래스에서는 생략하거나, 기본값을 비가상 공개 함수가 처리하고 그 함수가 가상 함수를 호출하는 NVI(Non-Virtual Interface) 구조를 쓰는 편이 안전합니다. 파생 클래스에서 기본값을 생략하면 Derived 타입으로 직접 호출할 때 인자를 생략할 수 없게 되는데, 이 불편함이 오히려 “기본값은 한 곳에서만 정한다”는 규칙을 강제해 줍니다.

함수 오버로딩과의 관계

기본 인자는 호출 시 인자 개수를 줄여 주는 문법일 뿐, 별도의 오버로드가 생기는 것은 아닙니다. 하지만 오버로드 해석에서는 기본 인자를 가진 함수가 더 적은 인자로도 후보가 되기 때문에, 같은 인자 개수를 받는 다른 오버로드와 겹치면 호출이 모호해집니다. 예를 들어 f(int)와 f(int, int = 0)가 함께 있으면 f(0)은 두 후보가 똑같이 잘 맞아 컴파일 에러가 됩니다(아래 “문제 3” 참고). 템플릿이나 enable_if로 제약을 건 오버로드와 섞을 때도 어떤 호출이 어디로 가는지 한눈에 보이지 않는다면, 기본 인자보다 명시적인 오버로드가 읽기 쉽습니다.

그 밖의 주의점

옵션이 많아지면 기본 인자를 길게 나열하기보다 옵션 구조체(struct Options)나 빌더 패턴을 쓰는 편이 낫습니다. 이 부분은 아래 “실무 패턴”에서 다시 다룹니다.

람다의 매개변수에도 기본 인자를 둘 수 있습니다. C++11 표준은 람다의 기본 인자를 금지했지만(많은 컴파일러가 확장으로 허용하긴 했습니다) C++14에서 이 제한이 없어졌습니다. 반면 함수 포인터나 std::function 타입에는 기본 인자 정보가 담기지 않으므로, 이들을 통해 호출할 때는 모든 인자를 넘겨야 합니다.

기본 인자 표현식은 인자를 생략한 호출마다 매번 평가됩니다. get_id()처럼 부작용이 있는 함수를 기본값으로 쓰면 생략 호출마다 실행되므로, 한 번만 평가되어야 한다면 기본값을 std::nullopt 등으로 두고 본문에서 처리하세요.

다른 매개변수를 기본값에 쓸 수도 없습니다. void f(int x, int y = x);는 컴파일 에러입니다. 함수 인자의 평가 순서가 정해져 있지 않으므로 표준이 기본 인자에서 매개변수를 쓰는 것을 금지하며, 멤버 함수의 기본값에 this를 쓰는 것도 같은 이유로 금지됩니다. 이런 동작이 필요하면 void f(int x) { f(x, x); }처럼 두 인자를 받는 버전으로 전달하는 오버로드를 만듭니다.

생성자의 기본 인자와 암시적 변환

기본 인자는 생성자에서 특히 조심해야 합니다. 모든 매개변수에 기본값이 있는 생성자는 기본 생성자가 되고, 인자 하나로 호출할 수 있게 된 생성자는 변환 생성자가 됩니다.

class Buffer {
public:
    Buffer(std::size_t size = 4096, bool zeroed = false);
};

void send(const Buffer& b);

Buffer a;        // 기본 생성자: Buffer(4096, false)
send(64);        // 컴파일됨: 64가 Buffer(64, false)로 조용히 변환
Buffer c = 128;  // 복사 초기화도 통과

작성자는 “크기의 기본값”과 “0으로 채우기 옵션”을 원했을 뿐인데, 그 결과 모든 정수가 Buffer로 암시적 변환되는 문이 열렸습니다. 다른 오버로드를 부르려던 send(bytesWritten)이 임시 64바이트 버퍼를 만들어 넘기는 식의 버그가 여기서 나옵니다. 생성자에 explicit을 붙이면 Buffer a;와 Buffer b(64);는 그대로 동작하고 send(64)와 Buffer c = 128;만 컴파일 에러가 됩니다. 두 번째 매개변수에 기본값을 추가하는 순간 이 생성자가 “인자 하나로 호출 가능한 생성자”가 된다는 점을 놓치기 쉬우므로, 생성자에 기본 인자를 추가하는 변경은 explicit 여부와 함께 검토하는 것이 좋습니다.

템플릿에서는 사용할 때만 인스턴스화

함수 템플릿의 기본 인자 표현식은 실제로 그 기본값을 쓰는 호출이 있을 때만 인스턴스화됩니다.

template <typename T>
void fill(std::vector<T>& v, const T& value = T{}) {
    std::fill(v.begin(), v.end(), value);
}

struct NoDefault { explicit NoDefault(int) {} };

std::vector<NoDefault> v(3, NoDefault{1});
fill(v, NoDefault{2});   // OK: T{}는 인스턴스화되지 않음
// fill(v);              // 여기서만 에러: NoDefault에 기본 생성자가 없음

덕분에 기본 생성자가 없는 타입에도 이 템플릿을 쓸 수 있지만, 반대로 기본값 표현식에 문제가 있어도 템플릿을 작성할 때는 드러나지 않고, 누군가 처음으로 인자를 생략해 호출할 때 헤더 안쪽을 가리키는 에러로 나타납니다. 템플릿 매개변수의 기본값(template <typename T = int>)은 이와 별개의 기능이며, 함수 인자가 아니라 추론되지 않은 템플릿 매개변수를 채우는 데 쓰입니다.

실전 예시

예시 1: 로그 함수

#include <iostream>
#include <string>
#include <chrono>
#include <ctime>

enum class LogLevel {
    Debug,
    Info,
    Warning,
    Error
};

void writeLog(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);
        char buf[32];
        std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", std::localtime(&time));
        std::cout << buf << " ";
    }

    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() {
    writeLog("서버 시작");
    writeLog("경고 메시지", LogLevel::Warning);
    writeLog("에러 발생", LogLevel::Error, false);
}

함수 이름을 log로 두면 <cmath>의 log와 이름이 겹치고, 열거자를 ERROR로 두면 Windows 헤더의 ERROR 매크로와 충돌할 수 있어 여기서는 피했습니다. std::ctime은 결과 끝에 개행 문자를 붙이므로 한 줄짜리 로그에는 std::strftime이 더 맞습니다.

예시 2: 파일 읽기

#include <fstream>
#include <stdexcept>
#include <string>

std::string readFile(const std::string& filename,
                     std::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);
}

예제 2처럼 기본값이 “최대 크기”를 뜻하는 경우에는 기본값이 곧 동작 정책이 된다는 점을 의식해야 합니다. 이 코드는 인자를 생략하면 파일 크기와 상관없이 1MB를 reserve하므로, 작은 설정 파일을 수백 개 읽는 코드에서는 불필요한 메모리 할당이 반복됩니다. 또 getline으로 줄 단위로 읽기 때문에 binary = true여도 원래 줄 끝이 무엇이었든 \n을 붙여 다시 조립하므로 바이너리 데이터가 깨질 수 있고, maxSize도 줄 단위로만 검사하므로 마지막 줄 때문에 한도를 넘을 수 있습니다. 기본 인자는 편하지만 호출하는 쪽에서 값이 보이지 않으므로, 이렇게 동작에 큰 영향을 주는 값일수록 이름 있는 상수(constexpr std::size_t kDefaultMaxSize)로 빼서 문서화해 두는 편이 좋습니다.

예시 3: 그리기 함수

#include <iostream>
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});
}

색상만 바꾸고 싶어도 width와 height를 함께 넘겨야 한다는 점이 이 방식의 한계입니다. 이런 함수가 늘어나면 아래 “실무 패턴”의 옵션 구조체가 더 잘 맞습니다.

예시 4: HTTP 요청

#include <iostream>
#include <map>
#include <string>

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: 순서 위반

// 에러: 기본값 있는 x 오른쪽에 기본값 없는 y
// void func(int x = 10, int y, int z = 30) { }

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

문제 2: 재선언 시 기본값

void func(int x = 10);

// 에러: 같은 매개변수에 기본값 재지정 (값이 같아도 안 됨)
// void func(int x = 10) { }

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

헤더를 포함한 .cpp의 정의에 같은 기본값을 다시 쓰면 GCC는 “redefinition of default argument” 에러를 냅니다.

문제 3: 오버로딩과 충돌

#include <iostream>
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 outer() {
    int localVar = 0;
    // 에러: 지역 변수는 기본 인자에 쓸 수 없음
    // void inner(int* ptr = &localVar);
}

// OK: 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, int y, int z) {
    std::cout << x << ", " << y << ", " << z << std::endl;
}

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

void func(int x) {
    func(x, 20, 30);
}
특징기본 인자오버로딩
코드 양함수 1개전달 함수가 여러 개
인자 개수별로 다른 구현불가 (같은 본문)가능
매개변수 타입 차이불가가능
기본값 변경 시호출하는 쪽을 다시 컴파일해야 반영라이브러리 쪽만 다시 빌드하면 반영

전달 함수(void connect(const std::string& h) { connect(h, 8080); })는 보통 인라인되므로 오버로딩이라고 해서 바이너리가 눈에 띄게 커지지는 않습니다. 오히려 기본 인자에는 표의 마지막 줄 같은 비용이 있습니다. 기본값이 호출하는 쪽의 기계어에 박히기 때문에, 헤더의 기본값을 8080에서 9090으로 바꿔도 새 헤더로 다시 컴파일되지 않은 오브젝트 파일이나 이미 배포된 바이너리는 여전히 8080을 넘깁니다. 기본값을 라이브러리 쪽에서 바꿀 수 있어야 하는 공개 API라면 전달 오버로드가 더 안전하고, 항상 함께 다시 빌드되는 내부 코드라면 기본 인자가 더 간단합니다.

정리하면, 같은 로직에 선택적 매개변수를 붙이는 경우에는 기본 인자를, 매개변수 타입이 다르거나 인자 개수에 따라 구현이 달라지는 경우에는 오버로딩을 씁니다.

// 기본 인자: 같은 로직, 선택적 매개변수
void writeLog(const std::string& msg, LogLevel level = LogLevel::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: 설정 함수

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: 하위 호환성

// 버전 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

이 “하위 호환”은 소스 호환에 한정된다는 점을 알아야 합니다. 기존 호출 코드를 다시 컴파일하면 문제없이 동작하지만, 바이너리 수준에서는 processData(const vector<int>&)와 processData(const vector<int>&, bool)이 이름 맹글링 결과가 다른 별개의 함수입니다. 그래서 공유 라이브러리(.so, .dll)의 함수에 기본 인자로 매개변수를 추가하면, 예전 버전으로 빌드된 실행 파일은 사라진 옛 심볼을 찾지 못해 로드 단계에서 “undefined symbol” 같은 에러로 실행되지 않습니다. 라이브러리 쪽 코드만 보면 “기본값을 줬으니 호환된다”고 판단하기 쉬운 부분이므로, ABI를 유지해야 하는 라이브러리라면 기존 시그니처를 그대로 남기고 새 매개변수를 받는 오버로드를 따로 추가하는 편이 안전합니다.

패턴 3: 옵션 구조체

매개변수가 늘어나면서 bool 인자가 여럿 붙기 시작하면 start("localhost", 3000, 100, false)처럼 호출부만 보고는 false가 무엇인지 알 수 없게 됩니다. 또 앞의 예시들처럼 뒤쪽 인자 하나만 바꾸려 해도 앞의 인자를 모두 적어야 합니다. 이런 경우에는 기본 인자를 계속 늘리기보다 기본값을 멤버 초기화로 가진 옵션 구조체를 받는 편이 읽기 쉽습니다.

struct ServerOptions {
    std::string host = "0.0.0.0";
    int port = 8080;
    int maxConnections = 100;
    bool enableLogging = true;
};

void start(const ServerOptions& opts = {});

start();                                        // 모두 기본값
start({.port = 3000, .enableLogging = false});  // C++20 지정 초기화

C++20의 지정 초기화는 멤버 선언 순서대로만 쓸 수 있고(.enableLogging을 .port보다 앞에 쓰면 에러), 생략한 멤버는 기본 멤버 초기화 값으로 채워집니다.

FAQ

헤더의 기본값을 바꿨는데 일부 동작이 예전 값 그대로입니다. 왜 그런가요?

기본값은 호출하는 쪽을 컴파일할 때 그 자리에 채워지기 때문입니다. 바뀐 헤더로 다시 컴파일되지 않은 오브젝트 파일이나, 그 헤더를 보고 미리 빌드된 다른 라이브러리는 예전 값을 계속 넘깁니다. 빌드 시스템의 의존성 추적이 헤더를 놓쳤거나 미리 빌드된 바이너리를 섞어 쓸 때 생기며, 깨끗하게 다시 빌드하면 해결됩니다.

성능 영향은?

기본 인자 자체에는 런타임 비용이 없습니다. 컴파일러가 호출 지점에 값을 채워 넣을 뿐이므로 인자를 직접 쓴 호출과 같은 코드가 됩니다. 다만 기본값 표현식이 함수 호출이라면, 인자를 생략할 때마다 그 함수가 실행되는 비용은 있습니다.

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

가능하지만, 인자를 생략할 때마다 매번 호출됩니다.

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" (매번 호출)

규칙을 정확히 확인하려면 어디를 보나요?

cppreference.com - Default arguments에 재선언 규칙, 가상 함수, 템플릿 관련 규칙이 예제와 함께 정리되어 있습니다.


같이 보면 좋은 글