std::function, std::variant

박정훈·2025년 3월 11일

cpp

목록 보기
4/5

1. 전통적인 방법 (추상 클래스와 가상 함수 사용)

보통은 “추상 클래스”를 만들어서, 여러 클래스가 같은 함수를 다르게 구현하도록 합니다. 예를 들어, 오리가 날 때 어떻게 날지 결정하는 기능을 만들 때,FlyBehavior라는 추상 클래스를 만들고, 이를 상속받은 FlyWithWings(날개로 날기)와 FlyNoWay(날지 않기) 클래스를 정의합니다.

#include <iostream>

// 추상 클래스: 공통 인터페이스 정의
class FlyBehavior {
public:
    virtual void fly() = 0;         // 순수 가상 함수 (반드시 구현해야 함)
    virtual ~FlyBehavior() {}        // 가상 소멸자
};

// 날개로 나는 행동 구현
class FlyWithWings : public FlyBehavior {
public:
    void fly() override {
        std::cout << "날개로 날아갑니다.\n";
    }
};

// 날지 않는 행동 구현
class FlyNoWay : public FlyBehavior {
public:
    void fly() override {
        std::cout << "날지 못합니다.\n";
    }
};

// Duck 클래스는 FlyBehavior를 사용해 다형성을 구현
class Duck {
private:
    FlyBehavior* flyBehavior;  // 행동을 나타내는 포인터
public:
    Duck(FlyBehavior* fb) : flyBehavior(fb) {}

    void performFly() {
        flyBehavior->fly();    // 현재 설정된 행동을 실행
    }

    void setFlyBehavior(FlyBehavior* fb) {
        flyBehavior = fb;
    }
};

int main() {
    // 처음에는 날개로 나는 행동으로 오리 생성
    Duck duck(new FlyWithWings());
    duck.performFly();  // "날개로 날아갑니다." 출력

    // 나중에 동적으로 날지 않는 행동으로 변경
    duck.setFlyBehavior(new FlyNoWay());
    duck.performFly();  // "날지 못합니다." 출력

    return 0;
}

2. 모던 C++ 방식 (std::function, 람다, std::variant 사용)

2.1 std::function과 람다 표현식 사용

모던 C++에서는 클래스를 따로 만들지 않고, std::function이라는 "함수를 담을 수 있는 상자"와 람다 표현식(이름 없는 간단한 함수)를 사용해서, 실행 시에 간단하게 행동을 변경할 수 있습니다.

#include <iostream>
#include <functional>

class Duck {
public:
    // std::function을 사용하면, 함수 형태(람다 함수 등)를 저장할 수 있습니다.
    std::function<void()> flyBehavior;

    Duck(std::function<void()> fb) : flyBehavior(fb) {}

    void performFly() const {
        flyBehavior();  // 저장된 함수를 실행
    }
};

int main() {
    // 람다 함수를 이용해 "날개로 날기" 행동 정의
    Duck duck([](){ std::cout << "날개로 날아갑니다.\n"; });
    duck.performFly();  // "날개로 날아갑니다." 출력

    // 실행 도중 행동을 "날지 못함"으로 변경
    duck.flyBehavior = [](){ std::cout << "날지 못합니다.\n"; };
    duck.performFly();  // "날지 못합니다." 출력

    return 0;
}

2.2 std::variant를 사용한 다형성

std::variant는 여러 타입 중 하나를 안전하게 저장할 수 있는 도구입니다. 오리의 행동이 미리 정해진 몇 가지 타입(예: FlyWithWings, FlyNoWay)이라면, 이들을 하나의 std::variant에 담고, std::visit를 사용해 실행할 수 있습니다.

#include <iostream>
#include <variant>

// 행동을 나타내는 두 가지 타입의 구조체 정의
struct FlyWithWings {
    void operator()() const {
        std::cout << "날개로 날아갑니다.\n";
    }
};

struct FlyNoWay {
    void operator()() const {
        std::cout << "날지 못합니다.\n";
    }
};

// std::variant를 사용하여, 위 두 행동 중 하나를 저장할 수 있음
using FlyBehavior = std::variant<FlyWithWings, FlyNoWay>;

// 현재 저장된 행동을 실행하는 함수
void performFly(const FlyBehavior& fb) {
    std::visit([](auto&& behavior) { behavior(); }, fb);
}

int main() {
    FlyBehavior behavior = FlyWithWings{};
    performFly(behavior);  // "날개로 날아갑니다." 출력

    behavior = FlyNoWay{};
    performFly(behavior);  // "날지 못합니다." 출력

    return 0;
}

이해 못 했던 것들

Duck duck(new FlyWithWings()); 와 Duck* duck = new FlyWithWings();의 차이

Duck duck1(new FlyWithWings());
Duck* duck2 = new FlyWithWings();의 차이

duck1.function();
duck2->function(); // delete duck2로 메모리 해제 필요

람다함수

[]() { std::cout << "안녕하세요!\n"; }

