[말랑 퀴즈] 26/08/22 해답

게시:     수정

카테고리:

태그:

제 말랑말랑 퀴즈 생성기는 이곳에서 확인하실 수 있습니다.

말랑말랑 퀴즈 — 해답지 ✅

날짜: 2026-08-22 문제 수: 5문제 총점: 31.2/50 (62점) 🟡 복습이 필요해요

📊 채점 결과

문항 유형 난이도 결과 점수
Q1 서술형 🟡 보통 🔺 부분 정답 3.5 / 10
Q2 객관식 🟡 보통 ⭕ 정답 10 / 10
Q3 빈칸 채우기 🟢 쉬움 🔺 부분 정답 6.7 / 10
Q4 서술형 🔴 어려움 🔺 부분 정답 4 / 10
Q5 OX 🔴 어려움 🔺 부분 정답 7 / 10
합계       31.2 / 50

총평

어제 49점에서 62점으로 올랐고, 특히 서술 방식이 좋아졌습니다. Q4에서 예시 시나리오(0s 걷기 → 0.1s 달리기 → 0.3s 걷기)를 들어 설명하신 건 어제 지적했던 “순서대로 적기”를 실제로 적용하신 것입니다. Q2도 오답 D(“가상 함수가 있으면 추상 클래스”)를 피해 정확히 B를 고르셨습니다.

Q1이 가장 아쉽습니다. 이 항목의 제목이 “operator=에서는 자기대입에 대한 처리가 빠지지 않도록 하자” 인데, 답에 자기대입이 한 번도 등장하지 않습니다. w = wa[i] = a[j]처럼 *thisrhs가 같은 객체일 때 delete pb 이후의 *rhs.pb가 이미 지운 메모리를 읽는다는 것이 이 항목의 출발점입니다. 대신 “delete에서 예외”라는, 실제로는 문제가 아닌 상황을 드셨습니다.

Q4는 “무엇을 하는가”는 썼지만 “왜 필요한가”에 답하지 못했습니다. 문제가 물은 건 부모가 이미 MaxSpeed를 비교하는데도 왜 중복처럼 보이는 검사를 또 하느냐 였고, 답은 공중입니다. IsMovingOnGround() 조건 때문에 점프 중에는 Sprint를 켜든 끄든 속도가 같아져서 부모 검사를 통과해버립니다.

두 문제 모두 문제 본문 안에 단서가 있었습니다. Q1은 “두 가지 상황”이라는 표현, Q4는 코드에 그어둔 IsMovingOnGround()입니다. 답을 떠올리기 전에 제시된 코드에서 유독 눈에 띄는 조건이 왜 거기 있는지 한 번 물어보는 습관을 들이면 좋겠습니다.


Q1. 🟡 보통 — 🔺 3.5/10

문제: 아래 operator=가 위험한 두 상황을 설명하고, 자기대입 검사 한 줄을 추가하는 방식의 한계와, 그 한 줄 없이 두 상황을 모두 안전하게 만드는 방법을 서술하시오.

Widget& Widget::operator=(const Widget& rhs) {
    delete pb;
    pb = new Bitmap(*rhs.pb);
    return *this;
}

📝 내 답:

(1) 1. delete pb; 에서 예외가 발생하는 상황 2. new 연산자에서 예외가 발생하는 상황

기존 pb를 백업해두지 않은 상태에서 pb를 삭제하고 대입하고 있으며, new 연산자에서 예외가 발생하면 메모리 누수가 발생할 수 있기 때문이다.

(2) 어디까지 동일한 객체라고 판단할 것인지 경계가 모호하다. (일부 변수만 같으면 같은가? 아니면 전체가 같아야 하는가?)

(3)

Widget& Widget::operator=(const Widget& rhs) {
    Bitmap origin = pb;
    Bitmap value = rhs;
    try { swap(pb, value); }
    catch(const std::exception e&) { pb = origin; }
    return *this;
}

