C++11 중괄호 초기화: narrowing 방지, Most Vexing Parse, vector<int>{10}과 (10)의 차이
이 글의 핵심
Widget w();가 객체가 아니라 함수 선언으로 해석되는 문제는 중괄호 초기화로 사라지지만, 대신 initializer_list 생성자가 있으면 그쪽이 우선 선택되는 새로운 함정이 생깁니다. 이 글은 = 초기화와 {} 초기화의 차이, 빈 중괄호의 의미, 멤버·집합 초기화에서의 동작을 비교하고 언제 ()를 유지하는 편이 나은지 기준을 제시합니다.
균일 초기화란?
균일 초기화(uniform initialization) 는 C++11에서 도입된 모든 타입을 {}로 초기화하는 일관된 문법입니다. 기본 타입, 배열, 구조체, 클래스, 컨테이너 등 모든 타입에 동일한 문법을 사용할 수 있습니다.
// C++03 이전: 다양한 초기화 방법
// 변수 선언 및 초기화
int x = 10;
int arr[] = {1, 2, 3};
std::vector<int> vec;
vec.push_back(1);
// C++11: 균일 초기화
int x{10};
int arr[]{1, 2, 3};
std::vector<int> vec{1, 2, 3};
왜 필요한가?:
- 일관성: 모든 타입에 동일한 문법
- 안전성: narrowing 변환 방지
- 명확성: Most Vexing Parse 해결
- 편의성: 컨테이너 초기화 간소화
초기화 방법 비교:
| 초기화 방법 | 문법 | narrowing | Most Vexing Parse |
|---|---|---|---|
| 복사 초기화 | int x = 10; | ⚠️ 허용 | - |
| 직접 초기화 | int x(10); | ⚠️ 허용 | ⚠️ T x();처럼 쓰면 발생 |
| 균일 초기화 | int x{10}; | ✅ 방지 | ✅ 해결 |
C++03까지는 초기화 문법이 대상에 따라 달랐습니다. 기본 타입은 =나 (), 배열과 C 구조체는 = {...}, 클래스는 생성자 괄호, 컨테이너는 빈 객체를 만든 뒤 push_back을 반복하는 식이었습니다. 특히 클래스 멤버 배열이나 new로 만든 배열은 원소를 초기화할 방법이 아예 없었습니다. C++11의 중괄호 초기화(표준 용어로는 리스트 초기화)는 이 모든 경우에 쓸 수 있는 하나의 문법을 만들었다는 점에서 “균일”이라는 이름이 붙었습니다. 다만 뒤에서 보듯 완전히 균일하지는 않아서, 같은 중괄호가 타입에 따라 다른 규칙으로 해석되는 경우가 있습니다. 이 글의 대부분은 그 예외를 이해하는 데 할애합니다.
기본 사용법
// 기본 타입
int x{10};
double y{3.14};
char c{'A'};
// 배열
int arr[]{1, 2, 3, 4, 5};
// 구조체
struct Point {
int x, y;
};
Point p{10, 20};
// 클래스
std::string s{"Hello"};
std::vector<int> vec{1, 2, 3};
narrowing 방지
// ❌ narrowing 에러
int x{3.14}; // 컴파일 에러: double -> int
// ✅ 명시적 변환
int x{static_cast<int>(3.14)};
// 일반 초기화는 허용 (컴파일러·옵션에 따라 경고)
int y = 3.14; // OK, y는 3
=나 ()로 초기화할 때의 암시적 변환 규칙은 C에서 물려받은 것이라 소수점 이하가 조용히 잘립니다. GCC와 Clang은 -Wconversion(또는 -Wfloat-conversion)을 켜야 경고를 내고, 기본 설정에서는 아무 말도 하지 않는 경우가 많습니다. 반면 중괄호 초기화에서 narrowing은 경고가 아니라 에러라는 것이 표준에 정해져 있습니다(GCC는 역사적인 이유로 일부 경우를 경고로만 처리하기도 해서 -Werror=narrowing을 함께 켜 두면 확실합니다).
narrowing 변환 상세:
균일 초기화는 다음과 같은 정보 손실 변환을 컴파일 에러로 막습니다:
// ❌ 부동소수점 → 정수
double d = 3.14;
int x{d}; // 에러
// ❌ 큰 정수 → 작은 정수
long long big = 1000000000000LL;
int y{big}; // 에러
// ❌ 정수 → 부동소수점 (정밀도 손실 가능)
int large = 16777217;
float f{large}; // 에러 (변수라서 값과 무관하게 narrowing으로 판정)
// ❌ 부호 있는 정수 → 부호 없는 정수 (음수)
int negative = -1;
unsigned int u{negative}; // 에러
// ✅ 명시적 변환으로 해결
int x2{static_cast<int>(d)};
int y2{static_cast<int>(big)};
float f2{static_cast<float>(large)};
unsigned int u2{static_cast<unsigned int>(negative)};
narrowing 판정에서 중요한 것은 값이 상수 표현식인가입니다. 초기값이 컴파일 시점에 알려진 상수이고 대상 타입에 정확히 들어가면 narrowing이 아닙니다. 그래서 char c{65};나 unsigned u{10};은 괜찮지만, int n = 65; char c{n};은 n이 실제로 65여도 에러입니다. 컴파일러가 실행 시점의 값을 알 수 없으니 “들어가지 않을 수도 있다”로 판정하는 것입니다. 위 예제의 float f{large}도 16777217이라는 값이 문제가 아니라 large가 const가 아닌 변수라서 에러가 나며, const int small = 100; float f{small};처럼 상수이고 float로 정확히 표현되는 값이면 통과합니다.
이 규칙이 실제로 가장 도움이 되는 곳은 템플릿과 제네릭 코드입니다. std::vector<int> v{a, b, c}에서 a가 double이거나 size_t인 경우, =였다면 조용히 변환되었을 값이 컴파일 에러로 드러납니다. 반대로 기존 코드를 중괄호로 바꾸다 보면 int 인덱스를 size_t 자리에 넣던 수많은 곳에서 에러가 쏟아지는데, 이는 대부분 원래부터 있던 부호 혼용을 드러내는 것이라 하나씩 명시적으로 정리할 가치가 있습니다.
실무 활용:
// ✅ API 파라미터 타입 안전성
void setVolume(int volume) {
// volume: 0~100
}
double userInput = 75.5;
// setVolume({userInput}); // 컴파일 에러 (narrowing)
setVolume(static_cast<int>(userInput)); // 명시적 의도
// ✅ 설정 파일 파싱
int parsePort(const std::string& value) {
double parsed = std::stod(value);
// return {parsed}; // 에러: double → int
return static_cast<int>(parsed); // 명시적
}
setVolume({userInput})처럼 함수 인자에 중괄호를 쓰면 매개변수를 리스트 초기화하므로 narrowing 검사가 함수 호출에도 적용됩니다. return {parsed};도 반환값을 리스트 초기화하므로 같은 검사를 받습니다. 다만 static_cast<int>는 narrowing을 허락할 뿐 값을 검증하지는 않습니다. parsePort에 "70000"이나 "abc"가 들어오면 포트 범위를 벗어난 값이 그대로 돌아가거나 std::stod가 std::invalid_argument 예외를 던집니다. 에러가 컴파일러에서 사라졌다고 문제가 해결된 것은 아니므로, 변환 전에 범위를 확인하고, 정수 문자열이라면 std::stoi나 std::from_chars로 처음부터 정수로 파싱하는 편이 낫습니다.
Most Vexing Parse 해결
class Widget {
public:
Widget() {}
};
// ❌ Most Vexing Parse
Widget w(); // 함수 선언으로 해석!
// ✅ 균일 초기화
Widget w{}; // 객체 생성
Most Vexing Parse란?:
C++에서 ()는 함수 선언과 객체 생성 모두에 사용될 수 있어, 컴파일러가 함수 선언으로 해석하는 문제입니다. 문법 규칙상 “선언으로 해석될 수 있는 것은 선언으로 해석한다”는 원칙이 있어서, 모호할 때 컴파일러는 항상 함수 선언 쪽을 택합니다. Widget w();는 “인자가 없고 Widget을 반환하는 함수 w의 선언”으로 완벽하게 유효한 문장이라 에러 없이 컴파일되고, 나중에 w.method()를 호출하는 줄에서야 request for member 'method' in 'w', which is of non-class type 'Widget()' 같은 엉뚱한 위치의 에러가 납니다. 엄밀히 말하면 빈 괄호 사례는 “vexing parse”의 가장 단순한 형태이고, Scott Meyers가 “most vexing”이라고 부른 것은 아래의 istream_iterator 예제처럼 인자 자체가 매개변수 선언으로 읽히는 경우입니다.
class Timer {
public:
Timer() { std::cout << "Timer 생성\n"; }
};
int main() {
Timer t(); // ❌ 함수 선언: Timer t();
// t.start(); // 에러: t는 함수
Timer t2{}; // ✅ 객체 생성
// t2.start(); // OK
}
더 복잡한 예시:
#include <fstream>
#include <iterator>
#include <algorithm>
int main() {
std::ifstream file("data.txt");
// ❌ Most Vexing Parse
std::vector<int> data(
std::istream_iterator<int>(file),
std::istream_iterator<int>()
);
// data는 함수 선언으로 해석됨!
// ✅ 해결 방법 1: 중괄호
std::vector<int> data{
std::istream_iterator<int>(file),
std::istream_iterator<int>()
};
// ✅ 해결 방법 2: 추가 괄호
std::vector<int> data2(
(std::istream_iterator<int>(file)),
(std::istream_iterator<int>())
);
}
첫 번째 선언이 함수가 되는 이유는 std::istream_iterator<int>(file)이 “file이라는 이름의 istream_iterator<int> 매개변수”로, std::istream_iterator<int>()가 “istream_iterator<int>를 반환하는 이름 없는 함수 포인터 매개변수”로 읽히기 때문입니다. 결과적으로 data는 두 매개변수를 받아 vector<int>를 반환하는 함수 선언이 됩니다. 중괄호는 선언 문법에 쓰일 수 없으므로 이 모호성이 원천적으로 사라집니다. 추가 괄호로 감싸는 방법은 C++11 이전에 쓰던 관용구로, (expr) 형태는 매개변수 선언이 될 수 없다는 점을 이용합니다. (예제에서 data를 두 번 선언한 것은 비교를 위한 것이라 실제로는 한쪽만 써야 컴파일됩니다.)
중괄호 해결책이 여기서 문제없이 동작하는 이유도 짚어 둘 만합니다. vector<int>는 initializer_list<int> 생성자를 갖고 있어서 중괄호를 보면 먼저 그 생성자를 시도하는데, istream_iterator는 int로 변환되지 않으므로 그 후보가 탈락하고 반복자 범위 생성자가 선택됩니다. 만약 요소 타입이 반복자로부터 변환 가능한 타입이었다면 전혀 다른 생성자가 선택되었을 것입니다. 이 “initializer_list 우선” 규칙이 뒤에서 다룰 함정의 근원입니다.
실전 예시
예시 1: 컨테이너 초기화
#include <vector>
#include <map>
#include <set>
int main() {
// vector
std::vector<int> numbers{1, 2, 3, 4, 5};
// map
std::map<std::string, int> ages{
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35}
};
// set
std::set<int> uniqueNumbers{5, 2, 8, 1, 9};
// 중첩 컨테이너
std::vector<std::vector<int>> matrix{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
}
예시 2: 구조체/클래스
struct Person {
std::string name;
int age;
double height;
};
class Rectangle {
private:
int width, height;
public:
Rectangle(int w, int h) : width{w}, height{h} {}
int area() const {
return width * height;
}
};
int main() {
// 구조체
Person p{"Alice", 30, 165.5};
// 클래스
Rectangle rect{10, 20};
std::cout << rect.area() << std::endl; // 200
}
예시 3: 동적 할당
#include <memory>
int main() {
// new
int* ptr = new int{42};
delete ptr;
// 배열
int* arr = new int[5]{1, 2, 3, 4, 5};
delete[] arr;
// 스마트 포인터
auto unique = std::make_unique<int>(42);
auto shared = std::make_shared<std::string>("Hello");
// 벡터
auto vec = std::make_unique<std::vector<int>>(
std::initializer_list<int>{1, 2, 3, 4, 5}
);
}
마지막 예제에서 std::initializer_list<int>{...}를 명시한 이유는 make_unique와 make_shared가 인자를 템플릿으로 완벽 전달하기 때문입니다. std::make_unique<std::vector<int>>({1, 2, 3})처럼 중괄호 목록을 그대로 넘기면, 중괄호 목록은 타입이 없어서 템플릿 인자 추론이 실패해 컴파일 에러가 납니다. 또 make_unique는 내부적으로 new T(std::forward<Args>(args)...)처럼 괄호로 객체를 만들기 때문에, make_unique<std::vector<int>>(10)은 원소 10개짜리 벡터가 됩니다. C++20부터는 괄호로도 집합체를 초기화할 수 있게 되어(Point p(1, 2);) make_unique<Point>(1, 2)가 동작하지만, 그 이전에는 집합체 구조체에 make_unique를 쓰려면 생성자를 따로 만들어야 했습니다.
예시 4: 반환값
#include <vector>
std::vector<int> getNumbers() {
return {1, 2, 3, 4, 5};
}
struct Point {
int x, y;
};
Point getOrigin() {
return {0, 0};
}
int main() {
auto numbers = getNumbers();
auto origin = getOrigin();
}
= vs {}
// = 초기화
int x = 10;
int y = 3.14; // narrowing 허용 (경고)
// {} 초기화
int a{10};
// int b{3.14}; // 에러: narrowing
// 복사 초기화
std::string s1 = "Hello"; // OK
std::string s2{"Hello"}; // OK
// 직접 초기화
std::vector<int> v1(10); // 10개 요소 (기본값)
std::vector<int> v2{10}; // 1개 요소 (값 10)
표준 문서에서 중괄호 초기화의 공식 이름은 리스트 초기화(list initialization)이고, “균일 초기화”는 C++11 도입 당시의 별칭입니다. 리스트 초기화는 다시 두 형태로 나뉩니다.
| 문법 | 표준 용어 | 비고 |
|---|---|---|
T x{args} | 직접 리스트 초기화 | 인자에 따라 집합체 초기화, 생성자 호출, initializer_list 생성자 중 하나 |
T x = {args} | 복사 리스트 초기화 | 규칙은 같지만 explicit 생성자가 선택되면 에러 |
T x{} | 리스트 초기화 + 값 초기화 | 기본 생성자 또는 0 초기화 (값 초기화 참고) |
두 형태의 차이는 explicit에서 드러납니다. explicit Widget(int)만 있는 클래스에서 Widget w{10};은 되지만 Widget w = {10};은 컴파일 에러입니다. 함수 인자로 f({10})을 넘기거나 return {10};으로 반환할 때도 복사 리스트 초기화라서 같은 제약을 받습니다. 반대로 T x;처럼 중괄호가 없는 선언은 리스트 초기화가 아니라 기본 초기화이므로, 지역 변수의 기본 타입 멤버는 쓰레기 값으로 남습니다. “중괄호로 통일”한다면 최소한 T x{}까지는 쓰는 습관이 안전합니다.
초기화 리스트 우선순위
class MyClass {
public:
MyClass(int x) {
std::cout << "int 생성자" << std::endl;
}
MyClass(std::initializer_list<int> list) {
std::cout << "initializer_list 생성자" << std::endl;
}
};
int main() {
MyClass obj1(10); // int 생성자
MyClass obj2{10}; // initializer_list 생성자 (우선!)
}
중괄호로 객체를 만들 때 컴파일러는 두 단계로 생성자를 고릅니다. 먼저 initializer_list 생성자만 후보로 놓고, 중괄호 안의 모든 원소가 그 요소 타입으로 변환될 수 있으면 다른 생성자가 더 잘 맞아도 그쪽을 선택합니다. 변환이 불가능할 때만 두 번째 단계로 넘어가 일반 생성자를 모두 비교합니다. obj2{10}에서 10은 int로 변환되므로 1단계에서 결정됩니다.
이 규칙에서 특히 나쁜 경우는 1단계에서 선택된 생성자가 narrowing을 요구하는 때입니다. 예를 들어 initializer_list<bool> 생성자와 (int, double) 생성자가 있을 때 Widget w{10, 5.0};은 (int, double)이 완벽히 맞는데도 initializer_list<bool>이 먼저 선택되고, 10과 5.0을 bool로 좁히는 변환이라 컴파일 에러가 납니다. 반대로 요소 타입으로 아예 변환이 불가능하면 1단계가 건너뛰어집니다. std::vector<std::string> v{10};은 int가 string으로 변환되지 않아서 initializer_list 생성자가 탈락하고, 크기 생성자가 선택되어 빈 문자열 10개짜리 벡터가 됩니다. vector<int>{10}과 vector<string>{10}의 결과가 완전히 다른 셈이라, 템플릿 코드에서 T{n}으로 컨테이너를 만들면 T에 따라 동작이 바뀝니다. 표준 라이브러리 구현이 내부적으로 중괄호 대신 괄호를 선호하는 이유가 이것입니다.
클래스를 설계하는 입장에서도 교훈이 있습니다. 이미 여러 생성자가 있는 클래스에 나중에 initializer_list 생성자를 추가하면, 기존에 Widget{a, b}로 다른 생성자를 호출하던 코드가 조용히 새 생성자로 옮겨 갈 수 있습니다. initializer_list 생성자는 처음 설계할 때 넣거나, 넣는다면 기존 호출부를 모두 점검해야 합니다.
자주 발생하는 문제
문제 1: vector 크기 vs 값
// ❌ 의도와 다름
std::vector<int> v1(10); // 10개 요소 (0으로 초기화)
std::vector<int> v2{10}; // 1개 요소 (값 10)
// ✅ 명확하게
std::vector<int> v3(10, 0); // 10개, 모두 0
std::vector<int> v4{1, 2, 3}; // 3개 요소
코드 리뷰에서 가장 자주 발견되는 형태는 std::vector<int> buffer{size};입니다. 작성자는 size개짜리 버퍼를 의도했지만 실제로는 값 size 하나를 가진 벡터가 만들어집니다. size가 size_t 변수라면 int로 좁히는 변환이라 컴파일 에러가 나서 오히려 다행인데, int 변수라면 에러 없이 크기 1짜리 벡터가 되고 이후 buffer[i] 접근에서 범위를 벗어난 메모리를 건드립니다. 크기와 초기값을 지정하는 생성자는 괄호로 부르는 것을 팀 규칙으로 두는 편이 안전합니다.
문제 2: auto와 함께
// ❌ 의도하지 않은 타입
auto x = {1, 2, 3}; // std::initializer_list<int>
// ✅ 명시적 타입
std::vector<int> y = {1, 2, 3};
auto와 중괄호의 조합은 C++ 버전에 따라 규칙이 달라져 더 헷갈립니다. C++17부터 auto x = {1};(복사 리스트 초기화)은 std::initializer_list<int>이지만 auto x{1};(직접 리스트 초기화)은 int입니다. auto x{1, 2};는 C++17에서 에러입니다. C++11/14에서는 auto x{1};도 initializer_list<int>였기 때문에 오래된 자료와 설명이 다를 수 있습니다. initializer_list는 원소 배열을 참조하는 가벼운 뷰이므로, 함수에서 return {1, 2, 3};으로 initializer_list를 반환하면 임시 배열이 사라진 뒤의 메모리를 가리키게 됩니다. auto로 받은 initializer_list를 오래 들고 다니지 말고 필요하면 std::vector나 std::array로 바꾸는 것이 안전합니다.
문제 3: narrowing
// ❌ narrowing 에러
int x{3.14};
char c{1000};
// ✅ 명시적 변환
int x{static_cast<int>(3.14)};
char c{static_cast<char>(1000)};
문제 4: 빈 초기화
class MyClass {
public:
MyClass() {
std::cout << "기본 생성자" << std::endl;
}
MyClass(std::initializer_list<int> list) {
std::cout << "initializer_list 생성자" << std::endl;
}
};
int main() {
MyClass obj1; // 기본 생성자
MyClass obj2{}; // 기본 생성자
MyClass obj3({}); // initializer_list 생성자 (빈 리스트)
MyClass obj4{{}}; // initializer_list 생성자 (원소 하나: 0)
}
빈 중괄호 {}는 initializer_list 우선 규칙의 예외입니다. 표준은 빈 중괄호이고 기본 생성자가 있으면 값 초기화, 즉 기본 생성자를 호출하도록 정해 두었습니다. 빈 initializer_list를 넘기고 싶다면 obj3({})처럼 괄호 안에 빈 중괄호를 인자로 넣어야 합니다. obj4{{}}는 비슷해 보이지만 바깥 중괄호가 리스트이고 안쪽 {}가 그 첫 원소로 해석되어, 값 초기화된 int 0 하나를 가진 리스트가 됩니다. 이 차이는 자료에 따라 다르게 설명되기도 할 만큼 미묘하므로, 빈 리스트가 필요한 곳에서는 MyClass obj{std::initializer_list<int>{}}처럼 의도를 명시하는 것이 가장 오해가 적습니다.
멤버 초기화
// 타입 정의
class MyClass {
private:
int x{10}; // C++11: 멤버 초기화
std::string s{"Hello"};
std::vector<int> vec{1, 2, 3};
public:
MyClass() = default;
MyClass(int value) : x{value} {} // 생성자 초기화 리스트
};
멤버 선언부의 기본값(default member initializer)에는 =나 {}만 쓸 수 있고 ()는 쓸 수 없습니다. std::vector<int> vec(10);을 멤버 선언에 쓰면 멤버 함수 선언으로 해석될 수 있어서 문법 자체가 금지되어 있습니다. 그래서 멤버에 크기 10짜리 벡터를 기본값으로 주려면 std::vector<int> vec = std::vector<int>(10);처럼 써야 하고, std::vector<int> vec{10};은 앞의 함정대로 원소 하나가 됩니다. 생성자 초기화 리스트에 값이 있으면 선언부의 기본값은 무시되므로, MyClass(int) 생성자에서 x는 10이 아니라 value가 됩니다. 기본값은 “생성자가 따로 지정하지 않을 때의 값”이라고 이해하면 됩니다.
집합 초기화
struct Point {
int x, y, z;
};
// 집합 초기화
Point p1{1, 2, 3};
Point p2{1, 2}; // z는 0
Point p3{}; // 모두 0
// 중첩 구조체
struct Line {
Point start, end;
};
Line line{{0, 0, 0}, {10, 10, 10}};
집합체(aggregate)는 사용자 정의 생성자가 없고, 모든 비정적 멤버가 public이며, 가상 함수가 없는 클래스나 배열입니다. 집합체를 중괄호로 초기화하면 생성자를 호출하는 것이 아니라 멤버를 선언 순서대로 하나씩 초기화합니다. 초기값보다 멤버가 많으면 남은 멤버는 기본값이 있으면 그 값으로, 없으면 값 초기화(정수는 0)됩니다. Point p2{1, 2}에서 z가 0이 되는 이유입니다. 반면 Point p4;처럼 중괄호 없이 선언한 지역 변수는 멤버가 초기화되지 않은 쓰레기 값이므로, 구조체 지역 변수는 {}를 붙이는 습관이 버그를 줄입니다.
집합체 초기화는 순서에 의존하므로 멤버 순서를 바꾸면 모든 초기화 코드의 의미가 조용히 바뀐다는 약점이 있습니다. 같은 타입의 멤버가 연속이면(int x, y, z) 컴파일러도 잡아 주지 못합니다. C++20의 지정 초기화(Point p{.x = 1, .z = 3};)를 쓰면 이름으로 멤버를 지정하므로 순서 변경에 안전해집니다. 단, 선언 순서대로만 나열할 수 있고 중간을 건너뛰는 것만 허용됩니다.
장단점
// ✅ 장점
// 1. 균일한 문법
int x{10};
std::vector<int> vec{1, 2, 3};
// 2. narrowing 방지
// int y{3.14}; // 에러
// 3. Most Vexing Parse 방지
Widget w{}; // 객체 생성
// ❌ 단점
// 1. initializer_list 우선순위
std::vector<int> v{10}; // 1개 요소 (의도: 10개?)
// 2. auto와 혼동
auto x = {1, 2, 3}; // initializer_list
장점 상세:
| 장점 | 설명 | 예시 |
|---|---|---|
| 일관성 | 모든 타입에 동일한 문법 | int x{10}; vector<int> v{1,2,3}; |
| 안전성 | narrowing 방지 | int x{3.14}; // 에러 |
| 명확성 | Most Vexing Parse 해결 | Widget w{}; // 객체 |
| 편의성 | 컨테이너 초기화 간소화 | map<string,int> m{{"a",1}}; |
단점 상세:
| 단점 | 설명 | 해결 방법 |
|---|---|---|
| initializer_list 우선순위 | 원소가 변환 가능하면 {}는 initializer_list 생성자 우선 | () 사용 |
| vector 크기 혼동 | vector<int> v{10};은 1개 요소 | vector<int> v(10); |
| auto 타입 추론 | auto x = {1,2,3};은 initializer_list | 명시적 타입 |
실무 권장:
// ✅ 균일 초기화 사용 권장
// 1. 기본 타입
int x{10};
double d{3.14};
// 2. 구조체
Point p{10, 20};
// 3. 컨테이너 (요소 나열)
std::vector<int> v{1, 2, 3};
// ⚠️ 괄호 사용 권장
// 1. vector 크기 지정
std::vector<int> v(100); // 100개 요소
// 2. 생성자 인자가 명확할 때
std::string s(10, 'x'); // "xxxxxxxxxx"
사용 권장사항
// ✅ 사용 권장
// 1. 컨테이너 초기화
std::vector<int> vec{1, 2, 3};
// 2. 구조체 초기화
Point p{10, 20};
// 3. narrowing 방지 필요
int x{value}; // narrowing 체크
// ❌ 주의 필요
// 1. vector 크기 지정
std::vector<int> v(10); // () 사용
// 2. auto와 함께
std::vector<int> v = {1, 2, 3}; // 명시적 타입
실무 패턴
패턴 1: 설정 객체
struct ServerConfig {
std::string host = "localhost";
int port = 8080;
int maxConnections = 100;
bool enableSSL = false;
};
// 균일 초기화로 간결하게
ServerConfig cfg{
"0.0.0.0",
3000,
500,
true
};
멤버에 기본값(= "localhost")이 있는 구조체도 C++14부터 집합체로 인정되어 이렇게 초기화할 수 있습니다(C++11에서는 기본값이 있으면 집합체가 아니라 컴파일 에러였습니다). 다만 이 예제는 앞에서 말한 순서 의존성의 전형입니다. 3000, 500이 포트와 최대 연결 수라는 것을 코드만 봐서는 알 수 없고, 누군가 maxConnections를 port 앞으로 옮기면 포트가 500이 됩니다. 설정 객체처럼 필드가 많고 일부만 바꾸는 경우에는 C++20 지정 초기화로 ServerConfig cfg{.host = "0.0.0.0", .port = 3000, .enableSSL = true};처럼 쓰는 것이 훨씬 읽기 쉽고 안전합니다.
패턴 2: 반환값 최적화
struct Result {
bool success;
std::string message;
int code;
};
Result processRequest() {
if (error) {
return {false, "Error occurred", 500};
}
return {true, "Success", 200};
}
패턴 3: 멤버 초기화
class Connection {
std::string host_{"localhost"};
int port_{8080};
std::vector<std::string> options_{
"keepalive=true",
"timeout=30"
};
public:
Connection() = default;
Connection(std::string host, int port)
: host_{std::move(host)}, port_{port} {}
};
FAQ
Q1: 균일 초기화는 언제 사용하나요?
A:
- 컨테이너 초기화 (요소 나열)
- narrowing 변환 방지가 중요할 때
- 일관된 코드 스타일을 원할 때
Q2: () vs {}의 차이는?
A:
- {}: narrowing 방지,
initializer_list생성자 우선, Most Vexing Parse 해결 - (): 일반 생성자 호출, vector 크기 지정
Q3: narrowing이란 무엇인가요?
A: 정보 손실이 발생하는 타입 변환입니다 (예: double → int, long long → int).
Q4: Most Vexing Parse란?
A: Widget w();가 함수 선언으로 해석되는 문제입니다. Widget w{};로 해결합니다.
Q5: initializer_list 생성자 우선순위는?
A: 중괄호 안의 원소들이 initializer_list의 요소 타입으로 변환될 수 있으면 다른 생성자보다 우선합니다. 빈 중괄호 {}는 예외로 기본 생성자를 부르고, 원소가 요소 타입으로 변환될 수 없으면(vector<string>{10}) 일반 생성자가 선택됩니다.
std::vector<int> v1{10}; // initializer_list: 요소 1개
std::vector<int> v2(10); // 일반 생성자: 크기 10
Q6: 빈 중괄호 {}는 무엇을 의미하나요?
A: 값 초기화(value initialization)를 수행합니다. 기본 타입은 0, 클래스는 기본 생성자를 호출합니다.
Q7: 균일 초기화의 성능은?
A: 일반 초기화와 동일합니다. 컴파일러가 최적화하므로 런타임 오버헤드가 없습니다.
Q8: 균일 초기화 학습 리소스는?
A:
- “Effective Modern C++” by Scott Meyers (Item 7)
- cppreference.com - List initialization
- “C++ Primer” by Lippman, Lajoie, Moo
관련 글: initializer_list, Value Initialization.
균일 초기화는 중괄호로 모든 타입을 일관되게 초기화하며, narrowing 변환을 방지합니다.
같이 보면 좋은 글
- C++ initializer_list 생성자: vector{10, 20}과 (10, 20)이 다른 이유와 중괄호 초기화 우선순위
- C++ std::call_once와 once_flag: 스레드 안전한 1회 초기화와 예외 시 재시도
- C++ Aggregate Initialization
- C++ async & launch