C++ 기본 인자 | 'Default Arguments' 가이드
이 글의 핵심
기본 인자로 선택적 매개변수를 한 함수 시그니처에 담을 수 있습니다. 선언·재선언 규칙, 오버로딩·가상 함수와 엮일 때의 함정, API 설계 패턴을 정리합니다.
기본 인자란?
기본 인자(default arguments) 는 함수 매개변수에 기본값을 지정하여, 호출 시 인자를 생략할 수 있게 하는 기능입니다. 함수 오버로딩을 대체하여 코드를 간결하게 만들 수 있습니다.
greet 함수의 구현 예제입니다.
void greet(const std::string& name = "Guest") {
std::cout << "Hello, " << name << "!" << std::endl;
}
int main() {
greet("Alice"); // Hello, Alice!
greet(); // Hello, Guest!
}
왜 필요한가?:
- 간결성: 오버로딩 없이 선택적 매개변수 구현
- 하위 호환성: 기존 함수에 새 매개변수 추가 시 기존 코드 유지
- 가독성: 자주 사용하는 값을 기본값으로 설정
connect 함수의 구현 예제입니다.
// ❌ 오버로딩: 코드 중복
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) {
// 연결 로직
}
기본 사용법
func 함수의 구현 예제입니다.
// 단일 기본 인자
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
}
규칙
func 함수의 구현 예제입니다.
// ✅ 오른쪽부터 기본값
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
}
기본 인자 규칙 상세:
- 오른쪽부터 연속: 기본값이 있는 매개변수는 오른쪽부터 연속되어야 합니다.
f1 함수의 구현 예제입니다.
// ✅ 올바른 예시
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만 기본값
- 선언에만 기본값: 헤더 파일의 선언에만 기본값을 지정하고, 구현 파일에는 생략합니다.
func 함수의 구현 예제입니다.
// header.h
void func(int x, int y = 20);
// source.cpp
void func(int x, int y) { // 기본값 없음
std::cout << x << ", " << y << '\n';
}
- 재선언 시 기본값 추가 가능: 같은 매개변수에 대해 재선언 시 기본값을 추가할 수 있지만, 중복 지정은 불가합니다.
func 함수의 구현 예제입니다.
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: 순서 위반
func 함수의 구현 예제입니다.
// ❌ 중간에 기본값 없음
// void func(int x = 10, int y, int z = 30) { // 에러
// // ...
// }
// ✅ 오른쪽부터 기본값
void func(int x, int y = 20, int z = 30) {
// OK
}
문제 2: 재선언 시 기본값
func 함수의 구현 예제입니다.
// ❌ 재선언 시 기본값 중복
void func(int x = 10);
// void func(int x = 10) { // 에러: 기본값 중복
// // ...
// }
// ✅ 정의에는 기본값 없음
void func(int x) {
// OK
}
문제 3: 오버로딩과 충돌
func 함수의 구현 예제입니다.
// ❌ 모호함
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 오버로딩
func 함수의 구현 예제입니다.
// 기본 인자
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개 함수) | ❌ 복잡 (여러 함수) |
| 구현 차이 | ❌ 불가 (같은 구현) | ✅ 가능 (다른 구현) |
| 타입 차이 | ❌ 불가 (같은 타입) | ✅ 가능 (다른 타입) |
| 유지보수 | ✅ 쉬움 | ⚠️ 중복 코드 |
| 바이너리 크기 | ✅ 작음 | ⚠️ 큼 (여러 함수) |
언제 무엇을 사용할까?:
log 함수의 구현 예제입니다.
// ✅ 기본 인자 사용
// - 같은 로직, 선택적 매개변수
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) { /* 벡터 처리 */ }
사용 권장사항
connect 함수의 구현 예제입니다.
// ✅ 기본 인자 사용 권장
// 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: 가능하지만, 매번 호출됩니다.
getDefault 함수의 구현 예제입니다.
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:
- “C++ Primer” by Lippman, Lajoie, Moo
- cppreference.com - Default arguments
- “Effective C++” by Scott Meyers
관련 글: Function Overloading, Function Basics.
한 줄 요약: 기본 인자는 함수 매개변수에 기본값을 지정하여 선택적 매개변수를 간결하게 구현합니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 함수 오버로딩 | “Function Overloading” 가이드
- C++ 함수 | “처음 배우는” 함수 만들기 완벽 가이드 [예제 10개]
- C++ Type Erasure | “타입 지우기” 패턴 가이드
관련 글
심화 부록: 구현·운영 관점
이 부록은 앞선 본문에서 다룬 주제(「C++ 기본 인자 | ‘Default Arguments’ 가이드」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.
내부 동작과 핵심 메커니즘
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
sequenceDiagram participant C as 클라이언트/호출자 participant B as 경계(런타임·게이트웨이·프로세스) participant D as 의존성(API·DB·큐·파일) C->>B: 요청/이벤트 B->>D: 조회·쓰기·RPC D-->>B: 지연·부분 실패·재시도 가능 B-->>C: 응답 또는 오류(코드·상관 ID)
- 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
- 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.
프로덕션 운영 패턴
| 영역 | 운영 관점 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가 |
| 안전성 | 입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가 |
| 성능 | 캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가 |
| 용량 | 피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가 |
스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.
확장 예시: 엔드투엔드 미니 시나리오
앞선 본문 주제(「C++ 기본 인자 | ‘Default Arguments’ 가이드」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
- 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
- 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
- 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
- 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
ctx = newCorrelationId()
validated = validateSchema(request)
authorize(validated, ctx)
result = domainCore(validated)
persistOrEmit(result, idempotentKey)
recordMetrics(ctx, latency, outcome)
return result
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스, 타임아웃, 외부 의존성, DNS | 최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검 |
| 성능 저하 | N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납 | 상한·TTL·힙/FD 스냅샷 비교 |
| 빌드·배포만 실패 | 환경 변수, 권한, 플랫폼 차이, lockfile | CI 로그와 로컬 diff, 런타임·이미지 버전 핀 |
| 설정 불일치 | 프로필·시크릿·기본값, 리전 | 스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화 |
| 데이터 불일치 | 비멱등 재시도, 부분 쓰기, 캐시 무효화 누락 | 멱등 키·아웃박스·트랜잭션 경계 재검토 |
권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.
배포 전에는 git add → git commit → git push 후 npm run deploy 순서를 권장합니다.
이 글에서 다루는 키워드 (관련 검색어)
C++, default-arguments, function, 함수, 기본값 등으로 검색하시면 이 글이 도움이 됩니다.