중급 C++ 준비

중급 C++ 백엔드 면접 질문

성능, 소유권, 운영 환경에서의 트레이드오프를 논의해야 하는 백엔드 개발자를 위한 선별된 중급 C++ 면접 질문 15개

중급 C++ AI 면접 시작하기신용카드가 필요하지 않습니다. 무료 세션 1회 제공.
영어 기술 면접 연습비원어민이 기술 면접 통과를 연습할 수 있는 모드입니다.

메모리 관리

1스마트 포인터에서 사용자 정의 삭제자는 어떻게 동작하며, C/C++ 상호 운용성 측면에서 언제 유용한가요?

사용자 정의 삭제자는 스마트 포인터가 소유한 리소스를 해제할 때 기본 delete 연산 대신 실행되는 호출 가능한 정리 로직입니다. fclose, close, curl_easy_cleanup, SSL_free, free 또는 라이브러리 전용 해제 함수처럼 특정 함수로 리소스를 정리해야 하는 C/C++ 상호 운용 작업에서 유용합니다. unique_ptr의 경우 삭제자 타입이 unique_ptr 타입의 일부가 되므로 포인터 크기에 영향을 줄 수 있습니다. 상태가 없는 삭제자는 최적화되어 크기를 차지하지 않을 수 있지만, 함수 포인터나 상태를 지닌 삭제자는 추가 메모리를 차지합니다. shared_ptr의 경우 삭제자가 제어 블록에 저장되며, 마지막 강한 소유자가 객체를 해제할 때 실행됩니다. 사용자 정의 삭제자를 사용하면 C 스타일 리소스도 RAII(Resource Acquisition Is Initialization) 패턴에 안전하게 참여시킬 수 있습니다.

using FilePtr = std::unique_ptr<FILE, int(*)(FILE*)>;

FilePtr open_file(const char* path) {
    return FilePtr(std::fopen(path, "r"), &std::fclose);
}

struct CurlDeleter {
    void operator()(CURL* h) const noexcept {
        if (h) curl_easy_cleanup(h);
    }
};

using CurlPtr = std::unique_ptr<CURL, CurlDeleter>;
CurlPtr make_curl() {
    return CurlPtr(curl_easy_init());
}
AI 코치와 함께 이 질문에 답해 보세요

2순환 참조, enable_shared_from_this, 참조 카운트 비용을 포함하여 weak_ptr와 shared_ptr의 제어 블록(control block) 동작 메커니즘을 설명해 주세요.

`shared_ptr`는 강한 참조 카운트(strong reference count), 약한 참조 카운트(weak reference count), 그리고 삭제자(deleter)나 할당자(allocator)와 같은 정리 정보를 포함하는 제어 블록을 통해 공유 소유권을 관리합니다. `shared_ptr` 객체를 복사하거나 소멸시키면 일반적으로 원자적 연산을 통해 강한 참조 카운트가 증가하거나 감소하므로, 서로 다른 스레드에서 개별 `shared_ptr` 인스턴스를 안전하게 조작할 수 있지만 매 참조 카운트 갱신마다 비용이 발생합니다. 강한 참조 카운트가 0에 도달하면 관리 대상 객체가 소멸하지만, 제어 블록은 약한 참조 카운트까지 모두 0이 될 때까지 메모리에 유지됩니다. `weak_ptr`는 객체 수명을 연장하지 않고 동일한 제어 블록을 가리킵니다. `lock()` 메서드는 객체가 여전히 유효하면 `shared_ptr`를 반환하고, 이미 소멸했다면 빈 `shared_ptr`를 반환합니다. `shared_ptr`로만 구성된 순환 참조는 강한 참조 카운트가 0에 도달하지 않아 메모리 누수가 발생하므로, 역방향 포인터나 관찰자(observer) 링크에는 `weak_ptr`를 사용해야 합니다. `enable_shared_from_this`는 이미 `shared_ptr`로 관리 중인 객체가 기존 제어 블록을 재사용하여 자기 자신을 가리키는 새 `shared_ptr`를 안전하게 생성할 수 있도록 하여, 독립된 제어 블록이 중복 생성되는 위험을 방지합니다. 참조 카운트 비용으로는 원자적 증감 연산, 캐시 경합(cache contention), 제어 블록 할당 오버헤드, 성능상 중요한 코드 경로에서의 실행 지연 등이 있습니다.