swap 말고도 객체 생성 시 두개의 코드 줄로 나누는 방식도 있었는데 정확히 기억은 안난다. 안전해지는 이유는 기존 데이터 수정은 없애, 예외 처리를 하게 두는 것이다.

정답:

(1) 위험한 두 상황

① 자기대입 (w = w)

*thisrhs같은 객체이면 pbrhs.pb도 같은 포인터다. 그런데 첫 줄에서 delete pb를 해버렸으므로, 다음 줄의 *rhs.pb이미 삭제된 메모리를 읽는다. pbdangling pointer가 된다.

자기대입은 생각보다 눈에 잘 안 띄게 발생한다.

w = w;             // 명백한 경우
a[i] = a[j];       // i == j이면 자기대입
*px = *py;         // 두 포인터가 같은 객체를 가리키면 자기대입

new에서 예외가 발생하는 경우

자기대입이 아니더라도 위험하다. delete pb가 이미 실행된 뒤 new Bitmap(...)메모리 부족이나 복사 생성자 예외로 실패하면, pb는 삭제된 메모리를 가리킨 채로 남는다. 역시 dangling pointer다.

즉 이 코드는 자기대입 안전성과 예외 안전성 둘 다 없다.

(2) 자기대입 검사의 한계

if (this == &rhs) return *this;   // 일치성 테스트

이 줄은 ①만 막고 ②는 못 막는다. 자기대입이 아닌 정상적인 대입에서 new가 예외를 던지면 여전히 dangling pointer가 된다.

게다가 대가가 있다.

  • 자기대입은 드물게 일어나는데 그것을 막자고 모든 호출에 분기를 추가하게 된다.
  • 코드가 복잡해지고, 분기 예측 실패 등 약간의 성능 손해가 생긴다.

(3) 문장 순서 조정

Widget& Widget::operator=(const Widget& rhs) {
    Bitmap* pOrig = pb;             // 원본 주소를 기억해둔다
    pb = new Bitmap(*rhs.pb);       // 먼저 복사한다
    delete pOrig;                   // 성공한 뒤에 삭제한다
    return *this;
}

“삭제 후 복사”를 “복사 후 삭제”로 뒤집은 것이 전부다. 이것만으로 두 문제가 동시에 사라진다.

  • 자기대입: *this == rhs여도 *rhs.pb를 읽는 시점에 원본이 아직 살아 있다. 복사가 끝난 뒤에야 원본을 지우므로 안전하다.
  • 예외: new가 실패하면 delete pOrig까지 가지 못하고 함수를 빠져나간다. pb원래 값 그대로 남아 객체가 유효한 상태를 유지한다.

대안 — 복사 후 맞바꾸기 (Copy-and-Swap)

// 방법 1: 명시적 복사 + swap
Widget& Widget::operator=(const Widget& rhs) {
    Widget temp(rhs);        // 복사 생성자로 사본 생성
    swap(*this, temp);       // 내부 자원을 맞바꿈
    return *this;
}

// 방법 2: 값 전달로 암시적 복사 + swap
Widget& Widget::operator=(Widget rhs) {   // rhs가 이미 복사본
    swap(*this, rhs);
    return *this;
}

복사 생성자와 swap만 제대로 있으면 자기대입 안전성과 예외 안전성이 자동으로 따라온다.

방법 자기대입 예외 평가
1. 일치성 테스트 단순하지만 불충분하고 우아하지 않음
2. 문장 순서 조정 안전하고 효율적
3. 복사 후 맞바꾸기 가장 깔끔. 복사 생성자·swap 필요

