[전문가를 위한 C++] 18장. 표준 라이브러리 알고리즘 마스터하기

Chapter 18. 표준 라이브러리 알고리즘 마스터하기

알고리즘 개요

std::function

  • <functional> 헤더 파일에 정의된 std::function 템플릿을 이용하면 함수를 가리키는 타입, 함수 객체, 람다 표현식을 비롯하여 호출 가능한 모든 대상을 가리키는 타입을 생성할 수 있다.
  • std::function을 다형성 함수 래퍼라고도 부르며 함수 포인터로도 사용할 수 있고, 콜백을 구현하는 함수를 나타내는 매개변수로 사용할 수도 있다.

    std::function<R(ArgType, ...)>
    // R : 리턴 타입
    // ArgType : 콤마로 구분한 매개변수
    
  • std::function으로 함수 포인터를 구현하는 방법은 다음과 같다.

    void func(int num, const string& str) {
      cout << "func(" << num << ", " << str << ")" << endl;
    }
    
    int main() {
      function<void(int, const string&)> f1 = func;
      // auto keyword를 사용할 수 있다.
      // auto f1 = func;
      f1(1, "test");
      return 0;
    }
    
  • std::function 타입은 함수 포인터처럼 작동하기 때문에 표준 라이브러리 알고리즘에 인수로 전달할 수 있다. find_if() 알고리즘에 적용한 예는 다음과 같다.

    bool isEven(int num) {
      return num % 2 = 0;
    }
    
    int main() {
      vector<int> vec{1,2,3,4,5,6,7,8,9};
    
      function<bool(int)> fcn = isEven;
      auto result = find_if(cbegin(vec), cend(vec), fcn);
      if (result != cend(vec)) {
        cout << "First even number: " << *result << endl;
      } else {
        cout << "No Even number found." << endl;
      }
    
      return 0;
    }
    
  • 람다 표현식을 process()함수의 std::function 매개변수의 값으로 지정할 수 있다.

     void process(const vector<int>& vec, function<void(int)> f) {
      for (auto& i : vec) {
        f(i);
      }
     }
    
     void print(int num) {
      cout << num << " ";
     }
    
     int main() {
      vector<int> vec {0,1,2,3,4,5,6,7,8,9};
    
      process(vec, print);
      cout << endl;
    
      int sum = 0;
      process(vec, [&sum](int num) { sum += num; });
    
      cout << "sum = " << sum << endl;
      return 0;
    
      // 0,1,2,3,4,5,6,7,8,9
      // sum = 45
     }
    
  • 콜백 매개변수로 std::function을 사용하지 않고, 다음과 같이 함수 템플릿으로 만들어도 된다.

     template<typename F>
     void processTemplate(const vector<int>& vec, F f) {
      for (auto& i : vec) {
        f(i);
      }
     }
     // processTemplate()은 일반 함수 포인터와 람다 표현식을 모두 받을 수 있다.
    

