C, C++/C++ 언어 / / 2024. 11. 9.

[C++] 범위 기반 for를 사용하기 위한 사용자 타입의 필요조건

범위 기반 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에 대한 내용은 여기에서 볼 수 있습니다.

 

[C++] 범위 기반 for 루프 (Range-based for loop) 사용법

범위 기반 for범위 기반 for 루프는 배열이나 컨테이너 같이 데이터의 연속을 나타내는 객체의 모든 멤버를 순환하는 구문입니다.이 기능은 C++11부터 도입되었습니다. 선언 방식은 다음과 같습니

codingembers.tistory.com

 

그래서, 이 기능을 직접 작성한 사용자 객체에도 사용하고 싶다는 생각을 하게 됩니다.

다음의 예문은   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 )로 만들 것입니다.

 

중첩 클래스에 관한 내용은 여기서 볼 수 있습니다.

 

[C++] 연관된 기능을 묶기 위한 중첩 타입( nested type )

중첩 타입( nested type )이란C++에서 클래스 타입( class type )이란 클래스( class )와 구조체( struct ), 그리고 공용체( union )를 말합니다.이러한 클래스 타입은 멤버 변수와 멤버 함수 외에, 객체의 타입

codingembers.tistory.com

 

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가 요구하는 기능을 훨씬 넘어섭니다.

 

이러한 반복자에 관한 내용은 여기서 볼 수 있습니다.

 

[C++] 반복자( Iterator )에 대한 설명과 종류

반복자( Iterator )란 무엇인가?Iterator는 컨테이너에서 원소의 위치를 표현하는, 포인터와 같은 객체입니다. 여기서, 컨테이너란 다른 객체들을 담는 집합 객체라고 할 수 있습니다. 컨테이너의

codingembers.tistory.com

 

끝으로, 이러한 범위 기반 for 구문은 코드를 읽게 쉽게 만들고, 사용하기 쉽습니다.

하지만, 이제 이 구문의 확장 방식을 보았으니, 이 기능의 한계점을 알게 되었을 것입니다.

첫 번째, 내부적으로   ++   연산만을 수행하기 때문에, 사용하는 객체의 원소 탐색이 한 방향으로만 이루어져야 한다는 것입니다.

두 번째, 탐색이 끝나는 지점을 미리 정해두고, 탐색을 수행하기 때문에, for 구문의 몸통 코드에서 새로운 원소를 추가하거나 삭제하면, 정의되지 않은 결과를 가져올 수 있습니다.

 

 

이 글과 관련 있는 글들

균일 초기화( uniform initialization )와 std::initializer_list

함수 삭제( = delete )를 통한 기능 제어

 

 

 

 

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