future & promise 사용법


  • promise 객체를 통해 자신과 연결된 future 객체를 생성할 수 있습니다. promise 객체에 값이 셋팅되었을 때 future를 통해서 해당 값을 가져올 수 있습니다.




future & promise 사용법 예시


#include <iostream>
#include <future>

using namespace std;

void WorkerThread(promise<string>* p, const string *name)
{
    try
    {
        // name이 nullptr일 경우 throw
        if (name == nullptr)
        {
            throw runtime_error("name is nullptr");
        }

        // promise에 값을 셋팅
        p->set_value(*name + ", Hello\n");
    }
    catch (const std::exception& exp)
    {
        // promise에 exception_ptr을 셋팅
        p->set_exception(make_exception_ptr(exp));
    }

    return;
}

int main()
{
    promise<string> p;

    // 자신과 연결된 promise 객체를 생성
    future<string> f = p.get_future();

    string str = "devhun";

    thread t(WorkerThread, &p, nullptr);

    f.wait();

    try
    {
        // promise에 셋팅된 값을 가져옴
        std::cout << f.get();
    }
    catch (const std::exception& exp)
    {
        // promise에 셋팅된 exception 값을 가져옴
        std::cout << exp.what();
    }

    t.join();

    return 0;
}

  • future와 promise를 통해서 다른 쓰레드에서의 결과값을 가져올 수 있습니다. 그리고 future의 get은 단 한번만 사용할 수 있습니다.

  • future를 통해서 exception 값을 셋팅하고 promise를 통해서 exception 값을 가져올 수 있습니다. 이때 exception을 promise에 셋팅할 때 반드시 exception_ptr로 셋팅해야 합니다. 그리고 promise에 exception이 셋팅되면 future의 get에서 이를 catch할 수 있습니다.

  • future의 get만을 사용해서도 promise에 값이 셋팅될 때까지 기다리지만, future의 get보다 wait이 가볍기 때문에 wait을 이용해서 대기하고 get을 통해 값을 가져오는 것이 좋습니다.




shared_future


#include <iostream>
#include <future>

using namespace std;

void WorkerThread(promise<string>* p, const string *name)
{
    try
    {
        if (name == nullptr)
        {
            throw runtime_error("name is nullptr");
        }

        p->set_value(*name + ", Hello\n");
    }
    catch (const std::exception& exp)
    {
        p->set_exception(make_exception_ptr(exp));
    }

    return;
}

int main()
{
    promise<string> p;

    shared_future<string> f = p.get_future();

    string str = "devhun";

    thread t(WorkerThread, &p, &str);

    f.wait();

    try
    {
        for (int i = 0; i < 10; ++i)
        {
            std::cout << f.get();
        }
    }
    catch (const std::exception& exp)
    {
        std::cout << exp.what() << "\n";
    }

    t.join();

    return 0;
}

  • future는 get 함수를 한 번만 호출할 수 있으며, 이후 호출부터는 no state 예외가 발생됩니다. 하지만, shared_future를 사용할 경우 get을 중복하여 호출할 수 있습니다.




packaged_task


#include <iostream>
#include <future>

using namespace std;

string WorkerThread(const string *name, const string* hello)
{
    this_thread::sleep_for(chrono::seconds(2));

    return *name + *hello;
}

int main()
{
    // return 타입과 매개변수 타입을 지정
    packaged_task<string(const string*, const string*)> task(WorkerThread);

    future<string> f = task.get_future();

    string str1 = "devhun";
    string str2 = ", Hello";

    // 복사 생성이 불가능하기 때문에 move를 통해 인자로 전달해야 합니다.
    thread t(move(task), &str1, &str2);

    cout << f.get() << endl;

    t.join();

    return 0;
}

  • packaged_task를 생성할 때 비동기로 실행할 함수를 매개 변수로 전달하고 함수의 리턴 타입과 매개 변수 타입에 대한 템플릿 타입을 지정합니다. 그리고 생성된 packaged_task를 이용해서 future 객체를 생성하여 이를 통해 비동기 실행에 대한 결과를 받아볼 수 있습니다.

  • packaged_task는 promise의 set_value를 사용하지 않고 함수의 return 타입을 대신하여 처리할 수 있습니다.

  • packaged_task는 promise의 set_exception을 사용하지 않고도 task에 대한 예외가 설정됩니다.




async


#include <iostream>
#include <future>

using namespace std;

