스토리텔링 개발자

[Effective STL] 39. predicate는 순수 함수일 것 본문

개발/Effective STL

[Effective STL] 39. predicate는 순수 함수일 것

김디트 2025. 1. 7. 11:12
728x90

항목 39. 술어 구문은 순수 함수로 만들자

 

 

 

술어 구문(predicate)
  • bool값을 반환하는 함수
  • 판독의 기준이 되는 로직을 포함하는 구문이다.

 

 

 

순수 함수(pure function)
  • 함수가 반환하는 값이 그 함수의 매개 변수에 종속되는 함수
  • f(x, y)의 반환값은 x, y가 바뀔 때에만 변한다.
  • 즉, 매개변수가 아닌 내부에 존재하는 데이터는 const여야 한다.

 

 

 

술어 구문 클래스(predicate class)
  • operator()가 술어 구문인 함수 객체 클래스

 

 

술어 구문 클래스는 복사에 신경써야 한다.
  • 함수 객체는 값으로 전달되기 때문이다.(항목 38 참조)
  • 그리고 STL 알고리즘이 내부적으로 함수 객체의 사본을 따로 보관할 수도 있다.
    • 그러므로 술어 구문 함수는 반드시 순수 함수여야 한다.

 

 

 

잘못 설계된 술어 구문 클래스
class BadPredicate
{
public:
    BadPredicate() : timesCalled(0) {}
    bool operator()(const Widget&)
    {
        return ++timesCalled == 3;
    }
private:
    size_t timesCalled; // 해당 값이 변경되며 순수 함수가 아니게 된다.
};

vector<Widget> vw;
...
// 3번째 요소만 삭제하고 싶다.
vw.erase(remove_if(vw.begin(), vw.end(), BadPredicate()), vw.end());
// 하지만 3번째 요소 뿐 아니라 6번째 요소도 삭제되어 버린다!
  • 그 이유는 remove_if가 대체로 아래처럼 구현되기 때문이다.
template<typename FwdIterator, typename Predicate>
FwdIterator remove_if(FwdIterator begin, FwdIterator end, Predicate p)
{
    // find_if : 주어진 범위 내에서 조건을 만족하는 첫 번째 요소 탐색
    begin = find_if(begin, end, p); // timesCalled가 0인 p가 복사 전달된다.
    if(begin == end)
    {
        return begin;
    }
	else
    {
    	FwdIterator next = begin;
        // remove_copy_if : 주어진 범위 내에서 조건을 만족하는 요소를 제외한 나머지 요소를 다른 범위로 복사
        return remove_copy_if(++next, end, begin, p); // timesCalled가 0인 p가 다시 복사 전달된다.
    }
}

 

 

 

술어 구문 함수를 순수 함수로 만드는 방법
  • 술어 구문 클래스의 opeartor() 함수를 const 함수로 선언한다.
class BadPredicate
{
public:
    bool operator()(const Widget&) const
    {
        return ++timesCalled == 3; // const 선언 되었으므로 에러가 발생한다!
    }
    ...
};
  • 하지만 이 방법만으로는, 아래 데이터에 접근하여 수정하는 것을 막을 수 없다.
    • mutable로 선언된 멤버 데이터
    • const가 아닌 지역 static 객체
    • const가 아닌 클래스 static 객체
    • 네임스페이스 스코프 안에 있는 const가 아닌 객체
    • const가 아닌 전역 객체
  • 술어 구문이 되는 모든 형태는 위 데이터에 접근하여 순수 함수가 되지 않을 여지가 있다.
bool anotherBadPrecdicate(const Widget&, const Widget&)
{
    static int timesCalled = 0;
    return ++timesCalled == 3; // 순수 함수가 아니다!!!!
}
  • 람다의 경우 캡처를 사용하여 캡처까지 같이 복사되도록 하는 트릭을 사용할 수 있을 것이다.
    • 이 경우 순수 함수가 아니지만 정상 동작한다.
int timesCalled = 0;
auto lambda = [&timesCalled](const int&)
{
    // timesCalled가 캡쳐되었다.
    return ++timesCalled == 3;
};

vw.erase(remove_if(vw.begin(), vw.end(), lambda), vw.end()); // 정상 동작
728x90
Comments