[C++] 초기값 리스트를 이용한 객체 초기화와 std::initializer_list

반응형

리스트 초기화( list initialization )

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

int main(){

    int intArray[] { 1, 2, 3, 4, 5 }; // list initialization
    for( int x : intArray){	// 범위 기반 for
        cout << x << " ";
    }
}

여기서 말하는 리스트 초기화( list initialization )는, C++에서 변수를 초기화하기 위한 세 가지 방법 중 하나입니다.

 

[C++] 초기화( initialization )의 세 가지 방법

변수의 초기화( initialization )C++을 사용해서, 코딩을 하다 보면 의외로 다양한 방법으로 변수를 초기화할 수 있음을 알게 됩니다. 그런데, 대부분은 자기만의 특정한 방식의 초기화 방법

codingbonfire.tistory.com

 

그리고, std::vector의 객체도 역시 중괄호로 묶인 초기값 리스트를 사용하여 초기화할 수 있습니다.

#include <iostream>
#include <vector>

int main(){

    std::vector<int> vec = { 1, 2, 3, 4, 5 };	// 리스트 초기화
    for( auto x : vec ){
        std::cout << x << " ";
    }
}

 

그럼, 사용자 정의( user-defined ) 클래스 객체도, 위와 같은 방법으로 초기화할 수 있을까요?

 

아래는 객체 생성 시, 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];
    }

    virtual ~IntArray(){
        delete [] m_pArray;
    }

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

    int getLength() const {
	return m_nLength;
    }
};

 

그런데, 이 객체를, 위에서 봤던, std::vector를 사용할 때와 같은 방법으로 초기화하면, 오류가 발생합니다.

 

int main(){
    IntArray arr{ 1, 2, 3, 4, 5 };	// 리스트 초기화
}

▼출력

error: no matching function for call to 'IntArray::IntArray(<brace-enclosed initializer list>)

위의 메시지는, std::vector와 같은 방법으로 초기화하려면, std::initializer_list를 매개변수로 하는 생성자를 제공해야 한다고 말하고 있습니다.

 

참고로, std::vector 클래스는, 실제 다음과 같은 initializer_list 생성자를 지원하고 있습니다.