🔍 피드백: 세 소문항 각 3.33점으로 배분했습니다.

  • (1) 1.5 / 3.33 — ②(new 예외)는 맞습니다. 그런데 ①이 자기대입이라는 걸 놓치셨습니다. 이 항목의 제목이 바로 그것입니다. w = w, a[i] = a[j], *px = *py처럼 *thisrhs가 같은 객체이면 pbrhs.pb가 같은 포인터이고, delete pb 다음 줄의 *rhs.pb방금 지운 메모리를 읽습니다.

    대신 적으신 “delete pb;에서 예외”는 실제 문제가 아닙니다. 소멸자는 예외를 던지지 않아야 하고(항목 8), 설령 던진다 해도 첫 줄이라 아직 잃을 게 없습니다.

    용어도 하나 바로잡습니다. new가 실패했을 때 생기는 건 메모리 누수가 아니라 dangling pointer입니다. delete는 이미 됐으니 새는 메모리는 없고, pb해제된 주소를 가리킨 채 남는 것이 문제입니다.

    다만 “기존 pb를 백업해두지 않은 상태에서 삭제하고 대입한다” 는 진단은 정확합니다. 그게 바로 (3)의 해법이 고치는 지점입니다.

  • (2) 0 / 3.33this == &rhs주소(포인터) 비교입니다. 두 표현식이 같은 객체를 가리키는지 정확히 판정하며, “일부 변수만 같으면?” 같은 모호함이 원리적으로 없습니다. 값이 같은 별개 객체는 주소가 다르므로 통과합니다.

    이 방식의 진짜 한계는 두 가지입니다. 첫째, 자기대입은 막지만 예외는 못 막습니다. 남남끼리 대입하다 new가 던지면 여전히 dangling입니다. 둘째, 드문 상황 하나를 막자고 모든 호출에 분기를 답니다.

  • (3) 2.0 / 3.33원리는 잡으셨습니다. “기존 데이터 수정은 없애”가 정확히 요지이고, origin을 남겨두려 한 발상도 방향이 맞습니다.

    그리고 “두 개의 코드 줄로 나누는 방식”이 바로 정답인 문장 순서 조정입니다. 기억이 안 난다고 하셨지만 실루엣은 남아 계셨습니다.

    Bitmap* pOrig = pb;
    pb = new Bitmap(*rhs.pb);   // 먼저 만들고
    delete pOrig;               // 나중에 지운다
    

    try/catch도 필요 없습니다. new가 던지면 delete pOrig에 도달하지 못한 채 함수를 빠져나가고, pb는 원래 값 그대로라서 자동으로 안전합니다. 예외 안전성은 잡아서 되돌리는 게 아니라 던져도 손해가 없도록 순서를 짜는 것이 더 좋은 접근입니다.

    작성하신 코드에는 타입 오류도 있습니다. pbBitmap*인데 Bitmap origin = pb;로 받으셨고, Bitmap value = rhs;rhsWidget입니다. Copy-and-Swap으로 가시려면 Widget 단위로 복사해야 합니다 — Widget temp(rhs); swap(*this, temp);.

출처: game_dev/cpp/chapter2/2025-06-07-cpp_2_11.md


Q2. 🟡 보통 — ⭕ 10/10

문제: Airplane::fly가 단순 가상 함수인 상태에서, 비행 방식이 다른 ModelCfly 재정의를 빼먹었다. 이 상황에 대한 설명으로 가장 올바른 것은?

  • A. ModelCfly를 재정의하지 않았으므로 new ModelC 지점에서 컴파일 에러가 발생한다.
  • B. 단순 가상 함수는 인터페이스와 함께 기본 구현까지 물려주기 때문에 재정의를 강제할 방법이 없고, ModelC는 자신에게 맞지 않는 Airplane::fly를 그대로 쓰게 된다. fly를 순수 가상 함수로 바꾸고 기본 동작을 defaultFly 같은 별도 함수로 분리하면 재정의를 강제할 수 있다.
  • C. fly를 비가상 함수로 바꾸면 파생 클래스마다 각자의 구현을 제공하도록 강제할 수 있으므로 이 문제가 해결된다.
  • D. Airplane은 가상 함수를 가지고 있으므로 추상 클래스이며, 따라서 Airplane* pa = new ModelC; 자체가 컴파일되지 않는다.

