Pages

Tuesday, January 31, 2023

std::move = std::optional이 출동할 때

최근 Rust를 살펴볼 기회가 있었습니다. Rust는 function이 결과를 반환할때 std::option이라는걸 사용해서 정상이면 A 클래스를, 비정상이면 B 클래스를 반환하는 형태를 취하고 있더군요.

그래서...... 혹시나 해서 찾아봤더니, C++에도 std::optional이 있는 것을 발견했습니다. 다만 대부분의 사용자들은 std::optional을 왜 쓰느냐는 입장이었습니다. 우리에겐 (밉고도 고운) nullptr이 있는데 뭐하러 저런걸 쓰느냐...... 하는 거였습니다. C++ Committee의 입장은 실수를 줄이기 위해서라고 하던데, 그렇다면 대체 무슨 실수가 나올 수 있을까요?

우선, 다음의 코드를 살펴봅시다.

struct Insider {
    /* whatever great data structure */
};

struct anti_memory_leak {
    Insider *insider=nullptr;
    ~anti_memory_leak() {
        if(insider) delete insider;
    }
};

 확실히, 이 코드는 별 특이사항이 없어 보입니다. Insider는 쓰일 때도 있고 안 쓰일때도 있어서 필요한 경우에만 heap에서 생성하도록 해 두었습니다. anti_memory_leak이 삭제될 경우 Insider 객체가 존재하면 같이 지우도록 해서 메모리 누수 방지도 해 두었습니다. 우리는 std::optional이 없어도 아무런 문제가 없음을 증명했습니다.

...... 과연 그럴까요?

다음의 코드를 봅시다.

void doSomethingGreat()
{
    anti_memory_leak object1;
    object1.insider=new Insider();

    std::vector<anti_memory_leak> vector1;
    vector1.push_back(std::move(object1));
    vector1.back().insider->value1=20; // CRASH!
}
 

이 코드는 맨 마지막줄에서 프로그램이 터지는 결과를 가져옵니다. 왜 그럴까요? 이유는 바로 윗줄에 있습니다. vector1.push_back()을 호출할 경우, std::move()를 쓴다고 하더라도 object1은 파괴 후 재생성됩니다. 이 때 anti_memory_leak의 파괴자가 호출되고, 파괴자는 insider를 heap에서 제거합니다. 뒤집어서 말하면, vector1.back().insider는 dangling pointer가 되어버리는 셈이죠. 아울러 유감스럽지만, push_back()이 아니라 emplace_back()을 사용하더라도 동작은 마찬가지입니다. 어쨌든 터지는 것은 매한가지 되겠습니다.

바로 이 때가 std::optional이 출동할 차례입니다. std::optional을 사용하여 객체를 선언하면 일단 메모리는 사용되지 않은 채로 초기화되고, 추가 메모리는 실제로 해당 객체를 선언할 때 사용을 시작합니다. 물론 객체 생성에 따른 overhead가 좀 더 있긴 합니다만, 사실 그건 유사한(?) 역할을 수행하는 std::shared_ptr 같은 클래스도 마찬가지지요. raw pointer가 있지만 해당 heap 영역을 좀 더 안전하게 사용하기 위해서 운영의 일부를 자동화하여 개발자가 자칫 놓칠 수 있는 메모리 관리상의 문제점들(메모리 누수와 dangling pointer 모두)을 쉽게 해결하고자 하는 것입니다.

위의 코드를 std::optional을 사용하여 다시 작성한다면 이렇게 작성할 수 있겠습니다.

struct Insider {
    int value1;
    /* whatever great data structure */
};

struct anti_memory_leak {
    std::optional<Insider> insider;
    ~anti_memory_leak() {
        if(insider) delete insider;
    }
};

void doSomethingGreat()
{
    anti_memory_leak object1;
    object1.insider=Insider();

    std::vector<anti_memory_leak> vector1;
    vector1.push_back(std::move(object1));
    vector1.back().insider.value1=20; // OK
}

사실 객체를 다루는 코드의 흐름이 단순하다면 크게 문제가 없겠지만, 멀티스레딩이라던가, 아니면 코드가 어떤 식으로든 복잡해지게 되면 분명히 나도 모르게 메모리를 해제하게 되거나, 내지는 해제해야 할 때 못하는 경우가 생겨나게 됩니다. std::optional은 이런 경우에 대비한 일종의 보험이라 하겠습니다. 보험료를 내는 사람들은 (뭐 좀 아깝긴 하지만) 그래도 만일의 경우에 대비해서 보험료를 내는 거잖아요? std::optional도 마찬가지라고 봅니다.

No comments:

Post a Comment