KoreanFoodie's Study

C++ 기초 개념 16-1 : C++ 유니폼 초기화(Uniform Initialization) 본문

Tutorials/C++ : Beginner

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};

 

Comments