string WorkerThread(const string *name, const string* hello)
{
    this_thread::sleep_for(chrono::seconds(2));

    return *name + *hello;
}

int main()
{
    string str1 = "devhun";
    string str2 = ", Hello";

    future<string> f = async(launch::async, WorkerThread, &str1, &str2);

    cout << f.get() << endl;

    return 0;
}

  • promise 또는 packaged_task는 비동기적으로 실행하기 위해서 thread 객체를 직접 생성하여 처리해야 했습니다. 하지만, asyn를 사용할 경우 내부적으로 쓰레드를 생성하여 매개변수로 전달한 함수를 비동기적으로 처리합니다.




lanch::async


async(launch::async, WorkerThread, &str1, &str2);

  • launch::async를 전달할 경우 내부적으로 새로운 쓰레드를 생성하여 함수를 비동기적으로 처리합니다.




lanch::deferred


async(launch::deferred, WorkerThread, &str1, &str2);

  • launch::deferred를 사용할 경우 별도의 쓰레드를 생성하지 않고 future 객체를 통해 get을 호출하였을 때 해당 쓰레드에서 함수를 호출하는 방식입니다.




'Programming Language > C, C++' 카테고리의 다른 글

C++ condition_variable 사용 방법  (0) 2023.04.15
C++ unique_lock 사용방법  (0) 2023.04.15
C++ mutex와 lock_guard  (0) 2023.04.15
C++ 다중 상속(Multiple inheritance)이란?  (0) 2023.03.26
C++ 람다(lambda)란?  (0) 2023.03.25

condition_variable

 

  • condition_variable은 C++11에 추가된 클래스로써, condition_variable을 통해 생성한 객체를 이용하면 조건에 맞지 않으면 대기하고, 다른 쓰레드에서 조건이 맞게 수정하였다면 이를 알려 대기중인 쓰레드를 깨울 수 있습니다.


  • condition_variable은 Event 객체와 같이 Windows에 종속적인 커널 오브젝트를 직접적으로 사용하지 않고 여러 OS에서 공통적으로 사용할 수 있는 클래스입니다.

 

  • condition_variable은 unique_lock을 같이 병행하여 사용하여 조건에 해당하는 값을 읽고 수정할 수 있습니다.




condition_variable 사용방법

 

#include <iostream>
#include <mutex>
#include <thread>
#include <queue>

std::mutex m;
std::condition_variable cv;
std::queue<int> q;

void EnqueueThread()
{
    for(;;)
    {
        {
            std::unique_lock<std::mutex> lk(m);
            q.push(rand() % 10);
        }

        // 동일한 condition_variable 객체를 대상으로 대기중인 쓰레드를 깨워 조건을 확인시킴
        cv.notify_one();
    }

    return;
}

void DequeueThread()
{
    for (;;)
    {
        std::unique_lock<std::mutex> lk(m);

        // lock을 건 상태에서 조건을 확인
        // 조건에 맞지 않는다면 lock을 해제 후 쓰레드 block
        // 조건에 맞을 경우 lock을 유지한 상태에서 쓰레드 block을 하지 않고 다음 로직 수행
        cv.wait(lk, []()->bool {return !q.empty(); });

        std::cout << q.front() << "\n";
        q.pop();
    }

    return;
}

int main()
{
    std::thread th1(EnqueueThread);
    std::thread th2(DequeueThread);

    th1.join();
    th2.join();

    return 0;
}

 

  • wait에 unique_lock과 람다를 전달해서 조건에 조건에 맞을 경우 unique_lock에 대한 소유권을 유지한체 다음 로직을 수행하고, 조건에 맞지 않을 경우 unique_lock에 대한 소유권을 해제하고 쓰레드 블락됩니다.

 

  • notify_one은 동일한 condition_variable 객체를 대상으로 대기중인 쓰레드를 하나를 깨워 condition을 다시 한 번 확인하도록 요청합니다. condition이 맞을 경우 이후 다음 로직을 수행하고, condition이 맞지 않을 경우 다음 로직을 수행하지 않고 다시 block됩니다.




'Programming Language > C, C++' 카테고리의 다른 글

future & promise, packaged_task, async 사용법  (0) 2023.04.18
C++ unique_lock 사용방법  (0) 2023.04.15
C++ mutex와 lock_guard  (0) 2023.04.15
C++ 다중 상속(Multiple inheritance)이란?  (0) 2023.03.26
C++ 람다(lambda)란?  (0) 2023.03.25