struct Parent;

struct Child {
    std::weak_ptr<Parent> parent; // non-owning back-reference
};

struct Parent {
    std::vector<std::shared_ptr<Child>> children; // owns children
};

void example() {
    auto p = std::make_shared<Parent>();
    auto c = std::make_shared<Child>();
    c->parent = p;
    p->children.push_back(c);

    if (auto locked = c->parent.lock()) {
        // safe use while Parent is still alive
    }
}
AI 코치와 함께 이 질문에 답해 보세요

템플릿

3C++의 값 범주(value category)와 완벽한 전달(perfect forwarding)을 설명하고, 이것이 효율적인 제네릭 API에서 왜 중요한지 설명해 주세요.

C++의 값 범주는 표현식(expression)의 특성을 나타냅니다. lvalue는 고유한 식별성(identity)을 가지며 표현식 평가 이후에도 참조될 수 있습니다. prvalue는 다수의 임시 객체나 계산된 값과 같은 순수 rvalue입니다. xvalue는 자원을 재사용할 수 있는 만료 직전의 객체(expiring object)입니다. 완벽한 전달은 전달 참조(forwarding reference, 보통 타입 추론이 일어나는 T&&)를 받고 std::forward<T>(arg)를 사용하여 호출자의 값 범주를 그대로 유지하면서 전달하는 템플릿 기법입니다. 이를 통해 lvalue는 lvalue로, rvalue는 rvalue로 유지되며, 이는 참조 축약(reference collapsing) 규칙 덕분에 가능합니다. 래퍼(wrapper), 팩토리(factory), 디스패처(dispatcher), emplace 스타일 함수 등에서 불필요한 복사를 방지하고 오버로드 선택 및 이동 동작(move behavior)을 온전히 보존할 수 있으므로 제네릭 백엔드 API에서 매우 중요합니다.

#include <iostream>
#include <string>
#include <utility>

void sink(const std::string&) { std::cout << "const ref\n"; }
void sink(std::string&&) { std::cout << "rvalue ref\n"; }

template <class T>
void wrapper(T&& arg) {
    sink(std::forward<T>(arg));
}

int main() {
    std::string s = "hello";
    wrapper(s);
    wrapper(std::string{"x"});
}
AI 코치와 함께 이 질문에 답해 보세요

타입 시스템

4일반적인 백엔드 코드에서 auto, decltype, decltype(auto), 그리고 템플릿 인자 추론(template argument deduction)을 비교해 주세요.

auto는 변수에 대해 템플릿과 유사한 추론 방식을 사용합니다. 단순히 auto만 사용하면 auto&, const auto&, auto&&와 같이 선언에서 명시적으로 요구하지 않는 한 참조와 최상위(top-level) const가 보통 탈락됩니다. decltype(expr)은 선언된 타입이나 표현식 타입을 더 정확하게 조사합니다. 괄호가 없는 식별자 표현식(id-expression)은 선언된 타입을 그대로 반환하며, 그 외의 lvalue 표현식은 T&, xvalue는 T&&, prvalue는 T를 생성합니다. decltype(auto)는 decltype의 규칙을 사용하여 타입을 추론하며, 참조를 보존해야 하는 반환 타입에 주로 사용됩니다. 템플릿 인자 추론은 auto와 유사하지만 T, T&, const T&, T&&와 같은 매개변수 형태에 따라 달라지며 고유한 규칙을 갖습니다. 중괄호 초기화 목록(braced initializer)이 흔한 차이점 중 하나입니다. auto x = {1,2}는 std::initializer_list<int>로 추론되지만, 일반 템플릿 매개변수는 매개변수가 initializer_list나 다른 적절한 타입을 요구하지 않는 한 단순한 중괄호 초기화 목록으로부터 T를 추론할 수 없습니다.

