KoreanFoodie's Study
C++ 기초 개념 16-1 : C++ 유니폼 초기화(Uniform Initialization) 본문
C++ 기초 개념 16-1 : C++ 유니폼 초기화(Uniform Initialization)
GoldGiver 2022. 5. 23. 15:19
모두의 코드를 참고하여 핵심 내용을 간추리고 있습니다. 자세한 내용은 모두의 코드의 씹어먹는 C++ 강좌를 참고해 주세요!
C++ 생성자 흔한 실수
다음과 같은 코드는 아무것도 출력하지 않는다.
#include <iostream>
class A
{
public:
A() { std::cout << "Constructor A is called!" << std::endl; }
};
int main()
{
A a(); // ?
}
왜냐하면
A a(); // ?
위 문장은 A 타입을 리턴하고, 인자를 받지 않는 함수를 정의한 것으로 컴파일러가 해석하기 때문이다! C++ 컴파일러는 함수의 정의처럼 보이는 것들을 모두 함수의 정의로 해석한다.
아래와 같은 경우는 어떨까?
#include <iostream>
class A
{
public:
A() { std::cout << "Constructor A is called!" << std::endl; }
};
class B
{
public:
B() { std::cout << "Constructor B is called!" << std::endl; }
};
int main()
{
B b(A()); // ?
}
위의 코드 또한 아무것도 출력하지 않는다. B b(A( )); 는 인자로 A 를 리턴하고 인자가 없는 함수를 받으며, 리턴 타입이 B 인 함수 b 를 정의한 것이다! 이러한 문제를 해결하기 위해, C++ 11 에서는 균일한 초기화(Uniform Initialization) 을 도입했다.
균일한 초기화(Uniform Initialization)
균일한 초기화를 위해서는 ( ) 대신 { } 를 사용하면 된다.
int main()
{
// A a(); // 함수 정의
A a{}; // 균일한 초기화!
}
균일한 초기화의 특징은, 일부 암시적 타입 변환을 불허하고 있다는 것이다!
예를 들어, 아래 코드를 보자.
#include <iostream>
class A
{
public:
A(int x) { std::cout << "Constructor A is called!" << std::endl; }
};
int main()
{
A a(3.5); // Narrow-conversion 가능
A b{3.5}; // Narrow-conversion 불가
}
아래 b 에서, 아래와 같은 암시적 타입 변환이 불가능해진다. 이들은 전부 데이터 손실이 있는(Narrowing) 변환 이다.
- 부동 소수점 타입에서 정수 타입으로의 변환
- long double 에서 double 혹은 float 으로의 변환, double 에서 float 으로의 변환
- 정수 타입에서 부동 소수점 타입으로의 변환
등등이 있다. 자세한 것을 여기를 참고하자.
따라서 { } 를 사용하면, 원하지 않는 타입 캐스팅을 방지해 미연에 오류를 잡아낼 수 있다.
{ } 를 이용하면 함수 리턴 시에 객체의 타입을 다시 명시하지 않아도 된다.
A foo()
{
return {3}; // A(3) 과 동일
}
{ } 를 이용할 경우, 컴파일러가 알아서 함수의 리턴타입을 보고 추론해준다.
초기화자 리스트 (Initializer list)
배열을 정의하거나, 컨테이너를 정의할 때 { } 를 쓰면 작업이 간단해진다.
#include <iostream>
#include <map>
#include <string>
#include <vector>
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);
}
initializer_list 사용 시 주의할 점
다음과 같은 코드가 있다고 하자.
#include <iostream>
#include <vector>
class A
{
public:
A(int x, double y)
{
std::cout << "Typical Constructor is called!" << std::endl;
}
A(std::initializer_list<int> lst)
{
std::cout << "initializer_list constructor is called!" << std::endl;
}
};
int main()
{
std::vector v1(10); // 길이가 10 인 벡터
std::vector v2{10}; // 원소 10 이 들어가 있는 벡터 (길이는 1)
A(1, 1.5);
A{2, 2.5};
}
위 코드는 main 함수의 마지막 줄에서 에러가 난다. 왤까?
이는 { } 를 이용해서 객체를 생성할 경우, 생성자 오버로딩 시에 해당 함수가 최우선으로 고려되기 때문이다. 즉, 컴파일러는 해당 생성자와 매칭시키기 위해 최선을 다한다.
위에서 A{2, 2.5} 가 실행되면, 초기화 리스트 버전의 생성자가 호출되는데, { } 는 균일한 초기화에서 보았듯, 데이터 손실이 있는 변환(Narrowing conversion) 을 불허한다. 그러므로 'double' 에서 'int' 로 narrowing conversion 을 시도한다는 에러가 발생한다.
이러한 문제가 발생하지 않는 경우는, initializer_list 의 원소 타입으로 타입 변환 자체가 불가능한 경우이어야 한다.
#include <iostream>
#include <initializer_list>
#include <string>
class A
{
public:
A(int x, double y)
{
std::cout << "Typical constructor is called!" << std::endl;
}
A(std::initializer_list<std::string> lst)
{
std::cout << "initializer_list constructor is called!" << std::endl;
}
};
int main()
{
A(1, 1.5); // Typical Constructor
A{2, 2.5}; // Typical Constructor
A{"It's", "OK!"}; // initializer_list Constructor
}
위에서는, int 나 double 타입에서 string 으로 변환될 수 없기 때문에 initializer_list 를 받는 생성자가 고려 대상에서 제외될 수 있다!
initializer_list 와 auto
{ } 를 사용할때 만약 auto 키워드를 써서 변수 타입을 정의하면 컴파일러는 어떤 방식으로 타입을 추론할까? 예시 코드를 보자.
auto a = {1};
auto b{2};
auto c = {1, 2};
auto d{1, 2};
규칙은 다음과 같다 :
- auto x = {arg1, arg2, ...} 형태의 경우, arg1, arg2 ... 들이 모두 같은 타입이라면 x 는 std::initializer_list<T> 로 추론된다.
- auto x{arg1, arg2, ...} 형태의 경우, 만일 인자가 단 1개라면 인자의 타입으로 추론되고, 여러 개일 경우 오류를 발생시킨다.
따라서 결과는 다음과 같다 :
auto a = {1}; // std::initializer_list<int>
auto b{2}; // int
auto c = {1, 2}; // std::initialier_list<int>
auto c2 = {1, 2, 3.5}; // Error! (인자들의 타입이 다름)
auto d{1, 2}; // Error! (두번째 규칙에 따라, 인자가 여러개일 수 없음)
유니폼 초기화와 auto 를 사용할때, 다음 코드의 타입은 어떻게 될까?
auto list = {"a", "b", "c"};
list 는 initializer_list<const char*> 타입이 되므로, 이를 string 으로 바꾸고 싶으면 C++14 에서 추가된 리터럴 연산자를 사용해야 한다.
auto list = {"a"s, "b"s, "c"s};
'Tutorials > C++ : Beginner' 카테고리의 다른 글
C++ 기초 개념 16-3 : decltype 과 std::declval (0) | 2022.05.24 |
---|---|
C++ 기초 개념 16-2 : constexpr 와 컴파일 타임 상수 (0) | 2022.05.23 |
C++ 기초 개념 15-5 : 쓰레드풀(ThreadPool) 만들기 (0) | 2022.04.21 |
C++ 기초 개념 15-4 : future, promise, packaged_task, async (0) | 2022.04.20 |
C++ 기초 개념 15-3 : atomic 객체와 memory order (0) | 2022.04.19 |