함수형 프로그래밍이란?
JavaScript 예제
function plus(a) {
let localVar = a;
return function(x) {
return localVar + x;
}
}
let plus3 = plus(3);
let plus5 = plus(5);
console.log(plus3(10)); // 13
console.log(plus5(10)); // 15
핵심 개념:
plus 함수는 다른 함수를 반환localVar를 "기억"함 (클로저)plus3과 plus5는 각각 다른 상태를 가진 함수Functor (함수 객체) 사용
class Plus
{
public:
explicit Plus(int a) : localVar{a} {}
int operator()(int x) const // () 연산자 오버로딩
{
return localVar + x;
}
private:
int localVar; // 상태를 저장
};
int main()
{
Plus plus3{3}; // localVar = 3인 함수 객체
Plus plus5{5}; // localVar = 5인 함수 객체
std::cout << plus3(10) << std::endl; // 13
std::cout << plus5(10) << std::endl; // 15
return 0;
}
동작 원리:
operator()를 오버로딩하여 객체를 함수처럼 호출 가능plus3(10) → plus3.operator()(10) 호출Functor의 장점:
람다 문법
[capture](parameters) -> return_type { body }
위 Functor를 람다로 변환
int main()
{
auto lambdaPlus3 = [localVar = 3](int x)
{
return localVar + x;
};
std::cout << lambdaPlus3(10) << std::endl; // 13
// 또는 직접 호출
std::cout << [](int x) { return 3 + x; }(10) << std::endl; // 13
return 0;
}
람다 vs Functor: 어셈블리 코드 동일
// Functor
Plus plus3{3};
plus3(10);
// Lambda
auto plus3 = [localVar = 3](int x) { return localVar + x; };
plus3(10);
// 둘 다 컴파일 후 동일한 어셈블리 코드 생성!
// 람다는 컴파일러가 자동으로 Functor 클래스를 생성
메모리 구조 비교
Functor의 메모리 구조:
Plus 객체 (sizeof: 4 bytes):
┌──────────────┐
│ localVar: 3 │ (4 bytes)
└──────────────┘
+ operator() 멤버 함수 코드 (코드 영역)
Lambda의 메모리 구조:
람다 객체 (sizeof: 4 bytes):
┌──────────────┐
│ localVar: 3 │ (4 bytes, 캡처된 값)
└──────────────┘
+ 컴파일러가 생성한 operator() 코드 (코드 영역)
람다의 장점:
캡처 방식
1. Capture by Value (값 복사)
int main()
{
int a = 10;
int b = 20;
// [=]: 모든 외부 변수를 값으로 캡처
auto lambda1 = [=]() {
std::cout << a << ", " << b << std::endl; // 10, 20
};
// [a, b]: 특정 변수만 값으로 캡처
auto lambda2 = [a, b]() {
std::cout << a + b << std::endl; // 30
};
// [a]: a만 값으로 캡처
auto lambda3 = [a]() {
std::cout << a << std::endl; // 10
// std::cout << b; // ❌ 컴파일 에러! b는 캡처하지 않음
};
a = 100; // 외부 변수 변경
lambda1(); // 여전히 10, 20 출력 (복사본이므로)
return 0;
}
2. Capture by Reference (참조)
int main()
{
int a = 10;
int b = 20;
// [&]: 모든 외부 변수를 참조로 캡처
auto lambda1 = [&]() {
std::cout << a << ", " << b << std::endl;
a = 100; // ✅ 외부 변수 수정 가능
};
// [&a, &b]: 특정 변수만 참조로 캡처
auto lambda2 = [&a, &b]() {
a += b; // 외부 a를 직접 수정
};
lambda1(); // 10, 20
std::cout << a << std::endl; // 100 (수정됨)
lambda2();
std::cout << a << std::endl; // 120
return 0;
}
3. 혼합 캡처
int main()
{
int a = 10;
int b = 20;
// a는 값으로, b는 참조로
auto lambda = [a, &b]() {
// a = 100; // ❌ 에러! a는 const (값 캡처)
b = 100; // ✅ OK (참조 캡처)
std::cout << a << ", " << b << std::endl;
};
lambda();
std::cout << b << std::endl; // 100 (수정됨)
return 0;
}
4. Init Capture (C++14)
int main()
{
// 새로운 변수를 람다 내부에서 생성
auto lambda = [value = 42, str = std::string("hello")]() {
std::cout << value << ", " << str << std::endl;
};
lambda(); // 42, hello
// Move capture
std::unique_ptr<int> ptr = std::make_unique<int>(100);
auto lambda2 = [p = std::move(ptr)]() {
std::cout << *p << std::endl;
};
// ptr은 이제 nullptr
return 0;
}
Dangling Reference 문제
std::function<void()> createLambda()
{
int localVar = 10;
// ❌ 위험! localVar은 함수가 끝나면 소멸
return [&localVar]() {
std::cout << localVar << std::endl; // Dangling reference!
};
}
int main()
{
auto lambda = createLambda();
lambda(); // ❌ Undefined Behavior! localVar은 이미 소멸됨
return 0;
}
해결 방법: Capture by Value
std::function<void()> createLambda()
{
int localVar = 10;
// ✅ 안전: 값으로 캡처
return [localVar]() {
std::cout << localVar << std::endl;
};
}
int main()
{
auto lambda = createLambda();
lambda(); // ✅ 10 출력
return 0;
}
힙 메모리의 경우도 주의
void dangerousCode()
{
auto ptr = std::make_shared<int>(42);
auto lambda = [&ptr]() { // ❌ 참조 캡처
std::cout << *ptr << std::endl;
};
// ptr이 여기서 소멸되면 lambda는 dangling reference를 가짐
}
캡처 선택 가이드
클래스 멤버 접근
class Counter
{
public:
Counter() : count{0} {}
void increment()
{
// [this]: 클래스의 this 포인터 캡처
auto lambda = [this]() {
count++; // 멤버 변수 접근 가능
std::cout << "Count: " << count << std::endl;
};
lambda();
}
void incrementMultiple(int times)
{
// 멤버 함수도 호출 가능
auto lambda = [this](int n) {
for (int i = 0; i < n; i++)
{
this->count++; // 명시적으로 this-> 사용 가능
}
display(); // 멤버 함수 호출
};
lambda(times);
}
void display() const
{
std::cout << "Final count: " << count << std::endl;
}
private:
int count;
};
int main()
{
Counter counter;
counter.increment(); // Count: 1
counter.incrementMultiple(5); // Final count: 6
return 0;
}
C++17: [*this] (Copy Capture)
class MyClass
{
public:
// 람다를 반환하는 경우: [*this] 필수!
std::function<void()> createCallback()
{
// ❌ [this]: 위험! 객체가 소멸되면 dangling pointer
// return [this]() {
// std::cout << data << std::endl;
// };
// ✅ [*this]: 안전! 객체 전체를 복사 (C++17)
return [*this]() {
std::cout << data << std::endl; // 복사본 사용
};
}
// 람다가 함수 내부에서만 사용되는 경우: [this] OK
void processData()
{
std::vector<int> numbers{1, 2, 3};
// ✅ [this]: 안전! 람다가 함수 밖으로 나가지 않음
std::for_each(numbers.begin(), numbers.end(), [this](int n) {
data += n;
});
}
private:
int data{42};
};
int main()
{
std::function<void()> callback;
{
MyClass obj;
callback = obj.createCallback(); // 객체 복사됨
} // obj 소멸
callback(); // ✅ 안전! 복사본 사용
return 0;
}
고차 함수란?
std::for_each
#include <algorithm>
#include <vector>
int main()
{
std::vector<int> numbers{1, 2, 3, 4, 5};
// 람다를 인자로 전달
std::for_each(numbers.begin(), numbers.end(), [](int n) {
std::cout << n << " ";
});
std::cout << std::endl; // 1 2 3 4 5
// 각 원소를 제곱
std::for_each(numbers.begin(), numbers.end(), [](int& n) {
n = n * n;
});
// numbers는 이제 {1, 4, 9, 16, 25}
return 0;
}
std::remove_if + erase
int main()
{
std::vector<int> numbers{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 짝수 제거
numbers.erase(
std::remove_if(numbers.begin(), numbers.end(), [](int n) {
return n % 2 == 0; // 짝수면 true
}),
numbers.end()
);
// numbers는 이제 {1, 3, 5, 7, 9}
for (int n : numbers)
{
std::cout << n << " ";
}
std::cout << std::endl;
return 0;
}
std::sort
int main()
{
std::vector<int> numbers{5, 2, 8, 1, 9, 3};
// 내림차순 정렬
std::sort(numbers.begin(), numbers.end(), [](int a, int b) {
return a > b; // a가 b보다 크면 a를 앞에
});
// numbers는 이제 {9, 8, 5, 3, 2, 1}
// 절댓값 기준 오름차순
std::vector<int> nums{-5, 2, -8, 1, -3};
std::sort(nums.begin(), nums.end(), [](int a, int b) {
return std::abs(a) < std::abs(b);
});
// nums는 이제 {1, 2, -3, -5, -8}
return 0;
}
std::transform
int main()
{
std::vector<int> numbers{1, 2, 3, 4, 5};
std::vector<int> squared(numbers.size());
// 각 원소를 제곱하여 새 벡터에 저장
std::transform(numbers.begin(), numbers.end(), squared.begin(),
[](int n) {
return n * n;
}
);
// squared는 {1, 4, 9, 16, 25}
return 0;
}
std::accumulate (std::reduce)
#include <numeric>
int main()
{
std::vector<int> numbers{1, 2, 3, 4, 5};
// 합계
int sum = std::accumulate(numbers.begin(), numbers.end(), 0,
[](int acc, int n) {
return acc + n;
}
);
std::cout << "Sum: " << sum << std::endl; // 15
// 곱셈
int product = std::accumulate(numbers.begin(), numbers.end(), 1,
[](int acc, int n) {
return acc * n;
}
);
std::cout << "Product: " << product << std::endl; // 120
return 0;
}
std::count_if
int main()
{
std::vector<int> numbers{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 짝수 개수 세기
int evenCount = std::count_if(numbers.begin(), numbers.end(),
[](int n) {
return n % 2 == 0;
}
);
std::cout << "Even count: " << evenCount << std::endl; // 5
return 0;
}
std::function이란?
기본 사용법
#include <functional>
int add(int a, int b) { return a + b; }
int main()
{
// 함수 포인터 저장
std::function<int(int, int)> func1 = add;
std::cout << func1(1, 2) << std::endl; // 3
// 람다 저장
std::function<int(int, int)> func2 = [](int a, int b) {
return a * b;
};
std::cout << func2(3, 4) << std::endl; // 12
// 함수 객체 저장
std::function<int(int, int)> func3 = std::plus<int>{};
std::cout << func3(5, 6) << std::endl; // 11
return 0;
}
함수를 매개변수로 전달
void execute(const std::function<void(int)>& fn, int value)
{
std::cout << "Executing function with " << value << std::endl;
fn(value);
}
int main()
{
// 람다 전달
execute([](int x) {
std::cout << "Result: " << x * 2 << std::endl;
}, 10);
// 출력:
// Executing function with 10
// Result: 20
return 0;
}
함수 컬렉션
int main()
{
std::vector<std::function<void(int)>> operations;
// 여러 함수를 벡터에 저장
operations.push_back([](int x) {
std::cout << "Double: " << x * 2 << std::endl;
});
operations.push_back([](int x) {
std::cout << "Square: " << x * x << std::endl;
});
operations.push_back([](int x) {
std::cout << "Cube: " << x * x * x << std::endl;
});
// 모든 함수 실행
for (const auto& op : operations)
{
op(5);
}
// 출력:
// Double: 10
// Square: 25
// Cube: 125
return 0;
}
콜백 시스템 구현
class EventSystem
{
public:
using Callback = std::function<void(const std::string&)>;
void registerCallback(Callback cb)
{
callbacks.push_back(cb);
}
void trigger(const std::string& event)
{
std::cout << "Event triggered: " << event << std::endl;
for (const auto& cb : callbacks)
{
cb(event);
}
}
private:
std::vector<Callback> callbacks;
};
int main()
{
EventSystem system;
// 여러 콜백 등록
system.registerCallback([](const std::string& event) {
std::cout << " Logger: " << event << std::endl;
});
system.registerCallback([](const std::string& event) {
std::cout << " Handler: Processing " << event << std::endl;
});
system.trigger("UserLogin");
// 출력:
// Event triggered: UserLogin
// Logger: UserLogin
// Handler: Processing UserLogin
return 0;
}
std::function vs auto
// auto: 컴파일 타임에 타입 결정, 더 빠름
auto lambda1 = [](int x) { return x * 2; };
// std::function: 런타임 다형성, 오버헤드 있음
std::function<int(int)> lambda2 = [](int x) { return x * 2; };
// auto는 타입이 다르면 저장 불가
std::vector<auto> funcs; // ❌ 컴파일 에러!
// std::function은 시그니처가 같으면 저장 가능
std::vector<std::function<int(int)>> funcs; // ✅ OK
Side Effect가 없음
// ❌ Side Effect 있음 (OOP 스타일)
class Counter
{
int count = 0;
public:
void increment() { count++; } // 상태 변경
int getCount() const { return count; }
};
// ✅ Side Effect 없음 (Functional 스타일)
int increment(int count) { return count + 1; } // 새 값 반환, 원본 불변
int main()
{
int count = 0;
count = increment(count); // 명시적 재할당
count = increment(count);
std::cout << count << std::endl; // 2
return 0;
}
순수 함수 (Pure Function)
// ✅ 순수 함수
int add(int a, int b)
{
return a + b; // 항상 같은 결과
}
// ❌ 순수하지 않은 함수
int globalCounter = 0;
int incrementGlobal()
{
return ++globalCounter; // 외부 상태 변경
}
불변성 (Immutability)
// Functional 스타일: 원본을 변경하지 않고 새로운 값 생성
std::vector<int> doubleValues(const std::vector<int>& numbers)
{
std::vector<int> result;
std::transform(numbers.begin(), numbers.end(),
std::back_inserter(result),
[](int n) { return n * 2; }
);
return result; // 새 벡터 반환
}
int main()
{
std::vector<int> original{1, 2, 3, 4, 5};
std::vector<int> doubled = doubleValues(original);
// original은 그대로 {1, 2, 3, 4, 5}
// doubled는 {2, 4, 6, 8, 10}
return 0;
}
OOP가 적합한 경우
// OOP 예제: 게임 캐릭터
class Character
{
int health;
int level;
std::string name;
public:
void takeDamage(int damage) { health -= damage; }
void levelUp() { level++; health = 100; }
bool isAlive() const { return health > 0; }
};
Functional Programming이 적합한 경우
// Functional 예제: 데이터 처리
auto result = numbers
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; })
| std::views::take(5);
두 패러다임 함께 사용
class DataProcessor // OOP: 클래스 구조
{
public:
std::vector<int> process(const std::vector<int>& data)
{
// Functional: 순수 함수 체인
std::vector<int> result;
std::copy_if(data.begin(), data.end(),
std::back_inserter(result),
[](int n) { return n > 0; } // 양수만 필터
);
std::transform(result.begin(), result.end(), result.begin(),
[](int n) { return n * 2; } // 2배
);
return result;
}
};
람다 표현식
캡처 방식
STL 알고리즘
std::for_each, std::transform, std::remove_ifstd::sort, std::accumulate, std::count_ifstd::function
함수형 프로그래밍 장점
패러다임 선택
코드없는 프로그래밍
C++ Lambda Expressions
C++ std::function