initializer_list의 소개
배열을 여러 원소들로 초기화하려면, 다음과 같이 균일 초기화( uniform initialization )를 사용하면 됩니다.
int main(){
int intArray[] { 1, 2, 3, 4, 5 }; // uniform initialization
for( int x : intArray){
cout << x << " ";
}
}
여기서, 균일 초기화는 C++에서 변수를 초기화하기 위한 세 가지 방법 중 하나입니다.
이러한 초기화들에 관한 내용은 여기에서 볼 수 있습니다.
그런데, 이러한 초기화 방법을 사용자 클래스를 초기화하는 데 사용하려면 어떻게 해야 할까요?
좀 더 정확히 말하자면, 아래에 정의한 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에 관한 내용은 여기에서 볼 수 있습니다.
이제, 이 생성자를 사용하는 균일 초기화( 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 생성자를, 균일 초기화 방식의 코드가 많이 작성된 경우 후에 추가하려 한다면, 기존의 코드들이 정상적으로 동작하지 않을 가능성이 발생하게 됩니다.
'C, C++ > 표준 라이브러리' 카테고리의 다른 글
[C++ 17] 함수의 결과와 값을 동시에 알려주는 optional 객체 (0) | 2024.09.25 |
---|---|
[C++] 오버로딩( overloading )을 통한 사용자 정의 데이터 입출력 (0) | 2024.09.24 |
[C++] getline 함수 사용법 (0) | 2024.08.24 |
[C++] std::tuple 사용법 (0) | 2024.08.09 |
[C++] transform 사용법 (0) | 2024.08.03 |