unique_lock 이란?

 

  • unique_lock 객체는 lock_guard와 같이 RAII 기법을 통해 생성자에서 락에 대한 소유권을 획득하고 소멸자에서 락에 대한 소유권을 반환하는 객체입니다.

 

  • unique_lock을 생성할 때 derfer_lock, try_to_lock, adopt_lock 중 하나를 추가로 전달하여 lock_guard보다 다양한 기능을 사용할 수 있습니다.

 

  • lock_guard가 상대적으로 unique_lock 보다 더 가볍기 때문에 lock_guard를 우선적으로 사용하고 필요시에 unique_lock을 사용하는 것이 좋습니다.




defer_lcok

 

#include <iostream>
#include <mutex>
#include <thread>

int num;
std::mutex m;

void WorkerThread() {
    for (int i = 0; i < 100000; ++i)
    {
        std::unique_lock<std::mutex> lk(m, std::defer_lock);
        lk.lock();

        ++num;
    }

    return;
}


int main()
{
    std::thread th1(WorkerThread);
    std::thread th2(WorkerThread);

    th1.join();
    th2.join();

    std::cout << num;

    return 0;
}

 

  • defer_lock을 전달할 경우 소유권 획득 시점을 생성자가 아닌 lock 멤버 함수를 호출하는 시점으로 연기할 수 있습니다. 그리고 lock이 호출되었다면, 소멸자에서 이를 반환합니다.




try_to_lock

#include <iostream>
#include <mutex>
#include <thread>

int num;
std::mutex m;

void WorkerThread() {
    for (int i = 0; i < 100000; ++i)
    {
        std::unique_lock<std::mutex> lk(m, std::try_to_lock);
        if (!lk.owns_lock()) {
            --i;
            continue;
        }

        ++num;
    }

    return;
}


int main()
{
    std::thread th1(WorkerThread);
    std::thread th2(WorkerThread);

    th1.join();
    th2.join();

    std::cout << num;

    return 0;
}

 

  • try_to_lock은 생성자에서 lock에 대한 소유권을 획득할 수 있는지 여부를 시도를 합니다. 만약, 생성자에서 lock에 대한 소유권을 획득하였다면, owns_lock() 멤버 함수의 return 값이 true이고 획득하지 못했다면 false를 return 합니다.




adopt_lock

 

#include <iostream>
#include <mutex>
#include <thread>

int num;
std::mutex m;

void WorkerThread() {
    for (int i = 0; i < 100000; ++i)
    {
        m.lock();
        std::unique_lock<std::mutex> lk(m, std::adopt_lock);
        ++num;
    }

    return;
}


int main()
{
    std::thread th1(WorkerThread);
    std::thread th2(WorkerThread);

    th1.join();
    th2.join();

    std::cout << num;

    return 0;
}

 

  • adopt_lock는 이미 매개변수로 전달한 동기화 객체에 대한 소유권이 이미 획득되어 있는 상태이고, 이를 소멸자에서 반환하기 위해서 사용합니다.




mutex

 

  • mutex는 C++11에 추가된 class로써 운영체제에 종속되지 않고 공통적으로 사용할 수 있는 유저 모드 동기화 객체입니다.

 

  • 배타적인 접근만 가능하며, 중복 락이 불가능합니다.




lock과 unlock

 

#include <iostream>
#include <thread>
#include <mutex>

std::mutex m;

void WorkerThread()
{
    for (int i = 0; i < 100000; ++i)
    {
        m.lock();
        ++num;
        m.unlock();
    }

    return;
}

int main(){
    std::thread th1(WorkerThread);
    std::thread th2(WorkerThread);

    th1.join();
    th2.join();

    std::cout << num;

    return 0;
}

 

  • lock()을 통해서 배타적인 접근이 가능하고, unlock()을 통해서 배타적인 접근에 대한 소유권 해제가 가능합니다.

 

  • try_lock()은 현재 배타적인 접근이 가능한지를 확인하고 소유권을 획득하는 멤버 함수이며, return 타입은 bool로써 소유권 획득 성공 여부를 뜻합니다.




lock_guard

 

#include <iostream>
#include <thread>
#include <mutex>

int num;
std::mutex m;

void WorkerThread()
{
    for (int i = 0; i < 100000; ++i)
    {
        std::lock_guard<std::mutex> lock(m);
        ++num;
    }

    return;
}