int x = 1;
const int cx = 2;
int& rx = x;

auto a = rx;        // int, copy of x
auto b = cx;        // int, top-level const dropped
const auto& c = cx; // const int&, reference preserved by declaration

a = 10;             // does not change x
AI 코치와 함께 이 질문에 답해 보세요

5이종(heterogeneous) 메시지나 이벤트를 모델링할 때 std::variant와 상속 기반 다형성의 장단점을 비교해 주세요.

std::variant는 고정되고 닫힌 타입 집합 중에서 정확히 하나의 대안을 보유하는 값 타입(value type)이며, 주로 std::visit이나 명시적 타입 조회를 통해 처리됩니다. 메시지 종류의 집합이 미리 알려져 있고, 가상 함수 디스패치 없이, 그리고 대개 객체별 힙 할당 없이 타입 안전한 처리를 원할 때 프로토콜 메시지나 이벤트 모델링에 유용합니다. 상속 기반 다형성은 기반 클래스와 가상 함수를 사용하여 공통 인터페이스를 통해 디스패치합니다. 이는 파생 메시지 타입 집합이 열려 있거나, 독립적으로 확장 가능하거나, 플러그인 형태이거나, 안정적인 인터페이스 뒤에 숨겨져 있을 때 더 적합합니다. 요약하자면 variant는 닫힌 합 타입(sum type), 메모리 지역성, 컴파일 타임 검사에 유리하며, 상속은 확장성, 런타임 다형성, 인터페이스 기반 설계에 유리합니다.

struct UserCreated { std::string id; };
struct UserDeleted { std::string id; };
struct PasswordChanged { std::string id; };

using Event = std::variant<UserCreated, UserDeleted, PasswordChanged>;

void handle(Event const& e) {
    std::visit([](auto const& msg) {
        using T = std::decay_t<decltype(msg)>;
        if constexpr (std::is_same_v<T, UserCreated>) {
            // handle creation
        } else if constexpr (std::is_same_v<T, UserDeleted>) {
            // handle deletion
        } else if constexpr (std::is_same_v<T, PasswordChanged>) {
            // handle password change
        }
    }, e);
}
AI 코치와 함께 이 질문에 답해 보세요

언어 의미론

6C++에서 정의되지 않은 동작(undefined behavior)이란 무엇이며, 백엔드 프로덕션 장애에서 어떻게 나타날 수 있나요?

정의되지 않은 동작(UB, Undefined Behavior)은 유효하지 않은 연산이 발생한 후 C++ 표준이 아무런 요구 사항도 규정하지 않는 동작을 의미합니다. 프로그램이 정상적으로 작동하는 것처럼 보일 수도 있고, 충돌(crash)이 발생하거나, 데이터가 손상되거나, 보안 취약점이 노출되거나, 컴파일러 최적화로 인해 예상치 못한 동작으로 이어질 수도 있습니다. 컴파일러는 UB가 발생하지 않는다고 가정하고 최적화를 수행하기 때문에, 릴리스 빌드나 프로덕션 트래픽 환경에서만 문제가 표면화되기도 합니다. 백엔드 장애는 댕글링 포인터/참조(dangling pointer/reference), use-after-free, 객체 수명 위반, 경계 초과 접근(out-of-bounds access), 부호 있는 정수 오버플로, 데이터 레이스(data race), 잘못된 캐스팅, 초기화되지 않은 메모리 읽기, 이중 해제(double free), strict-aliasing 위반 등에서 비롯될 수 있습니다. 완화 대책으로는 RAII (Resource Acquisition Is Initialization) 및 명확한 소유권/수명 설계, 안전한 추상화 및 경계 검사, 테스트/퍼징(fuzzing), 코드 리뷰, 정적 분석, 그리고 ASan(AddressSanitizer), UBSan(UndefinedBehaviorSanitizer), TSan(ThreadSanitizer)과 같은 새니타이저(sanitizer) 활용 등이 있습니다.

