정체불명의 모모

Effective STL - Chapter6 [함수자, 함수 객체 , 함수, 기타 등등..] 본문

프로그래밍(c++)/Effective STL(C++)

Effective STL - Chapter6 [함수자, 함수 객체 , 함수, 기타 등등..]

정체불명의 모모 2021. 11. 8. 16:42

오오...

이제 거의 끝이 보입니다!

앞으로 한 챕터만 끝내면

STL 책은 이걸로 끝~~

물론, 이 내용으로 마스터 될게 아니니깐

더 공부해야 겠죠 ㅎㅎ

그래도 책한권을 또 끝낸다니 뿌듯하네요~

이번 장도 어렵지만 열심히 알아 봅시다.

 

이번장의 주요 내용은 함수자 입니다.

그럼,

Let's go~!


ㅁ 함수자란?

: 함수 객체(function object)라고도 부르며, 0개 이상의 인자를 받아서 알고리즘의 기본 동작을 변형하거나

 확장 시켜주는 객체를 얘기합니다.


함수자 클래스는 값으로 전달되도록(pass-by-value) 설계하자

: STL에서 함수 객체는 함수 사이를 오갈 때 값으로 전달(즉, 복사) 됩니다.

 

▷ for_each 알고리즘을 보면 함수 객체를 값으로 받아들이고 값으로 반환 한다.

template <class InputIterator, class Function>
Function for_each(InputIterator first , InputIeterator last , Function fn);

하지만, "값에 의한 전달"은 꼭 철칙이라고 보긴 힘듭니다.

왜냐하면, for_each를 호출하는 쪽에서 매개 변수의 타입을 명시적으로 정해 주지 않는 경우도 있기 때문입니다.

 즉, for_each가 함수자의 전달과 반환을 참조(reference)에 의해 수행하는 예도 있습니다.

 

▷for_each 참조자로 전달 , 반환 코드 예 (unary_function 에 대해서는 밑에서 자세히 설명하겠습니다.)

class DoSomething : public unary_function<int, void>
{
    void operator()(int x) {...}
 };
 
 typedef deque<int>::iterator DequeIter;
 deque<int> dInt;
 ...
 DoSomething dosomething;
 ...
 for_each<DequeIter , DoSomething&>(dInt.beging() , dInt.end() , dosomething);
 // for_each를 Dequeiter를 호출하며, DoSomething&를 전달하고, 반환하게 합니다.
 // 즉 dosomething은 참조에 의한 전달과 반환을 합니다.

※ 컴파일러에 따라 함수 객체가 참조로 전달될때 아예 컴파일이 되지 않도록 해놓는 경우도 있다.

그렇기에, 참조가 아닌 값으로 객체를 전달, 반환을 하라고 하는 것 입니다.

 

ㅁ 함수 객체를 전달할 때 중요한 포인트!

1. 객체를 최대한 작게 만들어라.

: 객체를 참조가 아니라, 값으로 전달, 반환 하기 때문에 객체를 복사하여 사용하게 됩니다.

  그렇기 때문에 메모리를 많이 차지 하지 않기 위해서 최대한 객체를 작게 만들어 비용을 줄여야 합니다.

 

2. 함수 객체를 단형성으로 만들어라

: 다형성과 반대로 가상 함수를 가지지 못하도록 해야 합니다.

  왜냐하면, 파생 클래스의 객체가 기본 클래스 타입의 매개 변수로서 값으로 전달되면 '슬라이스 문제'가 발생 합니다.

  복사 도중 파생 클래스에 새로 추가된 부분이 잘려 나간다는 뜻입니다.

 

 But! 하지만! 
1. 함수 객체가 기존의 함수를 능가하는 장점 중 하나가 필요한 상태를 유지할 수 있다는 것입니다.
: 이 장점이 없는 함수 객체는 무의미 할 수 있습니다.
  그 자체의 역할 때문에 자연스럽게 큼직해진 함수 객체가 당연히 있을 수 있습니다.

2. 다형성을 가진 함수자를 만들지 말라는 것이 비현실적
: C++는 구현 상속과 동적 바인딩을 지원하고 있으며, 이 기능은 함수자 클래스를 설계할 때에도 매우 유용합니다. 

 

