16.1 유니폼 초기화(Uniform Initialization)
C++11 이전 초기화 문제상황
C++11 에 추가된 내용
아래 코드를 실행해보면 의도와는 다르게 생성자가 호출되지 않는다.
왜냐면, 아래 코드는 A 의 개체 a 를 만드는 것이 아니라, A 를 리턴하고 인자를 받지 않는 함수 a 를 정의 한 것이기 때문이다. -> C++ 컴파일러는 함수의 정의처럼 보이는 것들은 모두 함수의 정의로 해석하기 떄문이다.
아래 코드는 더욱이 헷갈린다. 위 코드와 동일하게 아무것도 호출되지 않는다.
왜냐면, B 개체 b 를 만드는 것이 아니라, A 를 리턴하고 인자가 없는 함수를 인자로 받으며, 리턴 타입이 B 인 함수 b 를 정의한 것이다. -> 말이 너무 어렵다 ;;
이러한 문제를 상당히 곤란한데, () 가 함수의 인자들을 정의하는데도 사용되고, 그냥 일반적인 개체의 생성자를 호출하는데도 사용되기 떄문이다. -> C++11 부터는 이러한 문제를 해결하기 위해서 균일한 초기화(Uniform Initialization) 을 도입했다.
균일한 초기화(Uniform Initialization)
Uniform Initialization 은 () 대신 {} 을 사용하기만 하면 끝이다.
성공적으로 컴파일 되었다면 "A의 생성자 호출!" 이라는 결과를 볼 수 있다.
기존의 () 대신해서 {} 를 사용하기만 하면 되지만, 이 두가지는 큰 차이가 있다.
바로 {} 는 데이터 손실이 있는(Narrowing) 변환을 불허한다는 점이다.
아래 코드를 보자 -> () 경우는데이터 손실이 있는(Narrowing) 변환을 허용하지만, {} 경우는 컴파일 에러를 발생시킨다.
데이터 손실이 있는(Narrowing) 변환이 있어나는 경우는 다음과 같다. -> 아래의 경우는 전부 데이터 손실이 있는(Narrowing) 변환이다.
부동소수점 타입 -> 정수 타입
long double -> double, float
double -> float
정수 타입 -> 부동소수점 타입
등등 ..
떄문에, {} 를 사용하게 되면 위와 같이 원하지 않는 타입 캐스팅을 방지해, 미연에 오류를 잡아낼 수 있다. -> 컴파일 에러에서 이러한 것들을 잡아낸다.
또한, {} 이용하게 되면 함수 리턴 시 굳이 생성하는 객체의 타입을 다시 명시하지 않아도 된다. -> {} 를 사용하게 되면 컴파일러가 타입을 알아서 추론해준다. -> 하지만, 코드 가독성 측면에서는 좋아보이지는 않는다.
초기화 리스트(Initializer list)
배열, vector 를 정의할 때 다음과 같이 작성했다.
C++11 부터는 std::initializer_list 를 이용해 {} 를 매개변수로 받을 수 있게 되었다.
아래 코드와 같이 std::initializer_list 를 매개변수로 받는 생성자를 만들 수 있다. -> () 를 사용해 생성자를 호출한다면 initializer_list 가 생성되지 않는다.
initializer_list 를 통해서 컨테이너들을 간단하게 정의할 수 있다.
vector 의 경우 배열과 같이 {} 내에 원소들을 나열해주면 된다.
map 의 경우 pair<Key, Valuie> 원소들을 초기화 리스트의 원소들로 받는다. -> pair 은 C++ STL 에서 지원하는 간단한 클래스로 그냥 두 개의 원소를 보관하는 개체로 보면 된다. -> map 에서는 pair 의 첫번쨰 원소를 Key, 두번쨰 원소를 Value 로 보면 된다.
initializer_list 사용 시 주의 점 (from 생성자)
생성자들 중에서 initializer_list 를 받는 생성자가 있다면 한 가지 주의해야 할 점이 있다. -> 만일 {} 를 이용해서 개체를 생성할 경우 initializer_list 를 매개변수로 받는 생성자가 최우선적으로 고려된다.
예를 들어, vector 의 경우 아래와 같은 형태의 생성자가 존재한다. (해당 생성자는 count 개수 만큼의 원소 자리를 미리 생성한다. )
아래 코드는 위 생성자를 호출하는 것이 아닌, 그냥 원소 1개 짜리 initializer_list 라고 생각해서 10을 보관하는 vector 를 생성하기 된다.
따라서 이러한 불상사를 막기 위해서는, 의도에 따라 {} 로 생성하기 보다는 () 를 이용해 아래와 같이 v 를 생성한다면 우리가 원하는 생성자를 호출할 수 있다.
initializer_list 를 받는 생성자가 최우선적으로 고려된다는 말은 컴파일러가 최선을 다해서 해당 생성자와 매칭시키기 위해 노력한다는 점이다. -> 여기서 컴파일러가 최선을 다해서 해당 생성자와 매칭하려 한다는 말이 중요하다!
아래 코드를 살펴보면, 컴파일 에러가 발생한다. ->
A a(3, 1.5);
는A(int x, double y)
생성자가 호출되어 아무런 문제가 없다. ->A b{3, 1.5};
는 {} 를 사용하기 때문에 컴파일러는A(std::initializer_list<int> lst) 생성자를 호출하기 위해 최선을 다하고 double 타입인 1.5 를 int 타입으로 암시적 형변환을 시켜 vector 에 2개의 원소가 들어가도록 동작하게 된다. (하지만{}
은 데이터 손실이 있는 형변환을 허용하지 않기 때문에 컴파일 에러가 발생한다) -> 다시 한번 말하지만, 컴파일러는 {} 생성의 경우 initializer_list 타입 생성자를 최우선시 한다!
아래에 경우는
A b{3, 1.5};
가 string 타입으로 형변환이 불가하기 때문에A(int x, double y);
생성자를 호출하게 된다.
initializer_list 와 auto
만약 {} 를 사용해서 개체를 생성할 때, auto 를 지정한다면 initializer_list 생성자로 개체가 생성된다. -> 아래 코드에서 list 는 initializer_list<int> 가 될 것이다. -> b 는 int 타입으로 추론되어야 할 것 같지만, 아니다..
위 예제는 비상식적이기 떄문에 C++17 부터는 다음과 같이 2가지로 구분해 auto 타입이 추론된다. -> 이전보다 직관적이다.
auto x = {arg1, arg2, ...}
형태의 경우arg1, arg2, ...
들이 모두 같은 타입이라면,x
는std::initializer_list
형태로 추론auto x {arg1, arg2, ...}
형태의 경우 만일 인자가 단 1개라면 인자의 타입으로 추론되고, 여러개일 경우 컴파일 에러를 발생시킨다.
initializer_list 와 auto 에서 주의점
아래 코드와 같이, 문자열을 다룬다면 list 는 initializer_list<std::string> 이 아닌, initializer_list<const char*> 이 된다는 점이다.
물론 이 문제는 C++14 에 추가된 리터럴 연산자를 통해서 해결할 수 있다. -> 다음에 코드에서는 initializer_list<std::string> 으로 추론된다.
Last updated