int grow(int x) {
    if (x + 1 < x) {
        return 0; // compiler may assume this is unreachable
    }
    return x + 1;
}
AI 코치와 함께 이 질문에 답해 보세요

7정의되지 않은 동작(undefined behavior), 지정되지 않은 동작(unspecified behavior), 구현 정의 동작(implementation-defined behavior)의 차이점을 백엔드 관련 예시와 함께 설명해 주세요.

정의되지 않은 동작(UB, Undefined Behavior)은 C++ 표준이 아무런 요구사항도 강제하지 않는 동작을 의미하며, 비정상 종료(crash), 데이터 손상, 보안 취약점, 최적화기로 인한 잘못된 컴파일(miscompilation) 등의 결과를 초래할 수 있습니다. 경계 밖 접근(out-of-bounds access), 해제 후 사용(use-after-free), 부호 있는 정수 오버플로, 데이터 경쟁(data race) 등이 대표적인 예시입니다. 지정되지 않은 동작(unspecified behavior)은 표준에서 두 가지 이상의 결과를 허용하지만, 구현체가 어떤 방식을 선택했는지 문서화할 필요가 없는 동작을 뜻합니다. 함수 인자의 평가 순서가 흔한 예이며, 따라서 부수 효과(side effect)가 특정 순서로 평가되는 것에 의존하는 코드를 작성해서는 안 됩니다. 구현 정의 동작(implementation-defined behavior)은 구현체가 동작 방식을 반드시 선택하고 이를 문서화해야 하는 동작을 말합니다. 기본 `char` 타입의 부호 유무, 표준 범위 내에서 기본 타입들의 크기나 표현 범위, 부호 있는 정수의 우측 시프트(right shift) 동작 일부가 여기에 포함됩니다. 백엔드 코드에서 정의되지 않은 동작은 정확성과 보안 측면의 위험 요소인 반면, 지정되지 않은 동작과 구현 정의 동작은 이식성 위험 요소이므로 프로토콜, 스토리지 및 크로스 플랫폼 로직에서는 이를 피하거나 격리해야 합니다.

int i = 0;
f(i++, i++); // both increments happen, but the order of argument evaluation is unspecified

// Prefer:
int a = i++;
int b = i++;
f(a, b);
AI 코치와 함께 이 질문에 답해 보세요

오류 처리

8예외 안전성 수준을 설명하고, noexcept가 이동 연산, 컨테이너, 코드 생성에 어떤 영향을 미치는지 설명해 주세요.

예외 안전성 수준은 연산 도중 예외가 발생했을 때 어떤 상태가 보장되는지를 나타냅니다. 기본 보장(basic guarantee)은 상태가 변경될 수는 있지만 불변식(invariant)이 유지되고 리소스 누수가 발생하지 않음을 의미합니다. 강력한 보장(strong guarantee)은 커밋 또는 롤백(commit-or-rollback) 시맨틱을 의미하며, 실패 시 관찰 가능한 상태가 변경되지 않습니다. 무예외 보장(nothrow guarantee)은 연산이 절대 예외를 던지지 않음을 의미합니다. 일반적인 구현 기법으로는 RAII (Resource Acquisition Is Initialization), 임시 객체에서 작업 수행, 복사 후 맞바꾸기(copy-and-swap), 예외를 던질 수 있는 작업이 성공한 후에만 커밋이 일어나도록 상태 변경 순서를 정렬하는 방법 등이 있습니다. `noexcept`는 하나의 계약으로, `noexcept` 함수가 예외를 던지면 `std::terminate`가 호출됩니다. 또한 이는 제네릭 코드와 컨테이너에도 영향을 미칩니다. 예를 들어 재할당 시 `std::vector`는 이동 생성자가 `noexcept`일 때만 요소를 이동할 수 있으며, 그렇지 않은 경우 예외 보장을 유지하기 위해 보통 `std::move_if_noexcept`를 통해 복사를 수행합니다. `noexcept`는 필요한 예외 전파 경로를 줄임으로써 코드 생성이나 최적화에도 도움을 줄 수 있지만, 이는 계약이 올바를 때만 유효합니다.

