Fundamental of CS/: : C++

[Functional Programming] Function Pointer, std::function

Jay.P Morgan 2024. 7. 31. 20:11

 

함수 포인터와 고차 함수

이번 시간에는 앞에서 다루지 않았던 함수 포인터(Function Pointer)에 대해 알아보고, 이후에는 함수를 다른 함수의 인자로 전달하는 방법에 대해 살펴보겠습니다.

먼저 간단한 함수를 하나 정의해 보겠습니다.

void FreeFunction(int i)
{
    std::cout << "Free Function " << i << std::endl;
}

이 함수는 정수 값을 받아 출력하는 일반적인 함수입니다. FreeFunction(10)처럼 호출하면 "Free Function 10"이 출력됩니다.

이번에는 같은 기능을 함수 객체(Function Object, Functor)로 만들어 보겠습니다.

함수 객체는 클래스에서 함수 호출 연산자(operator())를 오버로딩하여 함수처럼 사용할 수 있는 객체입니다.

class FunctionObject
{
public:
    void operator()(int i)
    {
        std::cout << "Function Object " << i << std::endl;
    }
};

이 객체 역시 함수처럼 호출할 수 있습니다.

FunctionObject obj;
obj(10);

실행 결과:

Function Object 10

마지막으로 람다 표현식을 이용해 동일한 기능을 만들어 볼 수 있습니다.

auto LambdaFunction = [](int i)
{
    std::cout << "Lambda Function " << i << std::endl;
};

이 역시 함수처럼 호출할 수 있습니다.

LambdaFunction(10);

결과:

Lambda Function 10

여기서 중요한 차이점이 있습니다.

  • 함수 객체와 람다 함수는 변수나 객체처럼 저장하고 전달할 수 있습니다.
  • 하지만 일반 함수(Free Function)는 그대로는 변수처럼 다룰 수 없습니다.

이러한 문제를 해결하기 위해 사용하는 것이 바로 함수 포인터입니다.

 

 

 

함수 포인터(Function Pointer)

C 언어 스타일의 일반 함수도 함수 포인터를 이용하면 변수처럼 가리킬 수 있습니다.

예를 들어 다음과 같이 선언할 수 있습니다.

void (*FunctionPointer)(int);

이는 반환 타입이 void이고, int 하나를 인자로 받는 함수를 가리키는 포인터라는 의미입니다.

여기에 기존 함수를 저장할 수 있습니다.

FunctionPointer = FreeFunction;

그리고 함수 포인터를 통해 호출할 수 있습니다.

(*FunctionPointer)(20);

실행 결과:

Free Function 20

즉, 함수 포인터를 사용하면 일반 함수도 변수처럼 저장하고 전달할 수 있습니다.

하지만 함수 포인터에는 한계가 있습니다.

우리가 원하는 것은 단순히 일반 함수만 전달하는 것이 아니라,

  • 일반 함수
  • 함수 객체
  • 람다 표현식

처럼 호출 가능한 모든 객체(callable object)를 전달하는 것입니다.

이때 사용하는 것이 바로 std::function입니다.

 

 

 

std::function을 이용한 함수 전달

std::function은 다양한 형태의 호출 가능한 객체를 하나의 타입으로 감싸주는 함수 래퍼(function wrapper)입니다.

먼저 헤더를 추가합니다.

#include <functional>

그리고 함수 타입을 정의합니다.

void RunFunction(const std::function<void(int)>& fn)
{
    fn(10);
}

이 함수는 int를 입력받고 void를 반환하는 모든 호출 가능한 객체를 받을 수 있습니다.

이제 여러 형태의 함수를 전달할 수 있습니다.

일반 함수 전달

RunFunction(FreeFunction);

결과:

Free Function 10

함수 객체 전달

FunctionObject obj;
RunFunction(obj);

결과:

Function Object 10

람다 표현식 전달

RunFunction([](int i)
{
    std::cout << "Lambda Function " << i << std::endl;
});

결과:

Lambda Function 10

즉, std::function을 사용하면 함수 포인터뿐만 아니라 함수 객체와 람다 표현식까지 모두 동일한 방식으로 처리할 수 있습니다.

 

 

 