람다 표현식

  • 함수나 함수 객체를 정의하지 않고, 필요한 지점에서 곧바로 함수를 직접 만들어 쓸 수 있는 일종의 익명 함수.
  • [] : 람다 선언자 (람다 소개자), 캡쳐 블록
  • {} : 람다 표현식 본문

     auto BasicLambda = [] { cout << "Hello from Lambda" << endl; };
     BasicLambda();  // 이렇게 호출할 수 있다. 
    
  • 캡쳐 블록

    double data = 1.23;
    auto capturingLambda = [data] { cout << "Data=" << data << endl; };
    // 람다 표현식은 자신이 속한 스코프에 있는 변수에 접근할 수 있다.
    
    • 캡쳐한 변수는 람다 표현식 (컴파일러 입장에서는 이름없는 펑터)의 멤버로 복제된다. const 속성은 계속 이어 받는다.
    • 펑터 마다 함수 호출 연산자 operator()가 구현되어 있는데 람다 표현식의 경우 const로 설정 된다.
    • const 속성을 이어 받은 캡쳐된 변수를 수정하려면 mutable로 람다 표현식을 지정하면 된다.

      double data = 1.23;
      auto capturingLambda = [data]() mutable { data *= 2; cout << "Data = " << data << endl; };
      // mutable을 지정할 때는 매개변수가 없어도 받드시 소괄호를 적어야 한다.
      
    • 레퍼런스로 캡쳐하여 수정할 수도 있다.

      double data = 1.23;
      auto capturingLambda = [&data] { data *= 2; };
      
    • 람다 표현식이 속한 스코프의 변수를 모두 캡쳐할 수도 있다.

      [-]  // 모든 변수를 값으로 캡쳐
      [&]  // 모든 변수를 레퍼런스로 캡쳐
      
    • 캡쳐 리스트를 지정하면 골라서 지정할 수 있다.
    • [*this] : 현재 객체의 복제본을 캡쳐한다.
  • 람다 캡쳐 표현식
    • 람다 캡쳐 표현식은 사용할 캡쳐 변수를 초기화 할 수 있다.
      • 스코프에 있는 변수 중 캡쳐하지 않았던 것을 람다 표현식에 가져오는 데 사용할 수 있다.

        double pi = 3.1415;
        auto myLambda = [myCapture = "Pi : ", pi] { cout << myCapture << pi << endl; };
        // myCapture란 변수를 "Pi :"로 초기화 하고, 람다 표현식과 같은 스코프에 있던 pi 변수를 캡쳐한다.
        // myCapture처럼 비레퍼런스 캡쳐 변수를 캡쳐 이니셜라이저로 초기화할 때는 복제 방식으로 생성된다. 
        
      • 람다 캡쳐 변수는 std::move()를 비롯한 모든 종류의 표현식으로 초기화할 수 있다. unique_ptr처럼 복제할 수 없고 이동만 가능한 객체를 다룰 때 이 점을 반드시 명심해야 한다.
      • 기본적으로 값으로 캡쳐하면 복제 방식으로 적용된다.
      • 따라서 unique_ptr을 람다 표현식에서 값으로 캡쳐할 수 없다.
      • 하지만 람다 캡쳐 표현식을 사용하면 다음과 같이 이동 방식으로 복제할 수 있다.

        auto myPtr = std::make_unique<double>(3.145);
        auto myLambda = [p = std::move(myPtr)] { cout << *p; };
        
  • 람다 표현식을 리턴 타입으로 사용하기
    • std::function을 이용하면 함수가 람다 표현식을 리턴하게 만들 수 있다.

      function<int(void)> multiplyBy2Lambda(int x) {
        return [x] { return 2 * x; };
      }
      // 리턴 타입은 인수를 받지 않고 정수를 리턴하는 함수인 function<int(void)>다. 
      
      // 아래와 같이 호출한다.
      function<int(void)> fn = multiplyBy2Lambda(5);
      cout << fn() << endl;
      /*
      auto fn = multiplyBy2Lambda(5);
      cout << fn() << endl;
      */
      
      // 아래와 같이 더 세련되게 표현할 수 있다.
      auto multiplyBy2Lambda(int x) {
        return [x] { return 2 * x; };
      }
      
    • 리턴한 람다 표현식은 함수가 끝난 뒤에 사용된다. x를 [&x]로 캡쳐하면 multiplyBy2Lambda() 함수의 스코프는 존재하지 않기 때문에 x의 레퍼런스는 이상한 값을 가리키게 된다.

  • 람다 표현식을 매개변수로 사용하기
    • std::function 타입의 함수 매개변수는 람다 표현식을 인수로 받을 수 있다. (std::function 부분 다시 읽어보자)
  • 표준 라이브러리 알고리즘 활용 예제
    • count_if

      vector<int> vec {1,2,3,4,5,6,7,8,9};
      int value = 3;
      int cnt = count_if(cbegin(vec), cend(vec),
                          [value](int i) { return i > value; }]);
      
    • generate

      vector<int> vec(10);
      int value = 1;
      generate(cbegin(vec), cend(vec),
            [&value] { value *= 2; return value; });
      

함수 객체

표준 라이브러리 심층 분석

  • 반복자
  • 불변형 순차 알고리즘 : 검색, 비교, 집계
  • 가변형 순차 알고리즘 : 복사, 삭제, 순서 바꾸기
  • 연산 알고리즘
  • swap, exchange 알고리즘
  • 분할 알고리즘
  • 정렬 알고리즘
  • 이진 탐색 알고리즘
  • 집합 알고리즘
  • 최대/최소 알고리즘
  • 병렬 알고리즘
  • 수치 처리 알고리즘