std::vector<T> v;
// During reallocation, an implementation may effectively do:
T* dest = allocate(new_cap);
for (T& x : old_storage) {
    construct(dest++, std::move_if_noexcept(x));
}
AI 코치와 함께 이 질문에 답해 보세요

9실패할 수 있는 연산에서 예외(exception)의 대안으로 사용되는 std::expected 또는 이와 동등한 결과(outcome) 타입을 설명해 주세요.

`std::expected<T, E>`는 성공 값 `T` 또는 명시적 오류 `E` 중 하나를 나타내어, 예외 전파에 의존하는 대신 오류를 함수의 반환 타입 일부로 만듭니다. 이는 파싱, 유효성 검사, 네트워크 호출, 스토리지 조회, 재시도 가능한 백엔드 연산처럼 오류 발생이 예상되고 호출자가 로컬에서 이를 처리해야 하는 연산에 유용합니다. 오류 타입에는 오류 코드, 카테고리, 재시도 가능 여부, 메시지, HTTP/RPC 매핑과 같은 구조화된 정보를 담는 것이 좋습니다. Expected/outcome 타입은 오류를 확인하고 전파하는 방식으로 결합할 수 있으며, C++23 스타일의 API에서는 `and_then`, `transform`, `or_else`와 같은 모나딕(monadic) 연산을 사용해 깊게 중첩된 조건문 없이 여러 단계를 체이닝할 수 있습니다. 예외와 비교했을 때 제어 흐름과 API 계약이 명확해지고 예외가 비활성화된 환경이나 ABI 경계에서도 잘 동작합니다. 다만 프로젝트 정책에 따라 드물게 발생하거나 비지역적이거나 진정으로 예외적인 실패에는 여전히 예외가 적합할 수 있습니다.

enum class Errc { BadRequest, NotFound, Timeout, DbUnavailable };

struct Error {
    Errc code;
    bool retryable;
    std::string message;
};

std::expected<User, Error> load_user(UserId id);

HttpResponse handle(UserId id) {
    auto user = load_user(id);
    if (!user) {
        switch (user.error().code) {
            case Errc::BadRequest: return {400, user.error().message};
            case Errc::NotFound: return {404, "not found"};
            case Errc::Timeout: return {503, "try again"};
            case Errc::DbUnavailable: return {503, "service unavailable"};
        }
    }
    return {200, serialize(*user)};
}
AI 코치와 함께 이 질문에 답해 보세요

객체 수명

10자동, 동적, 임시, 그리고 비동기로 참조되는 객체의 수명 규칙을 설명해 주세요.

자동 객체(automatic object)는 생성 시점부터 해당 스코프가 끝날 때까지 생존하며, 스코프를 벗어난 후 이를 가리키는 포인터나 참조는 댕글링(dangling) 상태가 됩니다. 동적 객체(dynamic object)는 할당 및 생성 시점부터 명시적으로 소멸되거나 소유한 객체에 의해 소멸될 때까지 생존하며, 원시 포인터와 참조는 수명을 연장하지 않습니다. 임시 객체는 일반적으로 전체 표현식(full expression)이 끝날 때까지 생존하지만, 참조에 바인딩될 때 특정 수명 연장 규칙이 적용되기도 합니다(단, 모든 상황에 적용되는 것은 아닙니다). 콜백, 스레드, 타이머, 코루틴 등의 비동기 작업은 참조 대상 객체가 소멸된 후에 실행될 수 있으므로, 댕글링 참조 및 use-after-free 문제를 방지하기 위해 캡처와 저장된 참조의 수명을 명시적으로 관리해야 합니다.

