Uniform Initialization
카테고리: Cpp
태그: Programming
모두의 코드 씹어먹는 c++ 자료를 보고 정리한 내용입니다.
class A {
public:
A() { std::cout << "A 의 생성자 호출!" << std::endl; }
};
class B {
public:
B(A a) { std::cout << "B 의 생성자 호출!" << std::endl; }
};
int main() {
A a(); // ??
B b(A()); // ??
}
[!result]
위 코드를 컴파일하면 놀랍게도 아무것도 출력되지 않는다. 왜냐하면
A a();
는 A의 객체 a를 만든 것이 아니라 인자를 받지 않고, A를 리턴하는 함수 a를 정의한 것이고,
B b(A());
는 B의 객체 b를 만든 것이 아니라 인자가 없고 A를 리턴하는 함수를 인자로 받고, B를 리턴하는 함수를 정의한 것이기 때문이다. (forward declaration로 매우 유용하게 사용된다)
이러한 문제가 발생하는 것은 ()
가 함수의 인자들을 정의하는 데에도 사용되고, 그냥 일반적인 객체의 생성자를 호출하는 데에도 사용되기 때문이다.
이는 상당히 골치 아픈 일인데, c++ 11
에서는 이러한 문제를 해결하기 위해 균일한 초기화(Uniform Initialzation) 이라는 것을 도입했다.
균일한 초기화(Uniform Initialzation)
A a{};
Uniform Initialization 문법을 사용하는 방법은 그냥 기존의 ()
를 {}
로 바꿔주기만 하면 된다.
하지만 ()
를 이용한 생성과 {}
를 이용한 생성의 경우 한 가지 큰 차이가 있는데, 일부 암시적 타입 변환들을 금지힌다는 것이다.
class A {
public:
A(int x) { std::cout << "A 의 생성자 호출!" << std::endl; }
};
int main() {
A a(3.5); // Narrow-conversion 가능 - OK
A b{3.5}; // Narrow-conversion 불가 - Error
}
Uniform Initialization에서는 데이터 손실이 발생하는 불안정한 형변환 (Narrowing) 변환들을 불허한다.
따라서 {}
를 이용하게 된다면, 위와 같이 원하지 않는 타입 캐스팅을 미연에 방지해 오류를 잡아낼 수 있다.
class A {
public:
A(int x, double y) { std::cout << "A 생성자 호출" << std::endl; }
};
A func() {
return {1, 2.3}; // A(1, 2.3) 과 동일
}
int main() { func(); }
{}
의 또 다른 쓰임새는 함수 리턴 시 굳이 생성하는 객체의 타입을 명시하지 않아도 된다는 것이다. {}
를 이용할 경우 컴파일러가 알아서 함수의 리턴 타입을 보고 추론해준다.
Initializer List
배열의 경우 아래 코드와 같이 초기화 리스트를 통해 간편하게 초기화를 할 수 있었다.
int arr[] = {1, 2, 3, 4};
그렇다면 {}
를 이용해 비슷한 효과를 낼 수 없을까?
vector<int> v = {1, 2, 3, 4}; // ??
놀랍게도 c++ 11
부터 이와 같은 문법을 사용할 수 있게 되었다.
class A {
public:
A(std::initializer_list<int> l) {
for (auto itr = l.begin(); itr != l.end(); ++itr) {
std::cout << *itr << std::endl;
}
}
};
int main() { A a = {1, 2, 3, 4, 5}; }
[!result] 1 2 3 4 5
initialzer_list
는 {}
를 이용해 생성자를 호출할 때 전달된다. 당연한 이야기이지만, ()
를 사용해 생성자를 호출하면 initializer_list
가 생성되지 않는다.
initializer_list
를 이용하면 컨테이너들도 간단하게 초기화할 수 있다. 예를 들어
std::vector<int> v = {1, 2, 3, 4, 5};
vector
의 경우 예상했던 데로 원소들을 그냥 나열해주면 되고,
std::map<std::string, int> m = {
{"abc", 1}, {"hi", 3}, {"hello", 5}, {"c++", 2}, {"java", 6}};
map
의 경우도 비슷하게 pair<Key, Value>
원소들을 나열하면 된다. 참고로 pair
는 c++ STL에서 지원하는 간단한 클래스로 그냥 두 개의 원소를 보관하는 객체라고 보면 된다.
[!danger] 생성자들 중 initializer_list를 받는 생성자가 있을 경우 한 가지 주의해야 할 점이 있는데, {} 를 이용해 객체를 생성할 경우 생성자 오버로딩 중 해당 함수가 최우선으로 고려된다는 점이다.
예를 들어 vector
의 경우 아래와 같은 형태의 생성자가 존재하는데
vector(size_type count);
이 생성자는 count 갯수 만큼의 원소 자리를 미리 생성해놓는다. 그렇다면
vector v{10};
은 해당 생성자를 호출할까? 정답은 이미 말했듯이 그냥 원소 1개 짜리 initializer_list
라고 생각해 10을 보관하는 벡터를 생성하게 된다.
class A {
public:
A(int x, double y) { std::cout << "일반 생성자! " << std::endl; }
A(std::initializer_list<int> lst) {
std::cout << "초기화자 사용 생성자! " << std::endl;
}
};
int main() {
A a(3, 1.5); // Good
A b{3, 1.5}; // Bad!
}
위 코드의 경우도 A b{3, 1.5};
부분은 initializer_list
생성자를 호출하므로 컴파일 오류가 발생한다.
Initializer_list와 auto
만약 {}
를 이용해 객체를 생성할 때, 타입으로 auto
를 지정한다면 해당 객체는 initializer_list
객체가 생성된다. 예를 들어
auto list = {1, 2, 3};
와 같은 코드에서 list
의 타입은 initializer_list<int>
가 된다.
좀 더 자세한 케이스를 살펴보자.
auto a = {1}; // std::initializer_list<int>
auto b{1}; // std::initializer_list<int>
auto c = {1, 2}; // std::initializer_list<int>
auto d{1, 2}; // std::initializer_list<int>
상식적으로 b
는 int
로 추론되어야 할 것 같지만 c++ 11
에서는 위 a, b, c, d 모두를 std::initializer_list<int>
로 추론한다.
하지만 이는 꽤나 비상식적이기 때문에 c++ 17
부터 아래와 같이 두 가지 형태로 구분해 auto
타입이 추론된다.
-
auto x = {arg1, arg2, ...}
형태의 경우arg1
,arg2
,...
들이 모두 같은 타입이라면 x는std::initializer_list<int>
로 추론되고, 타입이 여러개일 경우 오류가 발생한다. -
auto x = {arg1, arg2, ...}
에서 만약 인자가 단 1개 라면 인자의 타입으로 추론된다.
auto a = {1}; // 첫 번째 형태이므로 std::initializer_list<int>
auto b{1}; // 두 번째 형태 이므로 그냥 int
auto c = {1, 2}; // 첫 번째 형태이므로 std::initializer_list<int>
auto d{1, 2}; // 두 번째 형태 인데 인자가 2 개 이상이므로 컴파일 오류
참고로 문자열의 경우
auto list = {"a", "b", "cc"};
를 하게 된다면 list
는 initializer_list<std::string>
이 아닌, initializer_list<const char*>
타입이 된다.
만약 string 타입으로 받고 싶다면 이전에 배운 리터럴 연산자를 이용해 해결이 가능하다.
using namespace std::literals; // 문자열 리터럴 연산자를 사용하기 위해
// 추가해줘야함.
auto list = {"a"s, "b"s, "c"s};
댓글 남기기