int main()
{
    std::thread th1(WorkerThread);
    std::thread th2(WorkerThread);

    th1.join();
    th2.join();

    std::cout << num;

    return 0;
}

 

  • lock_guard 객체는 RAII 디자인 패턴을 이용해서 생성자에서 락을 획득하고 소멸자에서 락을 반환하는 객체입니다.


  • lock_guard는 복사가 불가능하기 때문에 Call-by-value 방식으로 매개 변수 또는 리턴으로 값을 전달할 수가 없습니다.




C++ 다중 상속(Multiple inheritance)이란?

 

  • C++은 다중 상속을 지원하여 두 가지 이상의 클래스를 상속 받을 수 있습니다. 하지만, 다중 상속은 득보다 실이 더 많은 문법이기 때문에 사용하지 않는 것이 훨씬 더 좋은 방법입니다.




다중 상속의 모호성(Ambiguous)

 

#include <iostream>

using namespace std;

class CBaseOne {
public:
    void SayHello() {
        cout << "Hello World 1\n";
    }
};

class CBaseTwo {
public:
    void SayHello() {
        cout << "Hello World 2\n";
    }
};

class MultipleDerived : public CBaseOne, CBaseTwo{
public:

    void Say() {
    // 스코프 연산자를 통해 모호한 상황을 해결
        CBaseOne::SayHello();
        CBaseTwo::SayHello();
    }
};

int main() {

    MultipleDerived obj;

    obj.Say();

    return 0;
}

 

  • 위와같이 다중 상속을 받았는데, 부모 클래스들이 동일한 멤버 함수를 가지고 있을 경우 컴파일러는 어떤 부모 클래스의 멤버 함수를 호출할지 알 수 없습니다. 그렇기 때문에 스코프 연산자(::)를 통해서 이를 확실히 정해줘야 합니다.




가상 상속(Virtual Inheritance)

 

'윤성우의 열혈 C++' 다중 상속 이미지

 

#include <iostream>

using namespace std;

class CBase {
public:

    CBase() {
        cout << "CBase Constructor\n";
    }

  void SayHello(){
    cout << "Hello World\n";
  }

};

// 가상 상속
class CDerivedOne : virtual public CBase {
public:
    CDerivedOne()
    :CBase()
    {
        cout << "CDerivedOne Constructor\n";
    }
};

// 가상 상속
class CDerivedTwo : virtual public CBase {
public:
    CDerivedTwo()
        :CBase()
    {
        cout << "CDerivedTwo Constructor\n";
    }
};

class CLastDerived : public CDerivedOne, public CDerivedTwo {
public:
    CLastDerived()
        :CDerivedOne(),
        CDerivedTwo()
    {
        cout << "CLastDerived Constructor\n";
    SayHello();
    }
};


int main() {

    CLastDerived obj;

    return 0;
}

 

  • 위 코드에서 보는것과 같이 CDerivedOne, CDerivedTwo는 가상 상속을 받고 CLastDerived는 CDerivedOne, CDerivedTwo에 대해서 다중 상속을 받고있습니다.

    만약, CDerivedOne, CDerivedTwo가 가상 상속을 받지 않았다면 모호성으로 인해서 CLastDerived 생성자 내부에서 SayHello() 멤버 함수를 호출할 수 없게 되며 CBase에 대한 생성자도 두 번 호출되게 됩니다.

    이를 방지하려면 다중 상속을 하는 부모 객체는 virtual 키워드를 통해 가상 상속을 받아야 합니다.




C++ 람다(lambda)란?

 

  • C++ 11에 추가된 함수 객체 표현식으로서, 주로 다른 함수의 인자로 전달되거나 함수 내부에서 익명 함수로 사용됩니다.


  • 람다는 C++의 클로저(Closure)로서 캡쳐를 통해 람다 함수 내부에서 외부 변수에 대한 접근이 가능합니다.




람다 함수 객체를 생성하는 방법

 

 

  • 람다 왼쪽부터 오른쪽으로 순서대로 개시자(introducer), 인자(parameters), 반환 타임(return type), 함수의 몸통(statement)으로 구성되어 있습니다.

 

  • 람다 내부에서 외부 변수에 접근할 때 접근할 변수를 인자로 전달할 수도 있지만, Captrue 기능을 이용해서 접근할 수 있습니다. 이는 람다를 핸들러로서 사용하기 위해 정해진 인터페이스 형태로 정의해야만 할 때 매우 유용하게 사용할 수 있습니다.




[&] Capture

 

  • [&] Captrue는 외부의 모든 변수를 Call-by-reference 형태로 가져올 때 사용합니다.