📝 내 답: B

정답: B

해설:

public 상속은 인터페이스 상속구현 상속으로 나뉘고, 함수 타입이 그 조합을 결정한다.

함수 타입 인터페이스 상속 구현 상속 오버라이드
순수 가상 함수 ❌ (선택적) 필수
단순 가상 함수 ✅ (기본 구현) 선택
비가상 함수 ✅ (필수 구현) 불가
  • B (정답): 단순 가상 함수의 목적은 인터페이스와 기본 구현을 함께 물려주는 것이다. 그래서 재정의는 선택 사항이고, 컴파일러가 강제할 방법이 없다. ModelC는 조용히 Airplane::fly를 쓰게 되고 기본 구현이 적절하지 않은데도 사용된다.

    해결책은 인터페이스와 구현을 분리하는 것이다.

    class Airplane {
    public:
        virtual void fly(const Airport& destination) = 0;   // 인터페이스만 제공 → 재정의 강제
    protected:
        void defaultFly(const Airport& destination);        // 기본 구현. 비가상이라 재정의 불가
    };
    
    class ModelA : public Airplane {
    public:
        virtual void fly(const Airport& d) { defaultFly(d); }   // 명시적으로 선택
    };
    

    이제 ModelCfly를 빼먹으면 추상 클래스라 인스턴스를 만들 수 없어 컴파일 단계에서 걸린다. 다만 fly/defaultFly 두 이름이 생겨 클래스 네임스페이스가 지저분해진다.

    이를 줄인 방법이 순수 가상 함수에 구현을 제공하는 것이다.

    class Airplane {
    public:
        virtual void fly(const Airport& destination) = 0;
    };
    void Airplane::fly(const Airport& destination) { ... }      // 순수 가상 함수의 구현
    
    class ModelA : public Airplane {
    public:
        virtual void fly(const Airport& d) { Airplane::fly(d); }
    };
    

    더 깔끔하지만, 대신 서로 다른 보호 수준(public/protected)을 줄 수 없게 된다.

  • A (오답): 단순 가상 함수는 재정의가 선택이므로 컴파일 에러가 나지 않는다. 아무 경고 없이 통과해 런타임에 잘못 동작하는 것이 이 문제의 핵심이다.
  • C (오답): 정반대다. 비가상 함수는 인터페이스와 필수 구현을 물려주며 파생 클래스는 그것을 바꿀 수 없다.
  • D (오답): 추상 클래스가 되는 조건은 “가상 함수가 있는 것”이 아니라 “순수 가상 함수(= 0)가 있는 것” 이다. Airplane에는 단순 가상 함수뿐이라 추상 클래스가 아니다.

🔍 피드백: 만점입니다. 이 문제의 함정은 D였습니다. “가상 함수 → 추상 클래스”로 연결하기 쉬운데, 추상 클래스를 만드는 건 = 0이 붙은 순수 가상 함수뿐입니다. Airplane에는 구현이 있는 단순 가상 함수만 있으니 인스턴스도 만들 수 있습니다.

C도 방향이 정반대인 보기였는데 잘 걸러내셨습니다. 비가상 함수는 강제가 아니라 금지입니다 — 파생 클래스가 바꾸지 못하게 막는 쪽이죠.

세 함수 타입을 “오버라이드가 필수냐 / 선택이냐 / 불가냐” 한 축으로 외워두시면 이 계열 문제는 계속 맞히실 수 있습니다.

출처: game_dev/cpp/chapter6/2025-10-20-cpp_6_34.md


Q3. 🟢 쉬움 — 🔺 6.7/10

문제: 빈칸을 채우시오.

auto y1 = 27;        // 형식 = ① ___
auto y3 = { 27 };    // 형식 = ② ___

함수 반환 형식이나 람다 매개변수가 auto인 경우에는 ③ ___ 형식 연역 규칙이 적용되어 컴파일이 거부된다.

