[C++] 균일 초기화( uniform initialization )과 std::initializer_list

initializer_list의 소개

배열을 여러 원소들로 초기화하려면, 다음과 같이 균일 초기화( uniform initialization )를 사용하면 됩니다.

int main(){

    int intArray[] { 1, 2, 3, 4, 5 }; // uniform initialization
    for( int x : intArray){
        cout << x << " ";
    }
}

 

여기서, 균일 초기화는 C++에서 변수를 초기화하기 위한 세 가지 방법 중 하나입니다.

이러한 초기화들에 관한 내용은 여기에서 볼 수 있습니다.

 

[C++] 초기화( initialization )의 종류 정리

변수의 초기화C++을 사용해서, 코딩을 하다 보면 의외로 다양한 방법으로 변수를 초기화할 수 있음을 알게 됩니다.( 물론, 다른 언어도 다양한 방식의 초기화 방법을 제공합니다. ) 그런데, 대부

codingembers.tistory.com

 

그런데, 이러한 초기화 방법을 사용자 클래스를 초기화하는 데 사용하려면 어떻게 해야 할까요?

 

좀 더 정확히 말하자면, 아래에 정의한 IntArray는 생성 시 int 배열을 할당하고, 파괴될 때, 할당한 int 배열 메모리를 시스템에 반환하는 클래스입니다.

#include <cassert>	// for assert

class IntArray{
    int m_nLength;
    int* m_pArray;

public:

    IntArray( int nLength) : m_nLength(nLength){
        m_pArray = new int[nLength];
    }

    ~IntArray(){
        delete [] m_pArray;
    }

    int& operator[](int idx){       
        assert( idx >=0 && idx < m_nLength);
        return m_pArray[idx];
    }
    
    int getLength() const {
    	return m_nLength;
    }
};

 

이러한 클래스 객체를 생성 시, 다음과 같이 초기화할 수 있는 방법은 뭘까요?

IntArray arr{ 1, 2, 3, 4, 5};	// 초기화 리스트를 사용한 초기화

 

컴파일러는 위와 같은 중괄호의 초기화 리스트를 만나게 되면, 이를 std::initializer_list로 변환합니다.

이러한 initializer_list는 클래스의 생성자 및 다른 콘텍스트에서 사용할 수 있는, 지정된 형식의 개체 목록을 나타내는 클래스입니다.

 

따라서, initializer_list를 매개 변수로 받아들이는 생성자를 구현하면 될 것입니다.

먼저, initializer_list를 사용하려면 다음의 헤더를 포함해야 합니다.

#include <initializer_list>

 

이제, initializer_list를 매개 변수로 받는 생성자를 추가합니다.

#include <initializer_list>	
#include <algorithm> // for copy()

IntArray( int nLength) : m_nLength(nLength){	// 위와 동일
	m_pArray = new int[nLength];
}

// 중괄호 초기화를 사용하기 위한 생성자
IntArray( std::initializer_list<int> li) : IntArray( li.size() ){
	std::copy( li.begin(), li.end(), m_pArray);	
}

이 새로 추가된 생성자는 중복된 코드를 피하기 위해, initializer_list의 멤버 함수 size() 함수를 사용하여, 위에서 정의되었던 IntArray( int nLength ) 생성자를 다시 호출했습니다.

 

그리고, initializer_list는, std::string의 데이터를 참조하는 std::string_view와 같은 방식으로 동작하는, 참조 클래스입니다.

따라서, 위와 같이 선언하더라도, 함수 호출 시 중괄호 초기화 데이터의 복사가 발생하지 않습니다.

( const initializer_list <int>& 타입의 매개 변수를 사용할 필요가 없습니다. )

 

string_view에 관한 내용은 여기에서 볼 수 있습니다.

 

[C++] string_view에 대한 설명과 사용법

string_viewstd::string_view는 문자열을 사용하면서 발생하는, 무거운 복사 과정을 줄이고자 만든 클래스입니다.이 클래스는 실제 원본 문자열을 읽기 전용 방식으로 참조하는 기능을 합니다.이 클래

codingembers.tistory.com

 

이제, 이 생성자를 사용하는 균일 초기화( uniform initialization )를 실행할 수 있습니다.

int main(){

   IntArray arr{ 1, 2, 3, 4, 5};
   for( int i = 0; i < arr.getLength(); i++){
		cout << arr[i] << " ";
   }  
}

▼출력

1 2 3 4 5

 

이러한 방식의 초기화를 지원하는 클래스로서 std::vector가 있습니다.

//vector (initializer_list<value_type> il  생성자의 정의

vector<int> vec = { 1, 2, 3, 4, 5 };

 