[=] Capture

 

  • [=] Capture는 외부의 모든 변수를 Call-by-value 형태로 가져올 때 사용합니다. 그리고 Call-by-value 형태로 Capture할 경우 자동적으로 const 속성이 붙습니다.

 

  • 값 형태로 가져온 Capture 값을 수정하기 위해서는 [=]() mutable -> {} 형태로 사용해야 합니다.




[&, x, y]

 

  • [&, x, y] Capture는 외부의 x, y는 값 형태로 가져오고 나머지 모든 변수를 레퍼런스 형태로 가져오는 방식입니다. 이와같이 값과 래퍼런스를 혼합해서 Capture 할 수 있습니다.




[=] or [&] 그리고 this

 

  • 멤버 함수 내부에서 람다를 생성하고 [=] or [&] 형태로 캡쳐할 경우 자동으로 this가 캡쳐 됩니다.




람다와 외부 객체 참조

 

#include <iostream>

using namespace std;

class CTest {
public:
    CTest() {
        cout << "CTest\n";
    }

    ~CTest() {
        cout << "~CTest\n";
    }

    CTest(const CTest& obj) {
        cout << "Copy Creator\n";
    }

    void SayHello() const {
        cout << "Hello World\n";
    }
};

auto GetLambda() {
    CTest test;

  // [=]으로 캡쳐해야 함
    return [&]() -> void {
        test.SayHello();
    };
}

int main() {

    auto g = GetLambda();

    g();

    return 0;
}

 

  • GetLambda 함수를 호출하면 내부에서 생성된 람다 객체가 return 되면서 test 객체의 소멸자가 호출됩니다. 그렇기 때문에 람다 객체가 외부 변수에 접근할 때 해당 변수가 스택에서 이미 제거 될 수 있다는 점을 생각하고 사용해야 합니다. 즉, GetLambda 안에서 생성된 람다 객체는 GetLambda 함수 스택보다 오랫동안 유지되기 때문에 test를 Call-by-value 형태로 캡쳐해야 합니다.




클래스를 활용한 상속


class Animal {
  constructor(age, weight) {
    this.age = age;
    this.weight = weight;
  }
  eat() {
    return "eat";
  }
  move() {
    return "move";
  }
}

class Bird extends Animal {
  constructor(age, weight) {
    super(age, weight);
  }

  fly() {
    return "fly";
  }
}




프로토타입을 기반으로 한 상속


const Animal = (function () {
  function Animal(age, weight) {
    this.age = age;
    this.weight = weight;
  }

  return Animal;
})();

const Bird = (function () {
  function Bird(age, weight) {
    Animal.apply(this, [age, weight]);
  }

  Bird.prototype = Object.create(Animal.prototype);
  Bird.prototype.constructor = Bird;

  return Bird;
})();




클래스와 생성자 함수의 차이점


  • 클래스는 new 없이 호출할 경우 error가 발생합니다.

  • 클래스 내의 모든 코드에는 암묵적으로 strict mode가 지정되어 실행되며 strict mode를 해제할 수 없습니다.

  • 클래스 내의 메서드 및 정적 메서드는 모두 [[Enumable]]이 false 입니다.

  • 클래스를 통해 상속을 구현한다면, 부모 클래스와 자식 클래스의 인스턴스 프로토타입 체인뿐만 아니라 클래스간의 프로토타입 체인도 생성합니다.




const

 

  • const는 컴파일 또는 런타임에 결정되는 상수를 대상으로 사용되는 키워드입니다.




constexpr(constant expression)

 

  • constexpr은 컴파일 타임에 결정되는 상수를 대상으로 사용되는 키워드입니다.

 

  • constexpr은 const와 다르게 어떠한 상수식에서도 사용될 수 있습니다.




const와 constexpr 변수 초기화

int main() {
    int num1 = 0;

  const int num2 = num1 + 1;

  // 컴파일 에러
    constexpr int num3 = num1 + 1;

    return 0;
}

 

  • const는 어떻게 사용하느냐에 따라서 런타임에 초기화될 수 있습니다. 그렇기 때문에 확실하게 컴파일 타임에 초기화되어 보다 나은 런타임 성능을 얻기 위해서는 constexpr을 사용하는 것이 좋습니다.




constexpr 함수

 

#include <iostream>

using namespace std;

constexpr int Factorial(int n) {
    int total = 1;
    for (int i = 1; i <= n; i++) {
        total *= i;
    }

    return total;
}