📝 내 답: ① int / ② vector<int> / ③ 템플릿

정답:

intstd::initializer_list<int> ③ 템플릿

해설:

auto 형식 연역은 템플릿 형식 연역과 거의 같지만, 중괄호 초기치에서만 갈라진다.

int  x1 = 27;        // int
int  x3 = { 27 };    // int

auto y1 = 27;        // int
auto y3 = { 27 };    // std::initializer_list<int>   ← 여기만 다르다
auto y4{ 27 };       // std::initializer_list<int>

auto는 중괄호 초기치를 보면 std::initializer_list를 나타낸다고 가정한다. 그래서 연역이 2단계로 진행된다.

  1. autostd::initializer_list로 연역
  2. std::initializer_list<T>T템플릿 형식 연역으로 결정

원소 타입이 하나로 정해지지 않으면 2단계에서 실패한다.

auto x5 = { 1, 2, 3.0 };   // ⚠️ 오류. T를 int로도 double로도 정할 수 없음
auto x  = { 11, 23, 9 };   // ✅ std::initializer_list<int>

반면 템플릿 형식 연역은 그런 가정을 하지 않는다. 위 1단계가 통째로 없고 2단계만 있다.

template<typename T>
void f(T param);

f({ 11, 23, 9 });            // ⚠️ 오류. T를 연역할 수 없음

template<typename T>
void f(std::initializer_list<T> initList);   // 이렇게 명시해야

f({ 11, 23, 9 });            // ✅ T = int

함정은 여기다. auto라고 다 auto 형식 연역이 적용되는 게 아니다. 함수 반환 형식과 람다 매개변수의 auto에는 템플릿 형식 연역 규칙이 적용된다.

🔍 피드백: 빈칸 3개를 각 3.33점으로 배분해 ①③을 맞혀 6.7점입니다.

③ “템플릿”을 맞히신 게 좋습니다. 이 항목에서 제일 헷갈리는 부분인데, auto라고 다 auto 규칙이 적용되는 게 아니라는 걸 잡고 계십니다.

② 가 vector<int>가 아니라 std::initializer_list<int>입니다. { 27 }은 중괄호 초기치일 뿐 vector와는 무관합니다. vector가 되려면 auto y3 = std::vector<int>{ 27 };처럼 타입을 직접 써야 합니다.

기억을 붙잡는 고리를 드리면 — ③에서 답하신 “템플릿 형식 연역”이 왜 거부되는지를 거꾸로 생각해보세요. 거부되는 이유가 바로 “템플릿 규칙에는 std::initializer_list로 가정하는 단계가 없어서” 입니다. 즉 ②의 답이 ③의 이유 안에 들어 있습니다. auto만이 중괄호를 보고 initializer_list라고 가정한다 — 이 한 문장이 ②③을 함께 묶어줍니다.

출처: game_dev/moderncpp/1/2026-04-03-moderncpp_1_2.md


Q4. 🔴 어려움 — 🔺 4/10

문제: 부모 CanCombineWith가 이미 MaxSpeed를 비교하는데도 Sprint/ADS 플래그 비교 오버라이드가 필요한 이유와, 없을 때 발생하는 문제를 설명하시오.

float UEPCharacterMovement::GetMaxSpeed() const
{
    if (bWantsToSprint && IsMovingOnGround()) return SprintSpeed;   // 650
    if (bWantsToAim) return AimSpeed;                               // 200
    return Super::GetMaxSpeed();                                    // 600
}
// FSavedMove_Character::CanCombineWith
if (!FMath::IsNearlyEqual(MaxSpeed, NewMove->MaxSpeed, MaxSpeedThresholdCombine))
{
    return false;
}

📝 내 답:

(1) Sprint/Aim 같은 플래그는 합쳐질 수 없기 때문이다. 하나만 달라도 합치는게 불가능하다. 예를 들어 합쳐지는 범위가 0.5s라 치자. 0s에 sprint==1, 0.3s에 sprint==0가 되면 합칠 수 없다.

