아마 여러분들은 아래와 같은 실수를 한 번쯤 하셨을 것이라 생각합니다.
#include <iostream>
class A {
public:
A() { std::cout << "A 의 생성자 호출!" << std::endl; }
};
int main() {
A a(); // ?
}
성공적으로 컴파일 했다면,
놀랍게도 아무것도 출력되지 않습니다.
A a();
왜냐면 사실은 위 코드가 A의 객체 a를 만든 것이 아닌,
자료형 A를 리턴하는 a라는 함수를 정의한 것이기 때문입니다.
왜냐하면 C++의 컴파일러는
함수의 정의처럼 보이는 것들은 모두 함수의 정의로 해석하기 때문입니다.
이러한 문제는 ()
가 함수의 인자들을 정의하는데도 사용되고,
그냥 일반적인 객체의 생성자를 호출하는데에도 사용되기 때문입니다.
따라서 C++11 부터는 이러한 문제를 해결하기 위해
균일한 초기화(Uniform Initialization)을 도입 했습니다.
균일한 초기화 문법을 사용하기 위해서는
생성자 호출을 위해 ()
대신 {}
를 사용하면 됩니다.
class A
{
public:
A() { std::cout << "A의 생성자 호출!" << std::endl; }
};
int main()
{
A a{};
}
성공적으로 컴파일 하였다면,
A의 생성자 호출!
위와 같이 제대로 생성자가 호출되었음을 알 수 있습니다.
중괄호를 이용해서 생성자를 호출하는 문법은 동일합니다.
그냥 기존에 ()
자리를 {}
로 바꿔주기만하면 됩니다.
하지만, ()
를 이용한 생성과 {}
를 이용한 생성의 경우 한 가지 큰 차이가 있는데
바로 일부 암시적 타입 변환들을 불허하고 있다는 점입니다.
예를 들어 아래 코드를 봅시다.
#include <iostream>
class A {
public:
A(int x) { std::cout << "A 의 생성자 호출!" << std::endl; }
};
int main() {
A a(3.5); // Narrow-conversion 가능
A b{3.5}; // Narrow-conversion 불가
}
컴파일하면
A b{3.5}; // Narrow-conversion 불가
에서 double
을 int
로 변환할 수 없다는 오류가 발생합니다.
그 이유는 중괄호를 이용해서 생성자를 호출하는 경우
아래와 같은 암시적인 타입 변환이 불가능해집니다.
이들은 전부 데이터 손실이 있는(Narrowing) 변환 입니다.
* 부동 소수점 타입에서 정수 타입으로 변환
* `long double`에서 `double` 혹은 `float`으로의 변환, `double` 에서 `float`으로의 변환
* 정수 타입에서 부동 소수점 타입으로 변환
등등이 있습니다.
따라서 {}
를 사용하게 된다면,
위와 같이 원하지 않는 타입 캐스팅을 방지해서 미연에 오류를 잡아낼 수 있습니다.
{}
를 이용한 생성의 또 다른 쓰임새로,
함수 리턴 시에 굳이 생성하는 객체의 타입을 다시 명시하지 않아도 됩니다.
#include <iostream>
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(); }
성공적으로 컴파일 하였다면
A 생성자 호출
와 같이 잘 나옵니다. {}
를 이용해서 생성하지 않았다면, A(1, 2.3)
과 같이
클래스를 명시해야했지만, {}
를 사용할 경우,
컴파일러가 알아서 함수의 리턴타입을 보고 추론해줍니다.
배열을 정의할 때, 우리는 다음과 같이 작성합니다.
int arr[] = {1,2,3,4};
그렇다면 중괄호를 이용해서 마찬가지 효과를 낼 수 없을까요? 예를 들면,
vector<int> v = {1,2,3,4};
와 같이 말이죠.
근데, 놀랍게도 C++11부터 이와 같은 문법을 사용할 수 있게 되었습니다.
#include <iostream>
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}; }
성공적으로 컴파일 하였다면,
1
2
3
4
5
와 같이 나옵니다.
initializer_list
는
우리가 {}
를 이용해서 생성자를 호출할 때, 클래스의 생성자들 중에
initializer_list
를 인자로 받는 생성자가 있다면 전달 됩니다.
🚫주의사항
()
를 사용해 생성자를 호출하면initializer_list
가 생성되지 않습니다.
initializer_list
를 이용하면, 컨테이너들을 간단하게 정의할 수 있습니다.
#include <iostream>
#include <map>
#include <string>
#include <vector>
template <typename T>
void print_vec(const std::vector<T>& vec) {
std::cout << "[";
for (const auto& e : vec) {
std::cout << e << " ";
}
std::cout << "]" << std::endl;
}
template <typename K, typename V>
void print_map(const std::map<K, V>& m) {
for (const auto& kv : m) {
std::cout << kv.first << " : " << kv.second << std::endl;
}
}
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
print_vec(v);
std::cout << "----------------------" << std::endl;
std::map<std::string, int> m = {
{"abc", 1}, {"hi", 3}, {"hello", 5}, {"c++", 2}, {"java", 6}};
print_map(m);
}
성공적으로 컴파일 하였다면,
[1 2 3 4 5]
abc : 1
c++ : 2
hello : 5
hi : 3
java : 6
와 같이 나옵니다.
생성자들 중에서 initializer_list
를 받는 생성자가 있다면,
한 가지 주의해야 할 점이 있습니다.
만일, {}
를 이용해서 객체를 생성할 경우,
생성자 오버로딩 시에 해당 함수가 최우선으로 고려된다는 점입니다.
예를 들어, vector
의 경우 아래와 같은 생성자가 존재합니다.
vector(size_type count);
이 생성자는 count 개수 만큼의 원소 자리를 미리 생성해놓습니다. 그렇다면,
vector v{10};
은 해당 생성자를 호출할까요?
아닙니다. 그냥 원소 1개짜리 initializer_list
라 생각해서
10
을 보관하고 있는 벡터를 생성하게 됩니다.
따라서, 이러한 불상사를 막기 위해서는 {}
로 생성하기 보다는 ()
을 이용해서
vector v(10);
과 같이 v를 생성한다면 우리가 원하는 생성자를 호출할 수 있게 됩니다.
#include <initializer_list>
#include <iostream>
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}; // Bad!
컴파일러는 {}
를 이용해서 생성자를 호출할 경우,
initializer_list
를 받는 생성자를 최우선으로 고려한다고 했습니다.
따라서, 컴파일러는 initializer_list
를 이용하도록 최대한 노력하는데,
1.5는 int
가 아니지만, double
에서 int
로 암시적 변환을 할 수 있으므로
이를 택하게 됩니다.
그런데 {}
는 데이터 손실이 있는 변환을 할 수 없다고 했습니다.
그런데 double
에서 int
로의 타입 변환은 데이터 손실이 있으므로
오류가 발생하게 됩니다.
사실, A(int x, double y)
이 생성자가 더 나은 매칭이지만,
C++컴파일러는 initializer_list
를 이용한 생성자를 최대한 고려하려고 합니다.
이러한 문제가 발생하지 않으려면
initializer_list
의 원소 타입으로 타입 변환 자체가 불가능한 경우여야 합니다.
#include <initializer_list>
#include <iostream>
#include <string>
class A {
public:
A(int x, double y) { std::cout << "일반 생성자! " << std::endl; }
A(std::initializer_list<std::string> lst) {
std::cout << "초기화자 사용 생성자! " << std::endl;
}
};
int main() {
A a(3, 1.5); // 일반
A b{3, 1.5}; // 일반
A c{"abc", "def"}; // 초기화자
}
성공적으로 컴파일 했다면
일반 생성자!
일반 생성자!
초기화자 사용 생성자!
와 같이 잘 나옵니다. 위 경우 int
나 double
이 string
으로 변환될 수 없기 때문에
initializer_list
를 받는 생성자는 아예 고려 대상에서 제외됩니다.
만일 {}
를 이용해서 생성할 때 타입으로 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에서는 모두 std::initializer_list<int>
로 정의됩니다.
하지만 C++17부터는 아래와 같이 두 가지 형태로 구분해서 auto
타입이 추론됩니다.
* `auto x = {arg1, arg2...} 의 경우, `arg1`, `arg2` 들이 같은 타입이라면
x는 `std::initializer_list<T>` 로 추론 됩니다.
* `auto x {arg1, arg2...}` 형태의 경우 만일 인자가 단 1개라면, 인자의 타입을 추론되고
여러개라면 오류를 발생시킵니다.
따라서 C++17부터는 다음과 같습니다.
auto a = {1}; // std::initializer_list<int>
auto b{1}}; // int
auto c = {1,2}}; // std::initializer_list<int>
auto d{1,2}}; // 오류
유니폼 초기화와 auto
를 같이 사용 할 때 또 한 가지 주의할 점은, 문자열을 다룰 때
auto list = {"a", "b", "cc"};
를 하게되면 list
는 initializer_list<std::string>
이 아닌
`initializer_list<const char*>이 된다는 점입니다. 이는 C++14에서 추가된
리터럴 연산자를 통해 해결 할 수 있습니다.
using namespace std::literals; // 문자열 리터럴 연산자 사용을 위해
auto list = {"a"s, "b"s, "cc"s};
와 같이 하면, initializer_list<std::string>
으로 추론됩니다.
참조 : 모두의 코드