std::function<void()> make_cb() {
    std::string s = "hello";
    return [&] { std::cout << s << '\n'; }; // BAD: captures local by reference
} // s is destroyed here

std::function<void()> safe_cb() {
    auto s = std::make_shared<std::string>("hello");
    return [s] { std::cout << *s << '\n'; }; // OK: callback owns the string
}
AI 코치와 함께 이 질문에 답해 보세요

11정적 초기화 순서 문제가 무엇인지 설명하고, constinit, 매직 정적 변수(magic statics), 의존성 주입이 이를 어떻게 완화하는지 설명해 주세요.

정적 초기화 순서 문제는 서로 다른 번역 단위(translation unit)에 있는 동적으로 초기화되는 네임스페이스 범위 객체나 정적 객체들의 상대적인 초기화 순서가 정해져 있지 않아(unspecified) 발생합니다. 한 전역 객체가 생성되기 전에 다른 객체에서 참조될 수 있으며, 프로그램 종료 시 소멸 순서에서도 유사한 문제가 발생할 수 있습니다. `constinit`은 정적 또는 스레드 로컬 변수가 정적/상수 초기화되도록 강제하며, 그렇지 않으면 컴파일 오류를 발생시켜 해당 변수의 동적 초기화 순서 의존성을 방지합니다. 매직 정적 변수, 즉 함수 로컬 정적 변수는 최초 사용 시점에 초기화되며 C++11부터 스레드 안전합니다. 의존성 주입은 제어된 순서로 객체를 생성하고 의존성을 명시적으로 전달함으로써 암묵적인 전역 의존성을 제거합니다.

// a.cpp
extern std::string configPath;
Logger logger(configPath); // may run before configPath is dynamically initialized

// b.cpp
std::string configPath = readConfigPath();
AI 코치와 함께 이 질문에 답해 보세요

12복사 생략(copy elision), NRVO(Named Return Value Optimization), RVO(Return Value Optimization)란 무엇이며, 언제 이들에 의존할 수 있나요?

복사 생략(copy elision)은 별도의 임시 객체를 생성하여 복사하거나 이동하는 대신, 객체를 최종 목적지에 직접 생성하는 것을 의미합니다. RVO는 대개 `return T{};`처럼 이름 없는 임시 객체나 prvalue를 반환하는 것을 가리킵니다. C++17 이후로는 prvalue가 결과 객체를 직접 초기화하므로 이러한 경우 중 상당수가 필수적으로 적용됩니다. NRVO는 `return x;`와 같이 이름이 있는 지역 객체를 반환하는 것을 말합니다. 컴파일러는 호출자의 반환 슬롯에 해당 지역 객체를 직접 생성할 수 있도록 허용되지만, 이는 보장되지 않습니다. 규정된 상황에서 C++17의 필수 prvalue 생략에는 의존할 수 있지만, NRVO가 항상 일어난다고 신뢰할 수는 없습니다. 또한, 이름 있는 지역 반환 값에 `std::move`를 사용하면 NRVO가 방해받을 수 있으므로 피해야 합니다.

struct T {
    T();
    T(const T&) = delete;
    T(T&&) = delete;
};

T make() {
    return T{}; // OK in C++17+: result object is constructed directly
}
AI 코치와 함께 이 질문에 답해 보세요

표준 라이브러리

13비교자, 동등성, 해시 함수의 요구사항은 연관 컨테이너의 정확성에 어떤 영향을 미치나요?

연관 컨테이너는 키의 동일성을 정의하고 내부 불변식(invariant)을 유지하기 위해 비교, 동등성, 해싱에 의존합니다. std::map과 같은 순서 정렬 컨테이너는 비교자가 엄격한 약순서(strict weak ordering)를 만족해야 합니다. 이때 키는 반드시 operator==가 아니더라도 둘 중 어느 것도 상대방보다 작지 않을 때 동등한 것으로 간주됩니다. 비순서형(unordered) 컨테이너는 동등성 판단이 동치 관계(equivalence relation)를 만족해야 하며, 서로 같다고 판별된 두 키는 반드시 동일한 해시값을 생성해야 합니다. 컨테이너에 저장된 키는 정렬 순서, 동등성 또는 해시를 변경하는 방식으로 수정되어서는 안 되며, 이를 위반하면 검색, 삭제 및 고유성 검사가 정상적으로 작동하지 않을 수 있습니다.

