본문 바로가기

Effective C++

(43)
항목 44: 매개변수에 독립적인 코드는 템플릿으로부터 분리시키자 템플릿은 코딩 시간 절약, 코드 중복 회피의 두마리 토끼를 한꺼번에 잡아 준다. 하지만 아무 생각 없이 템플릿을 사용하면 코드 비대화(code bloat)가 나타날 수 있다. 이 코드 비대화를 어떻게 하면 방지할 수 있을까? 우선적으로 써 볼 수 있는 방법은, 공통성 및 가변성 분석(commonality and variability analysis)이다. 이름은 좀 거창해보이지만 사실 우리가 항상 해오던 분석 방법이다. 우리가 어떤 함수를 만들고 있다가 무심코 다른 함수를 봤는데, 지금 만들고 있는 함수 구현 중 일부가 다른 함수의 구현에도 똑같이 있다는 사실을 알아챘다고 가정해보자. 당연히 우리는 두 함수로부터 공통 코드를 뽑아내고, 이것을 별도의 새로운 함수에 넣은 후, 이 함수를 기존의 두 함수가 ..
항목 43: 템플릿으로 만들어진 기본 클래스 안의 이름에 접근하는 방법을 알아두자. 회사에서 메세지를 전송하는 프로그램을 만들고 있다고 가정해보자. 메세지는 암호회될 수도 있고, 비가공텍스트(비암호화) 형태가 될 수도 있다. 만약 어떤 메세지가 어떤 회사로 전송될지를 "컴파일 도중"에 결정할 수 있는 충분한 정보가 있다면, 주저 없이 템플릿 기반의 방법을 쓸 수 있을 것이다. 여기에 덧붙여서 메세지를 보낼 때마다 관련 정보를 로그로 남기고 싶다. 이럴땐 파생 클래스를 사용하면 쉽게 이 기능을 붙일 수 있다. 이 파생 클래스를 자세히 보면, 메시지 전송 함수의 이름(sendClearMsg)이 기본 클래스에 있는 것(sendClear)과 다르다는 것을 알 수 있다. 이러한 설계는 다음과 같은 장점을 가진다. 기본 클래스로부터 물려받은 이름을 파생 클래스에서 가리는 문제(항목 33 참고) 하..
항목 42: typename의 두 가지 의미를 제대로 파악하자 질문: 아래의 두 템플릿 선언문에 쓰인 class와 typename의 차이점이 뭘까요? 답변 : 차이가 없다! 템플릿의 타입 매개변수를 선언할 때는 class와 typename이 완전히 똑같다. 하지만 언제까지나 class와 typename이 똑같으면 이 둘을 구분할 이유가 없다. 이 둘이 다를 때가 언제인지 알아볼려면, 일단 템플릿 안에서 우리가 참조할 수 있는 이름의 종류가 2가지라는 것부터 알아야 한다. 함수 템플릿이 하나 있다고 가정하자. 이 템플릿은 STL과 호환되는 컨테이너를 받아드리도록 만들었고, 이 컨테이너에 담기는 객체는 int에 대입할 수 있다. 이 템플릿이 하는 일은 컨테이너에 담긴 원소들 중 두번째 것의 값을 출력하는 것이다. 여기서 지역변수 iter와 value에 강조 표시를 한다..
항목 41: 템플릿 프로그래밍의 천릿길도 암시적 인터페이스와 컴파일 타입 다형성부터 객체 지향 프로그래밍의 핵심 축은 다음과 같다. 명시적 인터페이스(explicit interface) 런타임 다형성(runtime polymorphism) 예를 들어 아래의 클래스와, 다음의 함수가 있다고 가정하면, doProcessing() 안에 있는 w에 대해 말할 수 있는 부분은 다음과 같다. w는 Widget 타입으로 선언되었기 때문에, w는 Widget 인터페이스를 지원해야 한다. Widget의 맴버 함수 중 몇 개는 가상 함수이므로, 이 가상 함수에 대한 호출은 런타임 다형성에 의해 이루어진다. 하지만 템플릿과 일반화 프로그래밍의 세계에서는 뿌리부터 뭔가 다른 부분이 있다. 명시적 인터페이스 및 런타임 다형성은 그대로 존재하지만 중요도가 떨어진다. 이 세계에서는 암시적 인터페이스(implici..
항목 40: 다중 상속은 심사숙고해서 사용하자 C++에서 다중 상속(multiple inheritance: MI)에 관한 견해를 살펴보면, 크게 두 가지 진영으로 나뉜다. 단일 상속(single inheritance: SI)이 좋다면 다중 상속은 더 좋을 것이다. 단일 상속은 좋지만 다중 상속은 골칫거리밖에 안된다. 일단 이 두 가지 견해가 어떤 이야기인지 살펴보자. '다중 상속'하면 바로 떠올라야 하는 사실 중 하나는, 둘 이상의 기본 클래스로부터 똑같은 이름(이를테면 함수, typedef 등)을 물려받을 가능성이 생긴다는 점이다. 즉 다중 상속으로 인해 모호성이 생긴다는 것이다. 여기서 잘 보면, checkOut() 호출부에서 모호성이 발생하고 있는데, 사실 두 checkOut() 중에서 파생 클래스가 접근할 수 있는 함수가 딱 정해져 있다.(E..
항목 39: private 상속은 심사숙고해서 구사하자 C++은 public 상속을 is-a 관계로 나타낸다.(항목 32 참고) Student가 Person으로부터 public 상속으로 파생된 형태의 클래스 계통이 주어졌다고 가정하면, 함수 호출을 성공시키기 위해 컴파일러가 Student를 Person으로 암시적 변환을 수행하는 예제를 통해 is-a 관계를 설명했었다. 이 예제를 private 상속으로 살짝 바꿔보자. private 상속은 분명 is-a 관계를 뜻하지 않는다. 그럼 뭘까? 일단 private 상속의 동작 규칙부터 알아보자. private 상속의 동작 규칙 파생 클래스 객체(Student)를 기본 클래스(Person)으로 변환하지 않는다. 기본 클래스로부터 물려받은 맴버는 파생 클래스에서 모조리 private 맴버가 된다. 즉 기본 클래스에서 ..
항목 38: "has-a(…는 ...를 가짐)" 혹은 "is-implemented-in-terms-of(…는...를 써서 구현됨)"를 모형화할 때는 객체 합성을 사용하자 합성(composition) : 어떤 타입의 객체들이 그와 다른 타입의 객체들을 포함하고 있을 경우에 성립하는 그 타입들 사이의 관계 포함된 객체들을 모아서 이들을 포함한 다른 객체들을 합성한다 라는 뜻이다. 예를 보자. Person 객체는 string, Address, PhoneNumber 객체로 이루어져 있다. 개발자들 사이에선 이러한 합성 관계를 합성(composition) 레이러링(layering) 포함(containment) 통합(aggregation) 내장(embedding) 등 다양한 이름으로 부른다 public 상속 "is-a(…는...의 일종이다.)" (항목 32 참고) 객체 합성 "has-a(...는...를 가짐)" "is-implemented-in-terms-of(…는 ...를 써서 ..
항목 37: 어떤 함수에 대해서도 상속받은 기본 매개변수 값은 절대로 재정의하지 말자 C++에서 상속받을 수 있는 함수의 종류 비가상 함수 : 절대로 재정의하면 안된다. 가상 함수 : '기본 매개변수 값을 가진 가상 함수를 상속하는 경우' 재정의하면 안된다. 왜 안될까? 가상 함수는 동적으로 바인딩(지연 바인딩,late binding)되고, 기본 매개변수 값은 정적으로 바인딩(선행 바인딩,early binding),되기 때문이다. 객체의 정적 타입(static type) : 우리가 작성한 선언문을 통해 그 객체가 갖는 타입 어떤 클래스 계통을 예로 들어보자. ps, pc, pr은 모두 Shape*로 선언되어 있기 때문에, 정적타입은 그냥 모두 Shape*이다. 하지만 이들이 진짜로 가르키는 대상은 모두 다르다. 객체의 동적 타입(dynamic type) : 현재 그 객체가 진짜로 무엇이냐..
항목 36: 상속받은 비가상 함수를 파생 클래스에 재정의하는 것은 절대 금물! D라는 이름의 클래스가 B라는 이름의 클래스로부터 public 상속에 의해 파생되었고, B 클래스에는 mf()라는 public 맴버 함수가 정의되어 있다고 가정해보자. 여기서의 pB->mf() 이 당연히 이런식으로 동작할 것이라고 예상할 수 있다. 왜? 둘 다 같은 객체인 x에 대해 mf()를 호출하고 있기 때문이다. 하지만 당연하지 않을 수도 있다는 것이 문제이다. 특히 mf()가 비가상 함수이고 D 클래스가 자체적으로 mf() 함수를 또 정의하고 있는 경우 아래 코드와 같은 황당한 일이 벌어진다. 이렇게 같은 객체에서 같은 함수를 호출하는데, 실제로는 다른 함수를 호출하는 이유는, 비가상 함수는 정적 바인딩(static binding)으로 묶이기 때문이다.(항목 37 참고) => 즉 pB가 가르키는 ..