template <int N>
struct A {
    int operator()() { return N; }
};

int main() {
    A<Factorial(10)> a;

    std::cout << a() << std::endl;
}

 

  • 함수 return 타입에 constexpr을 사용하여 컴파일 타임 상수를 return 하도록 정의할 수 있습니다. 이 처럼 컴파일 타임 상수를 반환하는 함수를 정의하여 상수가 필요한 템플릿 인자라던가 배열의 길이를 정의할 때 사용할 수 있습니다.

 

  • constexpr 함수를 호출할 때 반드시 컴파일 타임 상수를 인자로 전달해야 컴파일 타임 상수를 return 합니다. 만약, 컴파일 타임 상수가 아닌 변수를 인자로 전달하여 호출한다면 정상적으로 호출은 되지만, 컴파일 타임 상수를 return하진 않습니다.




constexpr 함수의 조건

 

  • goto문 사용 불가

 

  • try/catch 사용 불가(C++ 20 부터는 사용 가능하다고 합니다.)

 

  • 리터럴 타입이 아닌 변수 정의 불가

 

  • 초기화되지 않은 변수 정의 불가

 

  • 로직 중간에 constexpr이 아닌 함수를 호출 불가




inline 함수와 constexpr 함수

 

  • 함수 inline은 컴파일러의 판단에 의해서 함수 호출부가 함수의 로직으로 치환되는 것을 말합니다.

 

  • constexpr 함수는 컴파일러의 판단에 의해서 컴파일 타임에 로직을 수행하여 결괏값으로 되는 것을 말합니다.




최적화 컴파일

 

  • 최적화 컴파일을 껐을 경우 inline 함수는 절대로 inline되지 않지만, constexpr은 최적화 컴파일과는 별개로 컴파일러의 판단에 의해서 컴파일 상수를 return 합니다.




constexpr 생성자

 

#include <iostream>

using namespace std;

class Vector {
public:
    constexpr Vector(int x, int y) : x_(x), y_(y) {}

    constexpr int x() const { return x_; }
    constexpr int y() const { return y_; }

private:
    int x_;
    int y_;
};

constexpr Vector AddVec(const Vector& v1, const Vector& v2) {
    return { v1.x() + v2.x(), v1.y() + v2.y() };
}

template <int N>
struct A {
    int operator()() { return N; }
};

int main() {
    constexpr Vector v1{ 1, 2 };
    constexpr Vector v2{ 2, 3 };

    // constexpr 객체의 constexpr 멤버 함수는 역시 constexpr!
    A<v1.x()> a;
    std::cout << a() << std::endl;

    // AddVec 역시 constexpr 을 리턴한다.
    A<AddVec(v1, v2).x()> b;
    std::cout << b() << std::endl;
}

 

  • constexpr 생성자의 경우 constexpr 함수에 적용되는 제약 조건들이 모두 적용됩니다. 또한 constexpr 생성자의 인자들은 반드시 리터럴 타입이여야만 하고, 해당 클래스는 다른 클래스를 상속 받을 수 없습니다.

 

  • constexpr 생성자를 가지고있어야만 constexpr 객체를 생성할 수 있습니다. 그리고 constexpr 객체의 constexpr 멤버 함수만이 컴파일 타임 상수를 return할 수 있습니다.




스마트 포인터( Smart Pointer )란?

 

  • C++은 Unmanaged Language로서 메모리 할당과 해제를 프로그래머가 직접 관리해주어야 합니다. 하지만, 협업을 하다보면 프로그래머의 실수로 메모리의 누수가 발생될 수 있습니다. 그래서 C++은 이러한 누수를 예방하고자 스마트 포인터를 사용할 수 있습니다.




스마트 포인터 동작원리

 

  • 스마트 포인터는 RAII( Resource Acquisition Is Initialization ) 디자인 패턴을 기반으로 설계되어 있습니다. 초기화된 스마트 포인터 객체가 스코프를 벗어나면 소멸자가 호출되는데 이 때 소멸자 내부에서는 해당 자원에 대해서 반환할지 여부를 결정하고 delete를 호출합니다.




스마트 포인터의 종류와 동작 방식

 

unique_ptr

 