struct Key {
    int tenant;
    std::string id;
};

struct KeyEq {
    bool operator()(Key const& a, Key const& b) const {
        return a.tenant == b.tenant && a.id == b.id;
    }
};

struct KeyHash {
    std::size_t operator()(Key const& k) const {
        return std::hash<int>{}(k.tenant) ^
               (std::hash<std::string>{}(k.id) << 1);
    }
};

std::unordered_map<Key, int, KeyHash, KeyEq> m;
AI 코치와 함께 이 질문에 답해 보세요

14정렬 및 비정렬 연관 컨테이너에서 이종 룩업(heterogeneous lookup)에 대해 설명해 주세요.

이종 룩업(heterogeneous lookup)을 사용하면 연관 컨테이너에서 키 타입과 다른 타입을 사용하여 검색할 수 있으므로, 임시 키 객체 생성을 방지할 수 있습니다. 정렬된 컨테이너에서는 `std::less<>`나 `is_transparent` 태그가 정의된 사용자 정의 비교자와 같은 투명한 비교자(transparent comparator)가 필요합니다. 비정렬 컨테이너에서는 키 타입과 조회 타입을 일관되게 처리할 수 있는 투명한 해시 함수와 투명한 동등 비교 함수자가 모두 필요합니다. 백엔드에서 흔히 볼 수 있는 예로는 `std::string`을 키로 사용하는 컨테이너에서 임시 `std::string`을 할당하지 않고 `std::string_view`나 `const char*`로 검색하는 경우가 있습니다.

std::map<std::string, int, std::less<>> counts;
counts.emplace("alpha", 1);

std::string_view key = "alpha";
auto it = counts.find(key); // no temporary std::string required
AI 코치와 함께 이 질문에 답해 보세요

동시성

15C++ 메모리 모델의 데이터 레이스(data race), 선행 발생(happens-before), 동기화 보장에 대해 설명해 주세요.

C++ 메모리 모델은 서로 다른 스레드에서 수행되는 연산 간의 순서와 쓰기 작업이 가시화되는 시점을 정의합니다. 데이터 레이스(data race)는 둘 이상의 스레드가 동일한 메모리 위치에 동시에 접근하고, 그중 최소 하나의 접근이 쓰기 작업이며, 접근 간에 선행 발생(happens-before) 관계가 성립하지 않거나 원자적(atomic) 연산으로 안전하게 처리되지 않았을 때 발생합니다. 데이터 레이스는 정의되지 않은 동작(undefined behavior)을 유발합니다. 선행 발생은 이전 연산의 부수 효과(side effect)가 이후 연산에 가시적으로 보이도록 보장하는 순서 관계입니다. 동기화 연산은 이러한 순서 관계를 형성합니다. 예를 들어 뮤텍스(mutex)를 해제(unlock)하면 이후 동일한 뮤텍스를 성공적으로 잠그는(lock) 동작과 동기화(synchronizes-with)되며, 적절한 원자적 릴리스/획득(release/acquire) 연산을 통해서도 스레드 간 동기화를 이룰 수 있습니다. 올바른 프로그램은 뮤텍스, 원자적 연산 또는 기타 동기화 메커니즘을 사용하여 공유 상태에 대한 선행 발생 관계를 확립합니다.

std::mutex m;
int x = 0;

void producer() {
    std::lock_guard<std::mutex> lk(m);
    x = 42;
} // unlock

void consumer() {
    std::lock_guard<std::mutex> lk(m); // lock after producer unlock synchronizes-with it
    std::cout << x << '\n';
}
AI 코치와 함께 이 질문에 답해 보세요