스토리텔링 개발자

[Effective STL] 46. 함수 vs 함수 객체(람다) 본문

개발/Effective STL

[Effective STL] 46. 함수 vs 함수 객체(람다)

김디트 2025. 1. 16. 11:51
728x90

항목 46. 알고리즘의 매개 변수로는 함수 대신 함수 객체가 괜찮다

 

 

 

추상화(abstraction)와 성능
  • 추상화가 높을수록 코드의 효율은 낮아진다.
  • 그런데 함수 객체가 함수보다 빠르다니?

 

 

 

double의 벡터를 내림차순으로 정렬하기
vector<double> v;
...
// 방법 1
sort(v.begin(), v.end(), greater<double>()); // 함수 객체 greater를 사용했다.

// 방법 2
inline bool doubleGreater(double d1, double d2)
{
    return d1 > d2;
}
sort(v.begin(), v.end(), doubleGreater); // 함수를 사용했다.
  • 방법 1이 거의 확실히 더 빠르다.
  • 이유는 인라인화(inlining)이다.

 

 

 

인라인화(inlining)
  • 인라인으로 선언되어 있고, 함수의 몸체가 있다면
    • 대부분의 컴파일러는 호출된 알고리즘의 템플릿 인스턴스화 과정에서 이를 인라인화 한다.
  • greater<double>::operator()는 인라인화 하므로..
    • sort가 인스턴스화 할 때 이 함수를 인라인 확장(inline-expand) 하게 된다.
    • 즉, sort는 함수 호출을 전혀 하지 않는다.
  • doubleGreater는 인라인화 되지 않는다.
    • 함수는 복사로 동작할 수 없으므로, 함수 포인터로 넘겨진다.
    • 보통의 컴파일러는 포인터 함수에 대해 인라인화를 하지 않는다.
    • 그러므로 sort 내부에서 사용될 때마다 간접 함수 호출을 매번 하게 된다.

 

 

 

C++의 sort와 C의 qsort
  • 재미있게도 함수 포인터 매개 변수가 인라인되지 않는다는 사실 때문에 qsort보다 sort가 속도 면에서 앞서게 된다.

 

 

 

함수 객체를 더 선호하는 또 다른 이유들
  • STL 플랫폼에서 완벽하게 유효한 코드이지만 컴파일을 거부하는 상황이 발생할 수 있다.
set<string> s;
...
// 해당 STL 플랫폼이 const 멤버 함수(string::size)를 처리하는 데 버그를 가지고 있어서
// 컴파일 에러가 발생하는 상황이라 가정하면?
transfrom(s.begin(), s.end(), ostream_iterator<string::size_type>(cout, "\n"),
                              mem_fun_ref(&string::size));
                              
// 아래처럼 함수 객체로 대처한다.
struct StringSize
{
    string::size_type operator()(const string& s) const
    {
        return s.zie();
    }
};
transfrom(s.begin(), s.end(), ostream_iterator<string::size_type>(cout, "\n"),
                              StringSize());
  • 함수 객체가 미묘한 언어 문제를 막아줄 수 있다.
    • 함수 템플릿의 인스턴스 이름이 함수 이름과 같지 않은 경우
// 두 부동소수점 실수의 평균 반환
template<typename FPType>
FPType average(FPType val1, FPType val2)
{
    return (val1 + val2) / 2;
}

// 숫자열에서 두 개씩 짝을 맞춘 후 각각의 평균을 스트림에 기록
template<typename InputIter1, typename InputIter2>
void writeAverages(InputIter1 begin1, InputIter1 end1, InputIter2 begin2, ostream& s)
{
    transform(begin1, end1, begin2,
              ostream_iterator<typename iterator_traits<InputIter1>::value_type>(s, "\n"),
              average<typename iterator_traits<InputIter1>::value_type> // 에러인가?
     );
}

// 아래처럼 수정한다.
// 함수 객체 작성
template<typename FPType>
struct Average
{
    FPType operator()(FPType val1, FPType val2) const
    {
        return average(val1, val2);
    }
}

template<typename InputIter1, typename InputIter2>
void writeAverages(InputIter1 begin1, InputIter1 end1, InputIter2 begin2, ostream& s)
{
    transform(begin1, end1, begin2,
              ostream_iterator<typename iterator_traits<InputIter1>::value_type>(s, "\n"),
              Average<typename iterator_traits<InputIter1>::value_type> // 함수 객체 사용
     );
}
  • 보통은 통과되지만, C++ 표준안에서는 금지하고 있다.
    • 이론적으로 타입 매개 변수를 하나 가지는 average란 이름의 다른 함수 템플릿이 존재할 수 있다.
    • 그러므로 average<typename iterator_traits<InputIter1>::value_type>은 애매한 표현식일 수 있다.
    • 함수 객체로 수정하여 보완한다.
  • 함수 객체로 수정한 위의 모든 사항은 사실 모던 C++에서는 람다로도 처리가 가능하다.
728x90
Comments