Uniform Initialization

Date:     Updated:

카테고리:

태그:

모두의 코드 씹어먹는 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>

상식적으로 bint로 추론되어야 할 것 같지만 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"};

를 하게 된다면 listinitializer_list<std::string>이 아닌, initializer_list<const char*> 타입이 된다.

만약 string 타입으로 받고 싶다면 이전에 배운 리터럴 연산자를 이용해 해결이 가능하다.

using namespace std::literals;  // 문자열 리터럴 연산자를 사용하기 위해
                                // 추가해줘야함.
auto list = {"a"s, "b"s, "c"s};



맨 위로 이동하기

Cpp 카테고리 내 다른 글 보러가기

댓글 남기기