리스트 초기화( 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 ) 방법으로, 생성하려고 하면, 기본 생성자가 호출될까요, 아님 원소의 개수가 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";
}
};
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생성자를 선호하는 경향이 있으므로, 원하는 생성자가 호출되지 않을 수도 있습니다.
이 글과 관련 있는 글들
'표준 라이브러리' 카테고리의 다른 글
| [C++] 내부 삽입을 통해 원소를 추가할 수 있는 std::emplace (0) | 2025.03.27 |
|---|---|
| [C++] 여러 개의 값을 저장할 수 있는 std::tuple (0) | 2025.03.26 |
| [C++] 문자열을 읽기 전용으로 참조하는 std::string_view (3) | 2025.03.19 |
| [C++] 호출 가능한 객체를 저장하는 std::function (0) | 2025.03.17 |
| [C++] 주어진 원소들에 함수를 적용하는 std::for_each (0) | 2025.03.12 |