▷ 데이터를 많이 가지고 있으며 다형성을 지닌 함수자 클래스를 만드는 방법 

1. 함수자 클래스에 넣고 싶은 데이터나 가상 함수를 뽑습니다.

2. 뽑은 것을 다른 클래스로 옮깁니다.

3. 함수자 클래스에다가 다른 클래스의 포인터를 넣습니다.

작고 , 가상함수를 가지지 않은 클래스를 하나 만들고, 데이터와 가상 함수를 가진 구현부 클래스를 또하나 만든 후에, 구현부 클래스에 대한 포인터를 작은 클래스(실제 함수자 클래스로 쓰입니다.)에 넣습니다.
template<typename T>
class A // 바뀐 A를 위한 새로운 구현부 클래스
{
 private:
   Widget w;
   int x;
   ...
   virtual ~A();
   
   virtual void operator()(const T& val) const;
   friend class B<T>;
};

template<typename T> 
class B : public unary_function<T,void> // 작고, 단형성을 가진 B 클래스
{
  private:
    A<T> *a;  // B가 가진 데이터는 구현부 클래스에 대한 포인터 뿐
    
  public:
   void operator()(const T& val) const 
   { 
     a->operator()(val); // 이 함수는 이제 가상함수가 아닙니다.
    }                        // A에게 호출을 전달할 뿐입니다. (슬라이스 문제 x)
  ...
};

위와 같이 하면, 크기도 작고 단형성을 유지하면서도, 많은 데이터를 액세스할 수 있고, 다형성 클래스처럼 

동작하는 함수자 클래스(B) 가 될 수 있습니다.


그렇기에 이는 두가지 의미를 가집니다.

1. 함수자는 작게 만들 것

2. 다형성을 없앨 것 

 


술어 구문은 순수 함수로 만들자
  • 술어 구문이란?
    : 술어 구문이란, bool 값(아니면 내부적으로 bool로 변환될 수 있는 어떤 값)을 반환하는 함수를 일컫습니다.
      STL에서 폭넓게 사용되는 함수이지요. 
      표준 연관 컨테이너의 비교 함수가 우선 술어 구문이고요. 술어 구문은 공통적으로 find_if 같은 알고리즘과
      여러 정렬 알고리즘의 매개 변수로 넘겨집니다.

  • 술어 구문 클래스란?
    : operator( ) 술어 구문인 함수자 클래스를 합니다.
      즉, 이 클래스의 operator( ) 가 true 나 false를 반환하는 것 입니다.
      STL에서는 술어 구문을 요구하는 모든 경우에 대하여 술어 구문이나 술어 구문 클래스의 객체를 사용해야 합니다.

  • 순수 함수란?
    : 함수가 반환하는 값이 그 함수의 매개 변수에 종속되는 함수를 일컫습니다.
      예를 들어 f가 순수 함수이고 x 와 y가 객체이면 f(x,y)의 반환 값은 x나 y의 값이 바뀔 때에만 변할 수 있습니다.
      c++에서는 , 순수 함수가 사용하는 모든 데이터는 매개 변수로 넘겨지든가 함수가 동작할 동안에 변하지 않은
      상태로 있게 됩니다.(쉽게 말해 이런 데이터는 const로 선언되는 데이터입니다.)
      
    ※ 똑같은 매개 변수를 넘겨서 여러 번 호출 했을 때 매번 다른 결과를 가지게 되면 '순수 함수'가 아닙니다.
즉, 술어 구문 클래스는 operator( ) 에 'const' 와 '순수 함수'를 이용하여, 정확한 동작을 할 수 있게 합니다.

▷ 예제 코드 ( 함수 호출 3번째인 객체를 지워라 )

// print()
//static size_t timesCalled;

class BadPredicated : public unary_function < Widget, bool>
{
public:
	BadPredicated() : _called(0)
	{
	//	timesCalled = 0;
	}

	bool operator()(const int&) 
	{
		return ++_called == 3;
	}

private:
	size_t _called;
};


int main()
{
	vector<int> vInt;

	for (int i = 1; i <= 6; i++)
		vInt.push_back(i);

	vInt.erase(remove_if(vInt.begin(), vInt.end(), BadPredicated()), vInt.end());

	cout << "Size : "<<vInt.size() << endl;
	print(vInt);
	return 0;
}