(2) 플래그가 무시되는 상황이 발생한다.

  1. 합쳐지는 범위는 0.5s
  2. 0s 걷기
  3. 0.1s 달리기
  4. 0.3s 걷기

이 경우 달리기가 취소된다.

정답:

(1) 오버라이드가 필요한 이유 — 공중

지상에서는 부모 검사만으로 충분하다. Sprint를 켜면 GetMaxSpeed()가 650, 끄면 600을 돌려주므로 MaxSpeed가 달라지고, 부모가 두 Move를 합치지 않는다. 여기까지만 보면 우리 오버라이드는 중복처럼 보인다.

문제는 공중이다.

if (bWantsToSprint && IsMovingOnGround()) return SprintSpeed;
//                    ^^^^^^^^^^^^^^^^^^ 공중이면 거짓

IsMovingOnGround() 조건은 원래 공중에서 Sprint 속도로 활공하는 것을 막으려고 넣은 것이다. 그런데 그 부작용으로, 점프 중에는 Sprint를 켜든 끄든 GetMaxSpeed()가 같은 값을 반환한다.

그러면 SetMoveFor()가 SavedMove에 기록해둔 MaxSpeed도 두 Move가 동일해지고, 부모의 MaxSpeed 비교를 그냥 통과한다.플래그가 서로 다른 두 Move가 합쳐진다.

상황 Sprint ON/OFF 간 MaxSpeed 부모 검사만으로
지상 650 vs 600 (다름) ✅ 막힌다
공중 둘 다 같은 값 통과 → 합쳐진다

그래서 오버라이드는 “속도가 같아도 플래그가 다르면 합치지 않는다” 를 보장한다.

(2) 오버라이드가 없을 때 벌어지는 일

  1. 공중에서 Sprint 상태가 바뀐 두 Move가 하나로 합쳐진다.
  2. 합쳐진 Move의 CompressedFlags하나뿐이다. 두 프레임의 서로 다른 입력 중 한쪽만 전송된다.
  3. 서버가 UpdateFromCompressedFlags()로 복원한 Sprint 상태가 클라의 실제 입력과 달라진다.
  4. 공중에 있는 동안은 티가 나지 않는다. GetMaxSpeed()가 어차피 같은 값을 주기 때문이다.
  5. 착지하는 순간 IsMovingOnGround()가 참이 되면서 Sprint 플래그가 속도에 반영되기 시작하고, 서버와 클라의 MaxSpeed가 갈린다.
  6. 위치가 어긋나므로 서버가 보정(correction) 을 보내고, 클라 캐릭터가 스냅된다.

핵심GetMaxSpeed()클라 예측 · 서버 시뮬레이션 · 보정 리플레이 세 경로에서 반드시 같은 답을 내야 하는 함수라는 것이다. 셋 중 하나라도 다른 답을 내면 그게 곧 위치 불일치이고 스냅이다.

참고 — 여기서 말하는 “결합(combine)”이란

흔한 오해가 “N프레임을 하나로 뭉친다”인데, 엔진이 실제로 하는 건 보류 중인 PendingMove 하나와 방금 만든 NewMove 하나, 두 개짜리 쌍 검사다. 게다가 누적 0.125초(MaxMoveDeltaTime) 상한이 있다. 즉 “10프레임을 1개로”가 아니라 “보내기를 한 프레임 미뤘다가 합쳐 보내는 것을 최대 0.125초까지 반복” 이다.