클래스의 빈 중괄호 초기화

위의 초기화의 종류를 설명한 글에서 "빈 중괄호 초기화"를 값 균일 초기화( value uniform initialization )라고 얘기했었습니다.

int val{ };	// value uniform initialization.

이 경우, 변수 val의 값은 0이 됩니다.

 

그런데, 클래스 객체의 경우는 매개 변수가 없는 기본 생성자를 통한 초기화를 수행하게 됩니다.

이 경우는 int와 같이 값을 초기화한다는 의미가 좀 다를 수밖에 없기 때문입니다.

class CWidjet{

    int m_nVal = 10;
public:

    CWidjet(){
        cout << "Default Constructor called : " << m_nVal << endl ;
    }

    CWidjet(int n ) : m_nVal(n){
        cout << "Parameter Constructor called\n";
    }
};

int main(){

    CWidjet w1{ };
    CWidjet w2 = { };
}

▼출력

Default Constructor called : 10
Default Constructor called : 10

 

그런데, initializer_list 생성자가 있는 클래스의 객체를 값 균일 초기화( value uniform initialization ) 방법으로 생성하려고 하면, 어떤 생성자가 호출될까요?

기본 생성자일까요, 아님 원소의 개수가 0인 initializer_list 생성자일까요?

class CWidjet{

    int m_nVal = 10;
public:

    CWidjet(){
        cout << "Default Constructor called : " << m_nVal  ;
    }

    // initializer_list 생성자
    CWidjet( initializer_list<int> li ){
        cout << "Initializer_List Constructor called\n";
    }
};

▼출력

Default Constructor called : 10

이 경우도 위와 결과와 같이 기본 생성자를 호출합니다.

 

따라서, 원소가 없는 initializer_list 생성자를 호출하려면 다음과 같이 초기화해야 합니다.

int main(){
    CWidjet w{ {} };		// direct uniform initialization
    CWidjet w2( {} );		// direct initialization
    CWidjet w3 = { {} };	// copy uniform initialization
}

▼출력

Initializer_List Constructor called
Initializer_List Constructor called
Initializer_List Constructor called

 

 

initializer_list 생성자 추가 시 주의사항

클래스에 initializer_list 생성자가 있는 경우, 컴파일러는 균일 초기화의 값들을 "무리하더라도" initializer_list의 타입에 맞추는 생성자 호출을 하게 됩니다.

 

먼저, initializer_list 생성자가 없는 클래스의 코드는 다음과 같습니다.

class CWidjet{

    int m_nVal;
    double m_dVal;
    bool m_bVal;

public:

    CWidjet(int nVal, bool bVal){
        cout << "int and bool constructor called\n";
    }

    CWidjet(int nVal, double dVal){
    	cout << "int and double constructor called\n";
    }
};

이 클래스 객체를 초기화하기 위한 다음의 호출들은 모두 예상하고 원하는 생성자를 호출합니다.

int main(){

	CWidjet w1( 10, true );
	CWidjet w2{ 10, false };

	CWidjet w3( 20, 3.5 );
	CWidjet w4{ 20, -3.5 };
}

▼출력

int and bool constructor called
int and bool constructor called
int and double constructor called
int and double constructor called

 

그런데, 이 클래스에 다음과 같은 initializer_list 생성자를 추가하면, 기존의 모든 균일 초기화는 클래스의 initializer_list 생성자를 호출하게 됩니다.

( int 타입의 매개 변수를 받는 생성자가 있음에도 불구하고 )

CWidjet( initializer_list<long double> li ){
	cout << "Initializer_List Constructor called\n";
}

▼출력

int and bool constructor called
Initializer_List Constructor called
int and double constructor called
Initializer_List Constructor called

 

심지어, 만약 위의 initializer_list <long double> 생성자 대신에, 다음의 생성자를 추가한다면 컴파일러는 오류를 발생시킵니다.

CWidjet( initializer_list<bool> li ){
	cout << "Initializer_List Constructor called\n";
}

//---------------------------------------------------------

int main(){

	CWidjet w1( 10, true );
	CWidjet w2{ 10, false };	// error !

	CWidjet w3( 20, 3.5 );
	CWidjet w4{ 20, -3.5 };		// error !
}

이것은, 컴파일러가 초기화 리스트의 값들을 initializer_list <bool> 객체에 담아야 되는데, 균일 초기화는 이러한 좁히기 변환( narrowing conversion )을 금지하기 때문입니다.

 

따라서, initializer_list 생성자를, 균일 초기화 방식의 코드가 많이 작성된 경우 후에 추가하려 한다면, 기존의 코드들이 정상적으로 동작하지 않을 가능성이 발생하게 됩니다.

 

 

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유