위 코드는 3번째 함수 호출할때 객체를 지워야 하는데, '3, 6'이 지워져 있다.

왜 그럴까??

우선, remove_if문에 대해 알아 보자

 

▷ remove_if문 

template<typename FwdIterator , typename Predicate>
FwdIterator remove_if(FwdIterator begin, FwdIterator end, Predicate p)
{
   begin = find_if(begin, end, p);
   if(begin == end) return begin;
   else
   {
      FwIterator next = begin;
      remove remove_copy_if(++next,end, begin ,p);
   }
}

위 코드를 보면 술어 구문 p가 find_if에 처음 전달되고 그 이후에 remove_copy_if에 값 전달 되고 있습니다.

 

remove_if를 처음 호출하면 익명의 BadPredicate 객체가 만들어지면서 이 객체의 _called 멤버가 0으로 초기화된

상태로 됩니다. 이 객체는 find_if로 복사되기 때문에, find_if는 또한 0의 _called 값을 가진 BadPredicate 객체를 

받게 됩니다.

 결론은 find_if는 사본만을 호출할 뿐 입니다.

그렇기 때문에 remove_copy_if는 자신의 술어 구문을 사용하는 셈이고, 이 술어 구문은 세번의 호출 후에 다시 true를

반환할 것 입니다. 


ㅁ 위와 같은 상황을 방지 하기위해

1. 술어 구문 클래스 안에 넣는 operator( ) 함수를 const로 선언하기
: 클래스의 데이터 멤버를 변경을 할 수 없습니다.

2. 순수 함수 이용 

 

▷ 코드 수정 후

// print()
static size_t timesCalled;

class BadPredicated : public unary_function < Widget, bool>
{
public:
	BadPredicated()
	{
		timesCalled = 0;
	}

	bool operator()(const int&) const
	{
		return ++timesCalled == 3;
	}
};


int main()
{
	vector<int> vInt;

	for (int i = 1; i <= 6; i++)
		vInt.push_back(i);

	vInt.erase(remove_if(vInt.begin(), vInt.end(), BadPredicated()), vInt.end());

	cout << "Size : "<<vInt.size() << endl;
	print(vInt);
	return 0;
}

정상적으로 작동


함수자 클래스는 어댑터 적용이 가능하게 만들자

▷ ptr_fun (함수 포인터 어뎁터 : adaptors for pointers to functions)

: ptr_fun이 하는 일은 typedef 타입 몇 개를 쓸 수 있게 만들어 주는 것 입니다.

 

□ ptr_fun을 사용하는 방법

: STL의 4대 표준 함수 어댑터(not1 ,not2 , bind1st, bind2nd)는 typedef 타입이 있어야 작동합니다.

 어댑터들이 요구하는 typedef를 제공하는 함수 객체를 "어댑터 적용이 가능(adaptable)하다"라고 하고,

 이런 typedef를 가지고 있지 않은 것들을 "어댑터 적용이 불가능하다."라고 일컫습니다.

  • 어댑터 적용 가능 하다.  -> typedef를 제공하는 함수 객체
  • 어댑터 적용이 불가능하다.

▷ 예제 코드

bool Pred(int n)
{
   return 30 <= n && n <= 40;
}

vector<int> v = {10,20,30,40,50};
// 30이상 40이하의 원소 개수 : 2개
count_if( v.begin(), v.end() , Pred);
// 30이상 40이하가 아닌 원소 개수 : 3개
count_if(v.begin(), v.end() , not1(ptr_fun(Pred)));
not1은 typedef 데이터여야 해서 'ptr_fun' 함수 포인터 어댑터를 이용하여 함수를 변환한다.

unary_function(템플릿) 

: unary_function 하나의 인수를 받는 operator()를 가진 함수자 클래스에 상속시킬 기본 구조체 입니다.

 

binary_function(템플릿)

: binary_function은 두개의 인수를 받는 경우에 함수자 클래스에 상속시킬 기본 구조체 입니다.

unary_function , binary_function은 템플릿입니다.
따라서 바로 상속시킬 수 있는 것이 아니라 , 템플릿으로 만든 구조체에 상속시켜야 합니다.
즉, 필요한 타입, 매개 변수를 지정해야 한다는 것 입니다.

 

