[C++] 함수형 프로그래밍 & 람다

chooha·2026년 1월 15일

C++

목록 보기
18/23

📚 C++ 함수형 프로그래밍과 람다

🔸 Functional Programming (함수형 프로그래밍)

함수형 프로그래밍이란?

  • 함수를 일급 객체(First-class citizen)로 취급
  • 함수를 변수에 저장하고, 인자로 전달하고, 반환 가능
  • 불변성(Immutability)과 순수 함수(Pure Function) 강조

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를 "기억"함 (클로저)
  • plus3plus5는 각각 다른 상태를 가진 함수

🔸 C++에서 함수 객체로 구현

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()를 오버로딩하여 객체를 함수처럼 호출 가능
  • 멤버 변수로 상태를 저장 (JavaScript의 클로저와 유사)
  • plus3(10)plus3.operator()(10) 호출

Functor의 장점:

  • 상태를 가질 수 있음
  • 인라인 최적화 가능
  • 타입 안전성

🔸 Lambda Expression (람다 표현식)

람다 문법

[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() 코드 (코드 영역)

람다의 장점:

  • 간결한 문법
  • 즉석에서 함수 정의 가능
  • 코드 가독성 향상

🔸 Capture (캡처)

캡처 방식

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;
}

🔸 Capture by Reference 주의사항

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를 가짐
}

캡처 선택 가이드

  • 작은 객체 (int, double 등): 값 캡처
  • 큰 객체: 참조 캡처 (수명 관리 주의!)
  • 수정이 필요한 경우: 참조 캡처
  • 람다를 반환하는 경우: 반드시 값 캡처
  • 스마트 포인터: move capture 고려

🔸 Lambda This Capture (클래스 내부)

클래스 멤버 접근

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;
}

🔸 Higher-Order Functions (고차 함수)

고차 함수란?

  • 함수를 인자로 받거나 함수를 반환하는 함수
  • C++ STL에서 많이 사용

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 (함수 래퍼)

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 vs Functional Programming

OOP가 적합한 경우

  • 상태를 관리해야 하는 경우 (게임 캐릭터, GUI 컴포넌트)
  • 객체 간의 복잡한 관계 (상속, 다형성)
  • 대규모 시스템 설계
// 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이 적합한 경우

  • 데이터 변환 파이프라인
  • 병렬 처리 (Side Effect 없어서 안전)
  • 수학적 계산
// 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;
    }
};

🔸 핵심 정리

람다 표현식

  • 함수 객체의 간결한 표현
  • 컴파일러가 자동으로 클래스 생성
  • Functor와 동일한 성능

캡처 방식

  • 값 캡처: 작은 객체, 람다를 반환할 때
  • 참조 캡처: 큰 객체, 수정이 필요할 때 (수명 주의!)
  • this 캡처: 멤버 접근 필요할 때
  • [*this]: 객체 복사 (C++17, 안전)

STL 알고리즘

  • std::for_each, std::transform, std::remove_if
  • std::sort, std::accumulate, std::count_if
  • 람다와 함께 사용하여 간결한 코드 작성

std::function

  • 호출 가능한 모든 객체를 저장
  • 함수를 인자로 전달하거나 컬렉션에 저장
  • 오버헤드 있음 (auto가 더 빠름)

함수형 프로그래밍 장점

  • Side Effect 없음
  • 순수 함수로 예측 가능
  • 병렬 처리에 유리
  • 테스트하기 쉬움

패러다임 선택

  • OOP: 상태 관리, 복잡한 시스템
  • Functional: 데이터 변환, 병렬 처리
  • 실전에서는 두 패러다임을 적절히 혼합

<참고 자료>

코드없는 프로그래밍
C++ Lambda Expressions
C++ std::function

0개의 댓글