🔍 피드백: 두 소문항 5점씩으로 채점했습니다.

  • (1) 1.5 / 5 — 답이 순환하고 있습니다. “플래그가 다르면 합칠 수 없기 때문”은 오버라이드가 무엇을 하는지를 그대로 옮긴 것이지, 문제가 물은 왜 그게 필요한지가 아닙니다. 질문의 핵심은 “부모가 이미 MaxSpeed를 비교하는데 왜 또 검사하느냐” 였습니다.

    답은 공중입니다. 문제에 제시된 코드에서 유독 눈에 띄는 조건이 하나 있었습니다.

    if (bWantsToSprint && IsMovingOnGround()) return SprintSpeed;
    //                    ^^^^^^^^^^^^^^^^^^ 이게 왜 여기 있을까?
    

    점프 중에는 이 조건이 거짓이라 Sprint를 켜든 끄든 같은 속도가 나옵니다. 그러면 부모의 MaxSpeed 비교는 “둘이 같네” 하고 통과시키고, 플래그가 다른 두 Move가 합쳐집니다. 지상에서는 650 vs 600으로 갈리니 부모만으로 충분하고요.

    즉 이 오버라이드는 공중에서만 의미가 있습니다. 앞으로 “왜 이 코드가 필요한가”를 물으면, 그게 없을 때 통과해버리는 구체적 상황을 찾아보세요.

  • (2) 2.5 / 5사슬의 앞부분은 정확합니다. “합쳐지면 플래그가 무시된다 → 달리기가 취소된다”는 2~3단계를 제대로 짚으신 것입니다. 시나리오를 시간순으로 적어 설명하신 방식도 좋습니다.

    빠진 건 결말입니다. 문제가 “순서대로”라고 물은 건 마지막까지 가라는 뜻이었습니다. 서버 상태가 클라와 달라진 다음에 무슨 일이 생길까요?

    공중에서는 티가 안 남 (어차피 속도 같음)
          ↓
    착지 순간 IsMovingOnGround()가 참 → Sprint가 속도에 반영되기 시작
          ↓
    서버와 클라의 MaxSpeed가 갈림 → 위치 불일치
          ↓
    서버가 보정을 보냄 → 캐릭터가 스냅
    

    “착지하는 순간 스냅이 난다” 가 이 사슬의 끝입니다. 눈에 보이는 증상까지 적으면 답이 완결됩니다.

    사소한 것 하나 — 예로 드신 0.5초는 실제로 0.125초(MaxMoveDeltaTime)입니다. “라 치자”라고 가정하셨으니 감점하지 않았습니다.

출처: game_dev/devlog/2026-02-10-EP_Replication-1.md


Q5. 🔴 어려움 — 🔺 7/10

문제: 다음 명제가 참(O)인지 거짓(X)인지 판단하라.

typedef를 구조체로 감싸면 템플릿화할 수 있으므로, 클래스 템플릿 안에서 MyAllocList<T>::type list;처럼 쓸 수 있으며 별칭 템플릿을 쓰는 것과 사용법상 차이가 없다.

📝 내 답: X — “아니었던 것 같긴한데 기억이 잘 안난다.”

정답: X

해설:

템플릿화 편법이 가능하다는 것까지는 맞다. 틀린 것은 “사용법상 차이가 없다” 는 마지막 결론이다.

클래스 템플릿 안에서 쓰려면 typename 접두사가 반드시 필요하다.

template<typename T>
class Widget {
private:
    MyAllocList<T>::type list;            // ❌ 컴파일 오류
    typename MyAllocList<T>::type list;   // ✅ typename이 있어야 한다
};

이유는 의존 타입(dependent type)이다.

MyAllocList<T>::type은 템플릿 매개변수 T의존하는 타입이다. 컴파일러는 T가 무엇인지 확정되기 전까지 ::type타입인지 정적 멤버 변수인지 알 수 없다. 그래서 “이건 타입이다”라고 알려주는 typename이 필요하다.

반면 별칭 템플릿은 이 문제가 없다.

template<typename T>
using MyAllocList = std::list<T, MyAlloc<T>>;

template<typename T>
class Widget {
private:
    MyAllocList<T> list;    // ✅ typename도, ::type도 필요 없다
};