여러 함수를 저장하고 실행하기

함수를 객체처럼 다룰 수 있다면 여러 개의 함수를 저장해 두고 순서대로 실행하는 것도 가능합니다.

예를 들어 다음과 같은 함수를 만들 수 있습니다.

void RunFunctions(const std::vector<std::function<void(int)>>& functions)
{
    for (const auto& fn : functions)
    {
        fn(10);
    }
}

그리고 여러 함수를 벡터에 저장할 수 있습니다.

std::vector<std::function<void(int)>> functions;

functions.emplace_back(FreeFunction);
functions.emplace_back(FunctionObject{});
functions.emplace_back([](int i)
{
    std::cout << "Lambda Function " << i << std::endl;
});

실행하면:

Free Function 10
Function Object 10
Lambda Function 10

처럼 저장된 함수들이 순서대로 실행됩니다.

이처럼 함수를 객체처럼 다루는 것은 함수형 프로그래밍의 핵심 개념입니다.

 

 

 

함수형 프로그래밍의 특징

함수형 프로그래밍에서는 함수를 단순히 실행하는 코드가 아니라 값처럼 취급할 수 있는 대상으로 봅니다.

함수는 입력(Input)과 출력(Output)을 가지며, 동일한 입력에는 항상 동일한 결과를 반환하도록 설계합니다.

예를 들어:

입력: 1, 2, 3

+1 함수 적용

출력: 2, 3, 4

처럼 입력과 출력의 관계가 명확합니다.

이 과정에서 함수 내부의 상태 변화나 외부 데이터 변경을 최소화하면 사이드 이펙트(side effect)가 없는 코드를 만들 수 있습니다.

또한 여러 함수를 조합하는 것도 가능합니다.

예를 들어:

  1. +1 함수
  2. ×2 함수
  3. -1 함수

를 순서대로 연결하면 하나의 새로운 처리 흐름을 만들 수 있습니다.

이러한 방식은 머신러닝 프레임워크에서도 활용됩니다.

예를 들어 신경망에서는:

  • 입력 데이터
  • 컨볼루션 레이어
  • 배치 정규화
  • 풀링 레이어
  • 출력 레이어

를 함수처럼 연결하여 하나의 명확한 데이터 흐름을 구성합니다.

 

 

 

C++에서 함수형 프로그래밍의 위치

함수형 프로그래밍이 강력하다고 해서 기존의 객체지향 프로그래밍이 필요 없어지는 것은 아닙니다.

각 패러다임은 적합한 문제가 다릅니다.

  • 객체지향 프로그래밍(OOP)은 객체와 상태를 중심으로 설계하는 문제에 적합합니다.
  • 함수형 프로그래밍은 데이터 변환, 병렬 처리, 함수 조합이 중요한 문제에 적합합니다.

C++는 여전히 객체지향 중심의 언어입니다.

하지만 C++11부터 추가된:

  • 람다 표현식
  • std::function
  • STL 알고리즘과 함수 객체

같은 기능 덕분에 함수형 프로그래밍 스타일도 자연스럽게 사용할 수 있게 되었습니다.

특히 람다 표현식은 코드의 가독성을 높이고, STL 알고리즘과 함께 사용할 때 매우 강력한 도구가 됩니다.

 

 

 

정리

이번 시간에는 다음 내용을 살펴보았습니다.

  • 일반 함수와 함수 객체, 람다 표현식의 차이
  • 함수 포인터를 이용해 함수를 변수처럼 다루는 방법
  • std::function을 이용해 다양한 호출 가능한 객체를 전달하는 방법
  • 함수를 저장하고 실행하는 방법
  • 함수형 프로그래밍의 기본 개념과 C++에서의 활용 방식

C++는 OOP 중심의 언어이지만, 람다 표현식과 STL을 활용하면 함수형 프로그래밍의 장점을 함께 가져올 수 있습니다.

처음에는 람다 표현식과 함수형 스타일이 낯설 수 있지만, 익숙해지면 코드의 가독성과 표현력을 높여주는 매우 유용한 프로그래밍 방식이 될 수 있습니다.