[말랑 퀴즈] 26/08/22 해답
카테고리: MallangQuiz
태그: Quiz
제 말랑말랑 퀴즈 생성기는 이곳에서 확인하실 수 있습니다.
말랑말랑 퀴즈 — 해답지 ✅
날짜: 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 = w나 a[i] = a[j]처럼 *this와 rhs가 같은 객체일 때 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)
*this와 rhs가 같은 객체이면 pb와 rhs.pb도 같은 포인터다. 그런데 첫 줄에서 delete pb를 해버렸으므로, 다음 줄의 *rhs.pb는 이미 삭제된 메모리를 읽는다. pb는 dangling 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처럼*this와rhs가 같은 객체이면pb와rhs.pb가 같은 포인터이고,delete pb다음 줄의*rhs.pb는 방금 지운 메모리를 읽습니다.대신 적으신 “
delete pb;에서 예외”는 실제 문제가 아닙니다. 소멸자는 예외를 던지지 않아야 하고(항목 8), 설령 던진다 해도 첫 줄이라 아직 잃을 게 없습니다.용어도 하나 바로잡습니다.
new가 실패했을 때 생기는 건 메모리 누수가 아니라 dangling pointer입니다.delete는 이미 됐으니 새는 메모리는 없고,pb가 해제된 주소를 가리킨 채 남는 것이 문제입니다.다만 “기존
pb를 백업해두지 않은 상태에서 삭제하고 대입한다” 는 진단은 정확합니다. 그게 바로 (3)의 해법이 고치는 지점입니다. -
(2) 0 / 3.33 —
this == &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는 원래 값 그대로라서 자동으로 안전합니다. 예외 안전성은 잡아서 되돌리는 게 아니라 던져도 손해가 없도록 순서를 짜는 것이 더 좋은 접근입니다.작성하신 코드에는 타입 오류도 있습니다.
pb는Bitmap*인데Bitmap origin = pb;로 받으셨고,Bitmap value = rhs;의rhs는Widget입니다. 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가 단순 가상 함수인 상태에서, 비행 방식이 다른 ModelC가 fly 재정의를 빼먹었다. 이 상황에 대한 설명으로 가장 올바른 것은?
- A.
ModelC가fly를 재정의하지 않았으므로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); } // 명시적으로 선택 };이제
ModelC가fly를 빼먹으면 추상 클래스라 인스턴스를 만들 수 없어 컴파일 단계에서 걸린다. 다만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> / ③ 템플릿
정답:
① int
② std::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단계로 진행된다.
auto→std::initializer_list로 연역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) 플래그가 무시되는 상황이 발생한다.
- 합쳐지는 범위는 0.5s
- 0s 걷기
- 0.1s 달리기
- 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) 오버라이드가 없을 때 벌어지는 일
- 공중에서 Sprint 상태가 바뀐 두 Move가 하나로 합쳐진다.
- 합쳐진 Move의
CompressedFlags는 하나뿐이다. 두 프레임의 서로 다른 입력 중 한쪽만 전송된다. - 서버가
UpdateFromCompressedFlags()로 복원한 Sprint 상태가 클라의 실제 입력과 달라진다. - 공중에 있는 동안은 티가 나지 않는다.
GetMaxSpeed()가 어차피 같은 값을 주기 때문이다. - 착지하는 순간
IsMovingOnGround()가 참이 되면서 Sprint 플래그가 속도에 반영되기 시작하고, 서버와 클라의MaxSpeed가 갈린다. - 위치가 어긋나므로 서버가 보정(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>::type은 T에 의존하므로, 컴파일러는 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보관 →new→delete 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
댓글남기기