int main()
{
    std::unique_ptr<CChild> p1 = std::make_unique<CChild>();

    std::unique_ptr<CChild> p2(new CChild);

    // 소유권 이전
    auto p3 = std::move(p2);

    // 대입 연산자를 통한 소유권 이전은 불가능합니다.
    //auto p4 = p3;

        return 1;
}

 

  • unique_ptr은 동적 할당 받은 메모리에 대한 참조를 하나의 스마트 포인터에서만 관리하기 위해 사용하는 스마트 포인터입니다. 그렇기 때문에 소유권을 복사할 수 없으며 오로지 std::move를 통해서만 소유권 이전이 가능합니다.




shared_ptr

 

int main()
{
    std::shared_ptr<CChild> p1(new CChild);

    std::shared_ptr<CChild> p2 = std::make_shared<CChild>();

    // 복사 생성자
    std::shared_ptr<CChild> p3(p2);

    std::shared_ptr<CChild> p4;

    // 대입 연산자
    p4 = p3;

    std::cout << p4.use_count();

    return 1;
}

 

  • shared_ptr은 동적 할당 받은 메모리에 대한 참조를 여러 스마트 포인터에서 관리하기 위해 사용하는 스마트 포인터입니다. 내부적으로 여러 스마트 포인터에서 관리하기 위해 해당 메모리에 대한 참조를 몇개의 스마트 포인터가 참조하고 있는지 관리합니다. shared_ptr이 스코프를 벗어나서 소멸자가 호출될 때 마다 참조하는 메모리에 대한 참조 카운트를 감소시키며 참조 카운트를 0으로 만든 shared_ptr이 delete를 호출하여 메모리를 반환합니다.




weak_ptr

 

// 순환 참조 발생 코드
#include <iostream>
#include <Windows.h>

class CTest {
public:

    CTest() {
        std::cout << "생성자\n";
    }

    ~CTest() {
        std::cout << "소멸자\n";
    }

    void ShowMe() {
        std::cout << "Hello\n";
    }

    std::shared_ptr<CTest> p;
};

int main()
{
    auto s1 = std::make_shared<CTest>();
    auto s2 = std::make_shared<CTest>();

    s1->p = s2;
    s2->p = s1;

    return 1;
}




// weak_ptr을 사용하여 순환 참조 방지
#include <iostream>
#include <Windows.h>

class CTest {
public:

    CTest() {
        std::cout << "생성자\n";
    }

    ~CTest() {
        std::cout << "소멸자\n";
    }

    void ShowMe() {
        std::cout << "Hello\n";
    }

    std::weak_ptr<CTest> p;
};

int main()
{
    auto s1 = std::make_shared<CTest>();
    auto s2 = std::make_shared<CTest>();

    s1->p = s2;
    s2->p = s1;

    auto s = s1->p.lock();
    s->ShowMe();

    return 1;
}

 

  • weak_ptr은 shared_ptr리 소유하는 메모리에 대한 접근 방법을 제공하지만, 참조 카운트 계산은 하지 않는 스마트 포인터입니다. 만약, shared_ptr로 생성된 2개의 인스턴스가 서로를 가리키는 shared_ptr을 멤버 변수로 가지고 있다면은 해당 인스턴스들은 절대로 메모리가 반환되지 않습니다. 이러한 현상을 순환 참조( Circular Reference )라고 하며 이러한 현상을 방지하고자 weak_ptr을 사용합니다.

 

  • weak_ptr은 참조하는 메모리에 접근할 때 lock 멤버 함수를 이용해서 shared_ptr 생성하여 접근할 수 있습니다.




auto_ptr을 사용하지 않는 이유

 

#include <iostream>
#include <Windows.h>

class CTest {
public:

    CTest() {
        std::cout << "생성자\n";
    }

    ~CTest() {
        std::cout << "소멸자\n";
    }

    void ShowMe() {
        std::cout << "ASDF";
    }
};

int main()
{
    // 배열 스마트 포인터를 지원하지 않음
    //std::auto_ptr<CTest> p(new CTest[20]);

    std::auto_ptr<CTest> p1(new CTest);
    std::auto_ptr<CTest> p2(p1);

    std::cout << p1.get() << std::endl;
    std::cout << p2.get() << std::endl;

    return 1;
}

 

  • auto_ptr은 사용이 권장되지 않는 스마트 포인터입니다. 그 이유는 두 가지가 있습니다. 첫 번째로는 배열 형식으로 스마트 포인터를 생성할 수 없습니다. 두 번째로는 얕은 복사를 수행하기 때문에 얕은 복사 이후 기존 스마트 포인터가 참조하는 값은 nullptr로 초기화되는 문제가 있기 때문에 사용이 권장되지 않습니다.