MyAllocList<T>는 별칭 템플릿이므로 비의존 타입이고, 컴파일러가 이것이 타입임을 이미 안다.

  typedef 편법 별칭 템플릿 (using)
템플릿화 구조체로 감싸는 편법 필요 직접 가능
사용 형태 MyAllocList<T>::type MyAllocList<T>
클래스 템플릿 안에서 typename 필수 불필요
타입 종류 의존 타입 비의존 타입

이 차이가 실제로 값을 하는 곳이 타입 특성(type traits) 이다.

// C++11 — typedef 기반. typename과 ::type이 붙는다
typename std::remove_const<T>::type

// C++14 — 별칭 템플릿. 둘 다 사라진다
std::remove_const_t<T>

C++14의 _t 버전은 별칭 템플릿으로 이렇게 정의되어 있다.

template<class T>
using remove_const_t = typename remove_const<T>::type;

🔍 피드백: 판단(7점)은 만점, 근거(3점)는 0점입니다.

X를 고르신 건 정확합니다. “기억이 잘 안 난다”고 하셨지만, 명제가 “차이가 없다“라고 단정하는 순간 의심하신 감각이 맞았습니다. 이런 절대적 단정은 대체로 거짓입니다.

빠진 근거는 typename 한 단어입니다.

typename MyAllocList<T>::type list;   // typedef 편법 — typename 필수
MyAllocList<T> list;                  // 별칭 템플릿 — 그냥 쓰면 됨

이유는 의존 타입입니다. MyAllocList<T>::typeT에 의존하므로, 컴파일러는 T가 정해지기 전까지 ::type이 타입인지 정적 멤버 변수인지 모릅니다. 그래서 “이건 타입이야”라고 알려주는 typename이 필요합니다. 별칭 템플릿의 MyAllocList<T>비의존 타입이라 그럴 일이 없습니다.

가장 실용적인 기억법은 C++14의 _t 입니다. 매일 쓰는 std::remove_const_t<T>가 왜 typename std::remove_const<T>::type보다 편한지 떠올려보세요. 그 편함의 정체가 바로 별칭 템플릿이고, 사라진 두 가지가 typename::type입니다.

출처: game_dev/moderncpp/3/2026-04-18-moderncpp_3_9.md


📌 복습 포인트

  • Q1 자기대입w = w, a[i] = a[j], *px = *py. delete pb*rhs.pb를 읽으면 지운 메모리를 읽는다 → game_dev/cpp/chapter2/2025-06-07-cpp_2_11.md
  • Q1 일치성 테스트의 한계 — 모호해서가 아니라 예외를 못 막아서 부족하다 (this == &rhs는 정확한 주소 비교) → game_dev/cpp/chapter2/2025-06-07-cpp_2_11.md
  • Q1 문장 순서 조정pOrig 보관 → newdelete pOrig. try/catch 없이 자기대입·예외가 동시에 해결된다 → game_dev/cpp/chapter2/2025-06-07-cpp_2_11.md
  • Q3 auto와 중괄호auto y = { 27 };vector가 아니라 std::initializer_list<int>game_dev/moderncpp/1/2026-04-03-moderncpp_1_2.md
  • Q4 CanCombineWith가 필요한 지점공중. IsMovingOnGround() 때문에 Sprint on/off가 같은 속도가 되어 부모 검사를 통과한다 → game_dev/devlog/2026-02-10-EP_Replication-1.md
  • Q4 사슬의 끝 — 합쳐짐 → 플래그 유실 → 공중엔 무증상 → 착지 순간 서버·클라 속도 불일치 → 보정 → 스냅game_dev/devlog/2026-02-10-EP_Replication-1.md
  • Q5 typename이 필요한 이유MyAllocList<T>::type의존 타입이라 타입임을 알려줘야 한다. 별칭 템플릿은 비의존 타입 → game_dev/moderncpp/3/2026-04-18-moderncpp_3_9.md

MallangQuiz 카테고리 내 다른 글 보러가기

댓글남기기