[]:
캡처 리스트(capture list) 라고 하며, 람다 함수가 주변 변수들을 사용할 수 있도록 "잡아올" 변수들을 지정합니다.
예를 들어, [x, y]라고 쓰면, 람다 내부에서 변수 x와 y를 사용할 수 있습니다.
빈 대괄호 []는 아무 변수도 캡처하지 않음을 의미합니다.

():
함수의 매개변수(parameter) 목록을 나타냅니다.
빈 소괄호는 이 함수가 아무 매개변수도 받지 않는다는 뜻입니다.

[]()는 "캡처 없이, 매개변수 없이"라는 의미의 람다 함수를 시작하는 구문입니다.

#include <iostream>

int main() {
    int value = 10;

    // 값 캡처: 변수 value의 값을 복사해서 사용
    auto lambdaByValue = [value]() {
        std::cout << "값 캡처: " << value << "\n";
    };

    // 참조 캡처: 변수 value를 참조하여 사용 (원본이 바뀌면 값도 바뀜)
    auto lambdaByReference = [&value]() {
        std::cout << "참조 캡처: " << value << "\n";
    };

    lambdaByValue();       // 출력: 값 캡처: 10
    lambdaByReference();   // 출력: 참조 캡처: 10

    value = 20;
    lambdaByValue();       // 여전히 10이 출력됨 (복사본이기 때문)
    lambdaByReference();   // 20이 출력됨 (원본 참조)

    return 0;
}

"value가 () 안에 들어가도 되는 거 아니야? auto lambdaByReference = [](&value){ std::cout << value << endl; };이런식으로 말이야."
[](&value)는 문법 오류입니다.
올바른 캡처 방법은 [&value]입니다.
만약 아무런 캡처도 하지 않으려면 빈 대괄호 []를 사용합니다.

operator()()에서 ()가 두 개 붙은 이유

operator()의 의미:
클래스 안에서 operator()를 정의하면, 해당 클래스의 객체를 마치 함수처럼 호출(call) 할 수 있습니다. 이것을 함수 호출 연산자 오버로딩이라고 합니다. 이름은 반드시 operator라고 지어야 합니다.

첫 번째 ()는 함수 호출 연산자의 이름 일부입니다.
두 번째 ()는, 그 오퍼레이터를 실제로 호출하는 괄호입니다.

struct Functor {
    void operator()() const { std::cout << "호출됨!\n"; }
};

Functor f;
f();  // 여기서 f()는 f.operator()()를 호출하는 것입니다.
#include <iostream>

class Adder {
public:
    int operator()(int a, int b) const {
        return a + b;
    }
};

int main() {
    Adder add;
    int result = add(5, 7);  // add.operator()(5, 7) 호출
    std::cout << "5 + 7 = " << result << "\n";  // 출력: 5 + 7 = 12
    return 0;
}
"f() 에서 f()라는 함수를 호출하는 건지 f의 생성자를 호출하는 건지 operator()()를 호출하는 건지 어떻게 구분해?"

f가 함수(Function)라면:
단순히 f()는 함수를 호출하는 것입니다.
f가 객체(Object)이고, 그 객체가 operator()를 오버로딩한 함수 객체(Functor)라면:
이 경우 f()는 f 객체의 operator()() 를 호출합니다.
f가 타입(Type)일 경우 (생성자 호출):
예를 들어, 클래스 이름 f 뒤에 ()를 붙이면 생성자를 호출하여 객체를 생성하는 것입니다.

타입(함수 vs. 객체 vs. 타입 이름)을 확인, 문맥(Context)을 확인
변수 선언이나 객체 생성문 안에 쓰였다면 생성자 호출일 가능성이 높습니다.
실행 코드(이미 생성된 객체를 가지고 f()를 호출하는 경우)라면 operator() 호출입니다.

using

using은 C++11부터 도입된 타입 별칭(type alias) 을 만드는 키워드입니다.
전통적인 방식:

typedef std::variant<FlyWithWings, FlyNoWay> FlyBehavior;

모던 방식:

using FlyBehavior = std::variant<FlyWithWings, FlyNoWay>;

std::visit([](auto&& behavior) { behavior(); }, fb); 자세한 설명

std::visit의 역할:
std::visit는 std::variant에 저장된 값(여러 타입 중 하나)을 꺼내어,
그 값에 맞는 처리를 하도록 함수를 호출하는 역할을 합니다.

람다 함수 부분 [](auto&& behavior) { behavior(); }:
[](auto&& behavior):
auto&& behavior는 어떤 타입이든 받아들이겠다는 뜻입니다.
즉, fb에 저장된 값이 어떤 타입이든 상관없이, 그 값을 behavior라는 이름으로 받아옵니다.

{ behavior(); }:
받아온 behavior를 함수처럼 호출합니다.
만약 fb에 FlyWithWings 객체가 저장되어 있다면, 그 객체의 operator()가 호출됩니다.

전체적으로:
std::visit는 fb 안에 있는 현재 값을 람다 함수의 매개변수로 전달하고,
람다 함수는 전달받은 객체를 함수처럼 호출해서, 그 객체에 정의된 동작(예: "날개로 날기" 또는 "날지 않기")을 실행하게 합니다.

0개의 댓글