'Programming Language > C, C++' 카테고리의 다른 글

C++ 람다(lambda)란?  (0) 2023.03.25
const와 constexpr(constant expression)의 차이점  (0) 2023.02.27
C++ 가상함수와 추상 클래스  (0) 2023.01.27
new와 malloc의 차이점  (0) 2023.01.27
얕은 복사와 깊은 복사  (0) 2023.01.27

가상 함수가 필요한 이유

 

#include <Windows.h>

class CPlayer {
public:
    void ShowMe() {
        std::cout << "CPlayer" << std::endl;
    }
};


class CChild : public CPlayer {
public:
    void ShowMe() {
        std::cout << "CChild" << std::endl;
    }
};

int main()
{
    CPlayer* p = new CChild();

    p->ShowMe();

    return 1;
}

 

  • C++은 데이터 타입을 기준으로 어떤 클래스의 함수를 호출할지를 결정 짓습니다. 그렇기 때문에 위와 같은 코드를 실행할 경우 p는 실질적으로 CChild 인스턴스를 가리키지만 CPlayer의 ShowMe 함수를 호출하게됩니다. 하지만, 가상 함수를 사용할 경우 데이터 타입이 아닌 인스턴스를 기준으로 어떤 함수를 호출할지 결정지을 수 있습니다.




가상 함수 사용법

 

#include <iostream>
#include <Windows.h>

class CPlayer {
public:
    virtual void ShowMe() {
        std::cout << "CPlayer" << std::endl;
    }
};


class CChild : public CPlayer {
public:
    virtual void ShowMe() override {
        std::cout << "CChild" << std::endl;
    }
};

int main()
{
    CPlayer* p = new CChild();

    p->ShowMe();

    return 1;
}

 

  • 가상 함수를 사용하려면 가상 함수로 정의할 멤버 함수에 virtual 키워드를 사용하면 됩니다. 그러면 해당 클래스를 상속 받은 클래스에서 가상 함수 오버 라이딩할 수 있으며, 컴파일러는 해당 가상 함수를 호출할 때 데이터 타입이 아닌 인스턴스에 있는 가상 함수 테이블 포인터를 참조하여 어떤 클래스의 가상 함수를 호출할지 결정 짓습니다.

  • 부모 클래스에서 상속 받은 가상 함수를 오버라이딩 할 때 이후 자식 클래스에서 오버라이딩을 허용시키려면 override 키워드를 사용하고 이번 클래스가 마지막 오버라이딩이라면 final 키워드를 붙이면 됩니다.




순수 가상함수와 추상 클래스

 

class CPlayer {
public:
    virtual void ShowMe() = 0;
};

class CChild : public CPlayer {
public:
    virtual void ShowMe() {
        std::cout << "CChild" << std::endl;
    }
};

int main()
{
    // 인스턴스와 불가
    CPlayer a;

    CPlayer* p = new CChild();

    p->ShowMe();

    return 1;
}

 

  • 순수 가상함수란 가상 함수의 정의 대신 0을 대입하는 형태로 선언된 함수를 말합니다. 이렇게 순수 가상 함수가 선언된 클래스는 인스턴스화될 수 없는 오직 인터페이스 역할로만 사용할 수 있는 클래스입니다. 그리고 C++은 순수 가상함수를 가진 클래스를 "추상 클래스"라고 합니다.




가상 함수 테이블이란?

 

  • 가상 함수 테이블은 컴파일러가 가상 함수를 가진 클래스를 컴파일하여 생성하는 가상함수 참조 테이블입니다. 그리고 가상 함수를 가진 클래스가 생성자가 호출되어 인스턴스화될 때 객체의 맨 앞에는 가상 함수 테이블 포인터가 셋팅됩니다.




생성자와 가상 함수 테이블

 

#include <iostream>
#include <Windows.h>

class CPlayer {
public:
    virtual void ShowMe() = 0;
};


class CChild : public CPlayer {
public:
    virtual void ShowMe() {
        std::cout << "CChild" << std::endl;
    }
};

int main()
{
    CPlayer* p = static_cast<CPlayer*>(malloc(sizeof(CChild)));

    p->ShowMe();

    return 1;
}

 

  • 가상 함수 테이블을 셋팅하려면 반드시 생성자가 호출되어야 합니다. 위와 같이 malloc으로 동적할당 후 가상 함수를 호출할 경우 가상 함수 테이블이 없기 때문에 참조 에러가 발생됩니다.




+ Recent posts