▷ 예제 코드

template<typename T>
class MeetsThreshold : public std::unary_function<Widget , bool>
{
   private:
      const T threshold;
   public:
      MeetsThreshold(const T & threshold);
      bool operator()(const Widget&) const;
};

struct WidgetNameCompare : std::binary_function<Widget , Widget , bool>
{
   std::binary_function<Widget , Widget , bool>
   {
      bool operator()(const Widget & lhs, const Widget & rhs) const;
   }
};

만약, Widget을 포인터로 'Widget*'로 인자를 넘길때는 const를 붙여 넘겨 줘야 합니다.

struct PtrWidgetNameCompare : std::binary_function<const Widget* , const Widget* , bool>
{
   std::binary_function<const Widget* , const Widget* , bool> 
   { 
      bool operator()(const Widget* lhs, const Widget* rhs) const;
    };
}

중요한 것은 unary_function , binary_function은 함수 객체 어댑터에서 요구하는 typedef를 제공해 주기 때문에,

어댑터 적용이 가능한 함수 객체를 만들려면 이들 클래스에서 상속하는 것이 가장 쉽습니다.

STL의 함수자 클래스는 operator( ) 함수를 하나 밖에 가질 수 없다고 내부적으로 가정하고 있고, unary_function이나
binary_function에 넘겨질 매개 변수와 반환 값 타입도 한 가지 뿐입니다.
 결론적으로 하나의 구조체에다가 두개의 operator( ) 함수를 넣지 말라는 뜻 입니다.

mem_fun , mem_fun_ref의 존재에는 분명한 이유가 있다.

□ mem_fun , mem_fun_ref 의 존재 이유

: 이 함수들은 알고리즘이 멤버 함수를 호출할때 문법을 사용하도록 해줍니다.

  • 멤버 함수를 함수 객체로 변환
  • 알고리즘이 객체 원소의 멤버 함수 호출 가능
  •  STL에서 제공하는 두 가지 멤버 함수 포인터
    - mem_fun_ref( ) : 객체로 멤버 함수 호출
    - mem_fun( ) : 객체의 주소로 멤버 함수 호출
for_each(vw.begin() , vw.end() , test );  // 이와 같은 문법으로 멤버함수 호출하도록 도와줌
for_each(vw.begin() , vw.end(), &Widget::test);  // 컴파일 되지 않는다.
for_each(vw.begin(), vw.end(), mem_fun(&Widget::test)); // mem_fun을 통해 컴파일이 된다. (정상적인 문법으로 사용하게 해줌)

▷ 템플릿 선언문

template<typename R, typename C>  // 매개 변수를 받지 않고
mem_fun_t<R,C>                    // const 멤버가 아닌 멤버 함수에 대한
mem_fun(R (c::*pmf)());           // mem_fun의 선언문 , C는 클래스이며 
                                  // R은 포인터로 가리켜지는 멤버 함수(pmf)의 반환값 타입이다.

 

오늘 본 내용들은 

처음 보는 내용들이 많았습니다.

어렵진 않았는데,

이런 개념도 있었구나 하고 생각했어요.

아직도 너무 모르는게 많은것 같아요... ㅡㅜ

마지막 챕터를 끝내면

아예 기초책을 시작 하려고 합니다.

구글링으로 어떤 것을 볼까 찾아봤는데, 

저에게 가장 좋을 것 같은 책을 찾았습니다.

http://www.yes24.com/Product/Goods/23207535

 

Programming : Principles and Practice Using C++ 한국어판 - YES24

C++ 창시자가 전해주는 프로그래밍의 이상과 원리! ‘오래가는 프로그래머’, ‘존경받는 프로그래머’가 되고 싶다면 초보 프로그래머에서 익숙한 개발자까지 누구나 곁에 두고 읽어야 할 필

www.yes24.com

이 책을 끝내면 위 내용으로 정리해서 올리도록 하겠습니다 ㅎㅎ

그럼 마지막 챕터에서 뵙겠습니다!


※ 참고 사이트

https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=gemji&logNo=130012516602 

 

함수자(functor)

함수 객체(function object)라고도 부르며, 0개 이상의 인자를 받아서 알고리즘의 기본 동작을 변형하거나 ...

blog.naver.com

 

Comments