범위 기반 for ( range-based for )
범위 기반 for는 배열이나 컨테이너 같이 데이터의 연속을 나타내는 객체의 모든 멤버를 순환하는 구문입니다.
다음의 예문은 std::vector 객체의 원소를 순환하기 위해 범위 기반 for 구문을 사용하는 법을 보여줍니다.
int main(){
using std::cout; // using 선언문
vector vec = { 1, 2, 3, 4, 5 };
for( const auto& x : vec ){ // 범위 기반 for
cout << x << ", ";
}
cout << '\n';
}
▼출력
1, 2, 3, 4, 5,
이 기능은 원소의 인덱스를 신경 쓸 필요가 없는 경우에 상당히 간편한 기능입니다.
원소를 처리할 변수의 이름( 여기선 x )과 속성( const나 참조 )만 설정하면, 나머지는 for 구문의 몸체만 신경 쓰면 되니까요.
이러한 범위 기반 for에 대한 내용은 여기에서 볼 수 있습니다.
그래서, 이 기능을 직접 작성한 사용자 객체에도 사용하고 싶다는 생각을 하게 됩니다.
다음의 예문은 int 타입의 배열을 담고 있는 사용자 정의 클래스를 보여줍니다.
#include <iostream>
#include <initializer_list> // for initializer_list
#include <algorithm> // for std::copy()
using namespace std;
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( const IntArray& ) = delete;
IntArray& operator=( const IntArray& ) = delete;
~IntArray(){
delete [] m_pArray;
}
};
이 클래스는 중괄호 초기화를 사용해서 초기화할 수 있도록, 초기화 리스트 매개변수를 사용하는 생성자를 지원하고, 설명에는 필요 없는 복사 생성자와 복사 대입 연산자를 삭제하였습니다.
그리고, 이 객체는 생성자에서 배열을 메모리에 할당하므로, 소멸자에서 이를 삭제하는 기능을 갖고 있습니다.
그런데, 이 클래스 객체의 원소를 출력하려고 범위 기반 for 구문을 사용하면, 컴파일러는 오류를 발생시킵니다.
int main(){
IntArray arr = { 1, 2, 3, 4, 5 }; // 초기화 리스트를 사용한 초기화
for( auto x : arr){ // 범위 기반 for. error !
cout << x << " ";
}
cout << '\n';
}
왜 그럴까요?
그리고, std::vector<int> 와 같이 범위 기반 for( ranged-base for ) 구문을 사용하려면 어떻게 해야 할까요?
범위 기반 for 구문이 작동하는 방법
위의 예문에서 컴파일러가 범위 기반 for 구문을 만나게 되면, 이 구문을 확장합니다.
for( auto x : arr){ // 범위 기반 for
cout << x << " ";
}
즉, 위의 코드를 아래와 같은 코드로 변경한다는 것입니다. ( C++ 17 기준 )
auto iter = arr.begin();
auto iter_end = arr.end();
for( ; iter != iter_end; ++iter ){
auto x = *iter;
cout << x << " ";
}
따라서, IntArray 클래스 객체를 범위 기반 for에서 사용할 수 있도록 하려면, 이 클래스는 위와 같은 begin 함수와 end 함수를 지원해야 합니다.
그리고, 코드를 보면, begin 함수가 반환하는 객체 iter는, IntArray의 원소를 지목하고, 원소 간의 이동이 가능하며( ++
연산 ), 원소의 값에 접근( * 연산 ) 할 수 있어야 한다는 것을 알 수가 있습니다.
또, 원소 탐색이 컨테이너의 끝까지 도달했음을 알기 위해서 비교 기능( != 연산 )도 필요합니다.
C++에서는 이러한 기능을 가진 객체를 반복자( iterator )라고 합니다.
즉, IntArray 객체의 원소를 대상으로 하는 반복자를 만들고, 이 반복자를 반환하는 begin 함수와 end 함수를 만들면 문제를 해결할 수 있습니다.
먼저, 반복자( iterator )에 필요한 함수들은 작성하면 다음과 같습니다.
class iterator{
int* m_ptr; // 클래스 멤버는 기본적으로 private
public:
iterator( int* ptr) : m_ptr(ptr){} // 생성자
int& operator*(){ return *m_ptr;}
bool operator!=(const iterator& other){ return m_ptr != other.m_ptr;}
iterator& operator++(){ ++m_ptr; return *this; }
};
이 기능들은 *, !=, ++ 연산자로, 이 연산자들은 순방향 반복자( forward iterator )의 부분집합입니다.
C++는 순방향 반복자로서 제 역할을 하려면 5개의 연산자( ++, *, ->, ==, != )를 구현할 것을 요구하지만, 여기서는 반드시 있어야 할 연산자만 작성했습니다.
그리고, 이 반복자 클래스는 IntArray 객체의 int 배열의 원소만 접근할 것이기 때문에, int 타입의 포인터를 사용했습니다.
또한, 이 반복자는 IntArray와 아주 밀접하게 연관된 클래스이므로, 이 클래스를 중첩 클래스( nested class type )로 만들 것입니다.
중첩 클래스에 관한 내용은 여기서 볼 수 있습니다.
IntArray의 begin 함수는 첫 번째 원소를 가리키는 반복자를 반환해야 하므로 다음과 같이 될 것입니다.
iterator begin() const{ // 첫 번째 원소를 가리키는 반복자 반환
return iterator(m_pArray);
}
그리고, IntArray의 end 함수는 마지막 원소의 다음 원소를 가리키는 반복자를 반환해야 합니다.
iterator end() const{ // 마지막 원소 다음 원소의 주소를 반환
return iterator(m_pArray + m_nLength);
}
이 모든 사항을 적용한 IntArray는 다음과 같습니다.
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( const IntArray& ) = delete;
IntArray& operator=( const IntArray& ) = delete;
~IntArray(){
delete [] m_pArray;
}
class iterator{ // 중첩 타입인 반복자
int* m_ptr;
public:
iterator( int* ptr) : m_ptr(ptr){}
int& operator*(){ return *m_ptr;}
bool operator!=(const iterator& other){ return m_ptr != other.m_ptr;}
iterator& operator++(){ ++m_ptr; return *this; }
};
// 반복자를 반환하는 begin과 end
iterator begin() const{
return iterator(m_pArray);
}
iterator end() const{
return iterator(m_pArray + m_nLength);
}
};
이제, 이 클래스의 객체를 범위 기반 for 구문에서 사용만 하면 되겠네요.
int main(){
IntArray arr = { 1, 2, 3, 4, 5 }; // 초기화 리스트를 사용한 초기화
for( auto x : arr){ // 범위 기반 for
cout << x << " ";
}
cout << '\n';
}
▼출력
1 2 3 4 5
이제, 왜 std::vector 컨테이너는 범위 기반 for 구문 사용할 수 있고, IntArray 클래스는 사용할 수 없었던 이유를 알았을 것입니다.
대부분의 C++ 표준 라이브러리 컨테이너들은 std::vector와 같이 begin 함수와 end 함수를 지원하고, 이 함수를 통해서 반복자를 제공합니다.
그리고, 이 반복자는 최소한 양방향 반복자( bidirectional iterator )로서, 순방향 반복자( forward iterator ) 보다 더 많은 연산자를 지원합니다.
이는 범위 기반 for가 요구하는 기능을 훨씬 넘어섭니다.
이러한 반복자에 관한 내용은 여기서 볼 수 있습니다.
끝으로, 이러한 범위 기반 for 구문은 코드를 읽게 쉽게 만들고, 사용하기 쉽습니다.
하지만, 이제 이 구문의 확장 방식을 보았으니, 이 기능의 한계점을 알게 되었을 것입니다.
첫 번째, 내부적으로 ++ 연산만을 수행하기 때문에, 사용하는 객체의 원소 탐색이 한 방향으로만 이루어져야 한다는 것입니다.
두 번째, 탐색이 끝나는 지점을 미리 정해두고, 탐색을 수행하기 때문에, for 구문의 몸통 코드에서 새로운 원소를 추가하거나 삭제하면, 정의되지 않은 결과를 가져올 수 있습니다.
이 글과 관련 있는 글들
균일 초기화( uniform initialization )와 std::initializer_list
'C, C++ > C++ 언어' 카테고리의 다른 글
[C++] 접근 권한을 부여받는 friend 클래스 (2) | 2024.11.11 |
---|---|
[C++] 접근 권한을 부여하는 friend 키워드 (2) | 2024.11.10 |
[C++] 연관된 기능을 묶기 위한 중첩 타입( nested type ) (0) | 2024.11.08 |
[C++] 값을 반환하기 위한 매개변수( out parameter )에 대하여 (4) | 2024.11.07 |
[C++] 함수에서 std::vector와 같은 객체를 반환하는 방법 (8) | 2024.11.03 |