vector(initializer_list<value_type> __l,
	     const allocator_type& __a = allocator_type()){ //... }

 

 

std::initializer_list

std::initializer_list<T>는, 클래스의 생성자 및 다른 상황에서 사용할 수 있는, 지정된 타입 T의 객체 리스트를 나타내는 클래스 템플릿입니다.

 

그리고, 컴파일러는 { 1,2,3,4,5 }와 같은 중괄호의 초기화 리스트를 만나게 되면, 이 초기화 리스트를 initializer_list 객체로 변환합니다.

따라서, 위와 같은 초기화 리스트를 사용하여 초기화가 가능한 클래스를 만들려면, initializer_list 타입의 매개변수를 가진 생성자( constructor )를 제공하면 될 것입니다.

 

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

#include <initializer_list>

 

그리고, 이 initializer_list는 다음과 같은 멤버 함수 3개만 제공하는 가벼운 클래스입니다.

size 원소의 개수를 반환합니다.
begin 리스트의 첫 번째 원소를 가리킵니다.
end 리스트의 마지막 원소 다음을 가리킵니다.

 

게다가, initializer_list는, 문자열을 참조하는 std::string_view와 같은 방식으로 동작하는, 참조 클래스입니다.

 

[C++] 문자열을 읽기 전용으로 참조하는 std::string_view

std::string_viewstd::string_view는 문자열을 사용하면서 발생하는, 무거운 복사 과정을 줄이고자 만든 클래스입니다. C++ 17 이상의 버전에서 사용할 수 있는, 이 클래스는 실제 원본 문자열을 읽기 전용

codingbonfire.tistory.com

그러므로, 이 객체를 전달받는 함수의 매개변수에, ( 객체의 복사를 막기 위한 ) 참조 타입( string_view& )을 사용할 필요가 없습니다.

 

이제, 위의 IntArray 클래스에 initializer_list를 매개 변수로 받는 생성자를 추가합니다.

#include <initializer_list>	
#include <algorithm> // for std::copy()
#include <cassert>	

class IntArray{
    int m_nLength;
    int* m_pArray;

public:

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

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

    ~IntArray(){
        delete [] m_pArray;
    }

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

	int getLength() const {
		return m_nLength;
	}
};

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

 

그리고, 위에서 말했듯이, 이 생성자를 호출할 때, 중괄호로 묶인 초기화 데이터의 복사가 발생하지 않기 때문에, const initializer_list<int>& 타입의 매개 변수를 사용할 필요가 없습니다.

 

이제, 리스트 초기화( 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

 

 

클래스의 값-리스트 초기화( value-list initialization )

위에서 링크한, 초기화의 종류를 설명한 글에서, "빈 중괄호 초기화"를 값-리스트 초기화( value-list initialization )라고 얘기했었습니다.

int val{ }; // value-list initialization.

이 초기화의 결과로, 변수 val의 값은 0이 됩니다.

그래서, 이 값-리스트 초기화를 zero initialization이라고도 부릅니다.

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

그리고, 이 과정에서 클래스 각각의 데이터 멤버에 대하여, 다시 값-리스트 초기화를 하게 됩니다.

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{ };	// value-list initialization
    CWidjet w2 = { };	// value-list initialization
}

▼출력

Default Constructor called : 10
Default Constructor called : 10

위의 m_nVal의 기본값이 설정되지 않는 경우, 값-리스트 초기화를 하게 되면 이 변수의 값은 0이 됩니다.

하지만, 위와 같이, 기본값( 여기서는 10 )이 지정된 경우, 그 값으로 초기화됩니다.

그런데, initializer_list 생성자가 있는 클래스의 객체를, 값-리스트 초기화( value-list initialization ) 방법으로, 생성하려고 하면, 기본 생성자가 호출될까요, 아님 원소의 개수가 0initializer_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";
    }
};

int main(){
    CWidjet obj{ };	// value-list initialization
}

▼출력

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

 

std::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 생성자를 호출하게 됩니다.

// long double 타입의 initializer_list를 매개변수로 하는 생성자
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

위에서 보듯이, 리스트 초기화를 사용하면, CWidjet 클래스에는, int 타입의 매개 변수를 받는 생성자가 있음에도 불구하고, initializer_list<long double> 생성자를 호출합니다. ( 직접 초기화는 영향받지 않습니다 )

 

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

// bool 타입의 initializer_list를 매개변수로 하는 생성자
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 생성자를, 리스트 초기화( list initialization ) 방식의 코드가 많이 작성된 경우 후에 추가하려 한다면, 기존의 코드들이 정상적으로 동작하지 않을 수 있다는 것을 알고 있어야 합니다.


참고로, 아래와 같이 int 타입의 변수에 double 타입의 값을 대입하면, double 타입의 값이 int 타입의 값으로 변환됩니다.

int intVal( 6.5 ); // 직접 초기화

이 경우, 8바이트의 double 타입이 4바이트( 사용하는 환경에 따라 int의 크기가 다를 수 있습니다. ) 크기의 int 타입으로 변환이 되고, 입력된 6.5의 값도 6으로 변경됩니다.

이렇게, 타입이 차지하는 크기가 줄어드는 변환을 좁히기 변환( narrowing conversion )이라고 합니다.

 

정리

  • 사용자 정의 객체를 중괄호로 묶인 값들의 리스트로 초기화하려면, initializer_list를 매개 변수로 하는 생성자를 제공해야 합니다.
  • 컴파일러는 리스트 초기화 구문을 만나면, initializer_list 생성자를 선호하는 경향이 있으므로, 원하는 생성자가 호출되지 않을 수도 있습니다.

 

 

이 글과 관련 있는 글들

객체의 모든 원소를 순환하는 범위 기반 for 구문

 

 

 

 

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