C++/C++

Effective C++ 요약

Elan 2021. 10. 26. 18:34

 

1. MACRO 대신 const, enum과 inline을 사용하라

  • 매크로를 사용해선 안되는 이유
    • 디버깅 문제 : 매크로는 전처리기에 의해서 처리되기 때문에 변수 이름을 쓰지 않으면 디버깅할 수 없음
    • 스코프 문제 : 매크로는 클래스 내에서 정의되더라도 스코프를 가지지 않음
    • 에러 : 매크로 함수는 에러를 발생
  • In-클래스 초기화
    • C++11은 static 멤버 수와 static const 멤버에 대해서 In-클래스를 지원한다.
      반면에 C++11 이전 컴파일러에서는 static const 멤버만 초기화를 지원한다.
class Constants{
    const int c = 10;
    int e = 10;
    char *f = 'abc';
    static const int a = 10;
    static int b = 10; 	//C++11 이전 버전에서는 Error
};

static int Constants::a = 10;	//static 멤버는 out-클래스 초기화를 사용하도록 권장

 

  • Enum 핵
    • Enum핵은 클래스의 이점과 매크로 상수의 이점을 누리기 위한 트릭이다.
class EnumHack{
private:
   enum{ Turns = 5 };	//이놈 핵!
   int array[Turns];
};

 

매크로 함수를 inline 템플릿 함수로 바꾸기

#define MAX(a, b) ((a)>(b) ? (a) : (b))	//매크로 함수는 괄호를 덕지덕지 붙인다.

template<typename T>
inline T Max(const T& a, const T& b){
    return ((a > b) ? a : b);
}

 

 

 

 

2. 가능한 const 키워드를 사용하라

  • 맴버 함수를 가능한 const로 선언한다.

  • const를 붙여 선언하면 컴파일러가 사용상의 에러를 잡아내는 데 도움을 줍니다. const는 어떤 유효범위에 있는 객체에도 붙을 수 있으며, 함수 매개변수 및 반환 타입에도 붙을 수 있으며, 멤버 함수에도 붙을 수 있습니다.

  • 상수 멤버 함수는 2가지 이점이 있다.
    첫째, 값 수정에 의한 불필요한 에러를 막을 수 있다.
    둘째, 상수 객체와 비 상수 객체 모두 상수 멤버 함수를 호출할 수 있다.
    (반면에 비 상수 멤버 함수는 상수 객체에 의해 호출될 수 없다.)
class TextBlock{
public:
    const char& operator[](std::size_t pos) const{
        return text[pos];
    }
private:
    std::string text;
}

const TextBlock ctb("hello");
std::cout << ctb[0];
ctb[0] = 'a'; //에러

TextBlock tb("hello");
std::cout << tb[0]
tb[0] = 'a';
  • 비트수준 상수성: 어떤 객체의 멤버 함수가 객체의 정적 멤버를 제외한 데이터 멤버를 건드리지 않은 경우에만 그 멤버 함수가 'const'임을 인정함

  • 개념적인(논리적인) 상수성: 상수 멤버 함수도 객체의 일부 몇 비트를 바꿀 수 있되 사용자 측에서 알아채지 못하도록(객체의 상태에 영향을 주지 않도록) 해야 함

  • 컴파일러는 비트수준의 상수 성을 강제하고, 프로그래머는 논리적 수준의 상수성을 강조한다.
    • 컴파일러는 비트수준의 상수성을 강요하기 때문에, 멤버 함수는 데이터를 수정할 수 없지만,
      포인터가 사용 됐을 때는 수정이 가능해져버린다.
class TextBlock{
public:
    char& operator[](std::size_t pos) const{
        return ptext[pos];	//비트적 상수성
    }
 private:
     char *ptext;
};

const TextBlock ctb("hello");
char *p = &ctb;
*p = 'J';	//컴파일러는 논리적 상수성을 보장하지 않는다.

 

mutable 한정자

  • mutable은 비정적 맴버에 대해서 비트 수준의 상수성을 해제할 수 있다.
    이때 데이터 멤버는 항상 수정이 가능해진다. 
    데이터 멤버가 상수 멤버 함수에 있을 때뿐만 아니라 그 자체가 상수 객체일 경우이다.
class TextBlock{
public:
    void set(int a) const{
        mut = a;
    }
};

//a)
TextBlock tb;
tb.set(11);
//b)
const TextBlock ctb;
ctb.mut = 10;

 

 

3. 변수를 사용하기 전에 초기화되어있는지 확인하라

  • 초기화 리스트가 더 효율적이다.
    • 객체의 데이터 맴버는 초기화 리스트에 의해 생성자의 바디에 들어가기 전에 초기화 될 수 있다. 
    • 초기화 리스트가 필수적으로 사용되어야 할 경우는 다음과 같다. a) 객체가 상수 혹은 레퍼런스 타입의 객체를 포함하고 있을 때 또는 b) 데이터 멤버가 빌트인 타입이 아니고 클래스 자체가 많은 데이터를 가지고 있어 복사 할당을 피할 수 없을 때
  • 비지역 정적 멤버 초기화 순서 문제
    • 클래스 외부에서 정적 객체가 초기화될 때, 비지역 정적 객체라고 한다. 비지역 정적 객체는 파일이 컴파일될 때 정의되고 초기화된다. 비지역 정적 객체의 정의와 레퍼런스가 다른 파일 안에 위치할 때, 컴파일 순서는 레퍼런스 오류를 야기한다.
//비지역 정적 객체
class FileSystem{...};
extern FileSystem glob;	//A.h에서 선언됨
FileSystem glob("");	//A.cpp에서 정의됨

class client{	//B.cpp에서 glob가 정의됨
    client(){
        mem = glob.mem();
    }
};
//만약에 B.cpp 가 A.cpp 이전에 컴파일 되었다면, 
//glob는 초기화 문제를 야기한다.
class FileSystem{...};

//비지역 정적 객체를 지역 정적 객체로 바꾼다.
FileSystem& glob(){
    static FileSystem fs;
    return fs;
};

class client{
    //glob()이 호출되면, 비지역 정적 객체가 먼저 초기화 된다.
    client(){
        mem = glob().mem();
    }
};

 

 

4. 항상 컴파일러가 암시적으로 정의/호출한 함수를 확인하라

  • 컴파일러는 암시적으로 함수를 생성한다.
    • 컴파일러는 모호함이 없을 때, 자동적으로 암시적인 함수를 생성한다. 디폴트 복사 할당 연상은 상수 성을 위반할 수 있다. 그러므로 디폴트 복사 할당 연산자는 생성되지 않는다.
class Ambiguity{
public:
    Ambiguity(int a) : ma(a) {}
private:
    const int ma;
};

Ambiguity A(1), B(2);
B = A;	// 복사 할당 연산은 암시적으로  "B.ma = A.ma" 를 정의하지만, 컴파일러에 의해 저지된다.

컴파일러가 생성하는 암시적 함수를 막아라.

  • 객체 간의 복사가 일어나지 않기 위해서, 암시적 함수를 명시적으로 private 멤버로 만드는 것이다.
class Explicit{
private:
    Explicit(const Explicit& a);
    Explicit& operator=(const Explicit& rhs);
};

Explicit A("");
Explicit B(A);	//허용안됨
B = A;		//허용안됨

 

 

 

 

5. 생성자와 소멸자

  • 소멸자를 virtual로 해야 할 때
    • 클래스의 다형성을 유지하고 싶을 때, 소멸자를 virtual로 선언해야 한다. 다형성이란 베이스 타입의 포인터로 파생 클래스에 접근이 가능함을 의미하기 때문에, 프로그램이 베이스 포인터를 통해서 해당 파생 클래스 객체를 제거할 때, 예측 불가한 상황이 발생할 수 있다. 반대로 상속이 없다면, 소멸자를 virtual로 선언할 필요가 없다. virtual 키워드는 오버헤드를 방생시키기 때문에, 레지스터와 캐시 사이즈를 염두해야 한다.
class Base{
    ~Base(){}
    //virtual ~Base() {}
};

class Derive : public Base{
    ...
};

Base *p = new Derive();
delete p;	//에러
  • 소멸자가 예외를 던지지 않도록 해라
    • 이유는 예외가 소멸자가 객체를 제거하는 것을 방해하고, 제거되지 않은 부분이 불확실한 동작을 발생시키기 때문이다.
  • 소멸자/생성자 안에서 절대로 가상 함수를 호출하지 않도록해라.
    • 생성자가 호출되는 시점에는 객체가 완전히 생성되지 않았기 때문에, 베이스 클래스의 생성자가 완전히 실행되지 않은 시점에는 파생 클래스가 아직 존재하지 않기 때문이다.
class Polygon{
public:
    Polygon(){
        setArea();
    }
    virtual void setArea() {"set default area calc equation"}
};

class Circle : public Polygon{
public:
    Circle();
    virtual void setArea() {"set circle area calc equation"}
};

Circle A;
Output: "set default area calc equation"

 

 

6. 할당 연산자 오버 로딩

  • 항상 *this를 반환하도록 해라
    • 이 규칙은 +=, -=, *= 과 같은 유사한 연산자에도 해당된다.
Widget& operator=(const Widget& rhs){
    return *this;
}

 

  • 할당 연산자는 self-assign이 가능하도록, 예외를 던지지 않도록 만들어라.
class Widget{
private:
    int *pb;
};
//self-assign 이 불가능
Widget& operator=(const Widget& rhs){
    delete pb;
    pb = new int(*rhs.pb);
    return *this;
}

Widget A();
A = A;

//예외가 발생하면 원본 객체는 dangling이 된다.
Widget& operator=(const Widget& rhs){
    if(this == &rhs){
        return *this;
    }
    delete pb;
    pb = new int(*rhs.pb);
    return *this;
}

//안전한 방법
Widget& operator=(const Widget& rhs){
    Widget temp(rhs);	//data를 temp로 복사한다.
    swap(temp);		//this와 temp의 데이터를 바꾼다.
    return *this;
}

A = B;	//A의 낡은 데이터는 연산이 끝나면 제거된다.

 

 

7. 객체의 모든 것을 복사하라 

  • 파생 클래스를 위한 복사 함수를 작성할 때, 베이스 클래스로부터 상속받은 데이터도 복사하도록 한다.
class Derived : public Base{
private:
    int mb;
};
//부적절한 예 : 베이스 클래스로부터 상속된 데이터가 복사되지 않았다.
Derived(const Derived& rhs) : mb(rhs.mb){}
Derived& operator=(const Derived &rhs){
    mb = rhs.mb;
    return *this;
}
//올바른 예
Derived(const Derived& rhs) : Base(rhs), mb(rhs.mb) {}
Derived& operator=(const Derived& rhs){
    Base::operator=(rhs);
    mb = rhs.mb;
    return *this;
}

 

 

 

8. RAII 원칙을 지키고 수동으로 리소스를 릴리스하지 않는다.

  • 빌트인 RAII 객체
    • C++ 은 auto_ptr <T>과 shared_ptr <T>라는 두 가지 RAII 객체를 제공한다. auto_ptr <T>는 일대일 대응이지만, 복사 연산이 일어나면, 소유권 이전이 일어난다. 그러나 shared_ptr <T>는 복수의 포인터가 하나의  값과 대응될 수 있다. 그리고 리소스는 더 이상 포인터가 없을 때 릴리즈 된다.
class Widget{...};
auto_ptr<Widget> prt1(new Widget());
auto_ptr<Widget> ptr2;
ptr2 = ptr1;	//복사 연산이 일어난 후에 ptr1 == NULL 이 된다.

shared_ptr<Widget> ptr3(new Widget());
shared_ptr<Widget> ptr4;
ptr4 = ptr3;

 

  • 단일 객체에 대해서만 RAII를 사용한다.
    • auto_ptr <T>와 shared_ptr <T>는 delete [] 와는 다른 자체 소멸자를 가지고 있다.
//나쁜 예
auto_ptr<string> aps(new string[10]);
shared_ptr<int> spi(new int[1024]);

 

  • new로 생성한 객체를 스마트 포인터에 저장하는 코드는 별도의 코드로 만든다.
    • 매개변수 실행 순서는 컴파일러마다 다르기 때문에, new와 스마트 포인터를 포함한 연산은 메모리 누수에 취약하다.
class Widget{...};
int prioity() {...}

//나쁜 예
processWidget(auto_ptr<Widget>(new Widget), priority());

//좋은 예
auto_ptr<Widget> ptr = new Widget();
processWidget(ptr, priority());

 

  • 원시 자원에 대한 명시적/암시적 접근
class Raw{...};

class RAII{
private:
    Raw resource;
};

//명시적 인터페이스 변환
Raw RAII:get() { return resource; }

//연산자를 통한 암시적 접근 : T() 연산의 반환값은 T가 된다.
RAII::operator Raw() const { return resource; }

RAII pt();
Raw bpt = pt.get();	//명시적 접근
Raw bpt = pt;		//타입 변환을 통한 암시적 접근

 

 

 

 

9. 리소스를 할당할 때는 new와 delete 키워드를 사용한다.

  • new <=> delete, new [] <=> delete []와 같이 서로 매칭 되는 키워드를 사용한다.
  • delete []가 실행될 때, array size(n)을 먼저 인식하고, 그에 해당하는 소멸자가 호출된다.

 

 

 

 

10. 클래스 설계를 타입 설계와 똑같이 취급한다.

  • 새로운 타입의 객체가 어떻게 생성되고 소멸되는가? (메모리 할당과, 생성자와 소멸자를 어떻게 설계하고 있는가?)
  • 객체 초기화와 객체 할당을 어떻게 다른가?(생성자가 호출되는가? 혹은 할당 연산자가 호출되는가?)
  • 값에 의한 전달은 무엇을 의미하는가?(복사 생성자를 어떻게 설계했는가?)
  • 합적적인 값을 정하는 규정은 무엇인가?(예외 처리를 어떻게 하고 있는가?)
  • 새로운 타입이 이미 존재하는 상속 구조에 적용될 수 있는가?(어떤 함수가 virtual로 선언되고 있는가?)
  • 어떤 종류의 타입 변환을 허용하고 있는가?(암시적/묵시적 형 변환을 신경 쓰고 있는가?)
  • 현재 쓰고 있는 연산자와 함수가 합당한가?(어떤 함수를 멤버 함수로 선언할지 결정해라)
  • 허용하지 말아야 할 표준 함수는 무엇인가?
  • 누가 멤버 변수에 접근할 수 있는가?(클래스 멤버를 public/protected/private인지 결절해라)
  • 선언되지 않은 인터페이스는 무엇인가?(퍼포먼스)
  • 새로운 타입은 얼마나 일반적인가(템플릿)
  • 새로운 타입이 정말로 필요한가?, 고작 이미 존재하는 클래스에 기능적으로 추가하고 싶을 때는, 파생 클래스가 아니라 비멤버 함수나 템플릿으로 만들어라.

 

 

 

11.  pass by value 보단 pass by reference를 선호한다.

  • const T& 을 사용해야만 하는 이유
    • 효율성 : pass by value는 함수 매개변수를 위한 일시적인 객체를 생성하기 위해 생성자 호출이 필요하고, 함수가 종료될 시점에는 소멸자를 호출한다.
    • 슬라이싱 문제 : 베이스 클래스의 생성자만 호출되고 파생 클래스의 생성자는 호출되지 않을 수 있기 때문에, 데이터 손실이 일어난다.
class Windows{
    virtual void display() {...}
};

class WindowScroll : public Windows{
    virtual void display() {...}
};

WindowScroll windObj

//베이스 클래스의 복사 생성자가 호출된다.
void printName(Windows w){
    display();
}
printName(windObj);	//베이스 클래스의 display() 가 호출된다.

void printName(const Windows& w){
    display();
}
printName(windObj);	//파생 클래스의 display() 가 호출된다.

 

  • 절대로 레퍼런스를 리턴하지 마라.
Rational a(1,2), b(3,4);

//나쁜 예 : 스코프 내의 지역변수의 레퍼런스를 반환하고 있음
const Rational& operator*(const Rational& lhs, const Rational& rhs){
    Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
    return result;
}
auto c = a * b;	//operator* 가 끝날 시점에 result는 소멸된다.

//나쁜 예 : 정적 지역 변수의 레퍼런스를 반환하고 있음
const Rational& operator*(const Rational& lhs, const Rational& rhs){
    static Rational result;
    result.n = lhs.n * rhs.n;
    result.d = lhs.d * rhs.d;
    return result;
}

auto d = a * b;
auto e = a * d;	//정적 변수가 변하면 연산의 값도 변하게 된다.

//나쁜 예 : 힙 기반 자원을 가리키는 포인터를 반환하고 있음
const Rational* operator*(const Rational& lhs, const Rational& rhs){
    Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
    return result;
}

 

 

 

 

12. 클래스 캡슐화

  • 네임스페이스는 클래스와 헬퍼 함수, 비멤버, 비-friend 함수를 관리를 도와준다. 클래스 구현을 정의하고 다른 헤더/구현 파일에서 헬퍼 함수를 그룹화하는 것은 코드를 구성하는 클라이언트 친화적인 방법이다. 여기서 핵심은 모든 관련 클래스와 함수가 동일한 네임스페이스 내에서 구성된다는 것이다.
//Header A.h : 클래스와 인터페이스를 선언한다.
namespace ClassA{
    class A{...}
}

//Header B.h : A 클래스의 헬퍼 함수를 선언한다.
namespace ClassA{
    void funA(A ma){...}
}

//Header C.h : A 클래스의 헬퍼 함수를 선언한다.
namespace ClassA{
    void funB(A ma){...}
}

 

 

13. 상속 규칙

  • public 상속은 "is-a" 개념이다 : 베이스 클래스 객체에 적용되는 것은 파생 클래스 객체에서도 적용이 가능해야 한다.
class bird{
    virtual void fly() = 0;
};

//나쁜 예 : fly 메소드가 선언되지 않음
class penguin : public bird{
    ...
};

//좋은 예
class penguin : public bird{
    virtual void fly(){
        error("penguin won't fly");
    }
};

 

  • private 상속은 "has-a" 개념이다 : 파생 클래스는 베이스 클래스의 용어로 구현된다.
    • 아래의 두 예제는 몇 가지 유사성이 있다.
      • 모든 Car 객체는 하나의 Engine 멤버 객체를 포함하고 있다.
      • 두 경우, 모두 Car* 를 Engine* 으로 변환할 수 없다.
      • 두 경우, 모두 Car 객체는 start() 함수를 통해서 Engine 객체의 start() 객체를 호출할 수 있다.
    • 아래의 두 예제는 몇가지 차이점이 있다.
      • 단순 컴포지션 변형은 Car 객체가 다수의 Engine을 포함할 때, 사용될 수 있다.
      • private 상속은 불필요한 다중상속을 야기할 수 있다.
      • private 상속은 Car 멤버가 Car* 를 Engine*로 변환하는 것을 허용한다.
      • private 상속은 베이스 클래스의 proteced 맴버에 접근 가능하게 한다.
      • private 상속은 Car 가 Engine의 가상 함수를 오버라이딩을 가능케한다.
      • private 상속은 Car 의 start() 함수를 통해서 Engine의 start() 함수를 호출하게 한다.
class Engine{
public:
    Engine(int numCylinders);
    void start();
};

//Approach 1 : 컴포지선
//베이스 타입의 맴버를 만들고, 인터페이스를 통해서 상호작용한다.
class Car{
public:
    Car() : e_(8) {}
    void start() { e_.start(); }
private:
    Engine e_;
}

//Approach 2 : private 상속
class Car : private Engine{
public:
    Car() : Engine(8) {}	//베이스 클래스의 생성자를 이용한다.
    using Engine::start;	//인터페이스 재정의 및 매개변수 전달을 축소한다.
}

Car *p1 = new Car();	//OK
Engine *p2 = new Car();	//에러

 

  • private 상속 vs. 컴포지션
    • 가능하면 컴포지션을 사용하고, 필요한 경우, 특히 protected 맴버 및 가상 함수일 경우,  private 상속을 사용한다. 그러나 private 상속은 변경할 경우가 많기 때문에 유지 관리하는데 더 많은 비용이 든다.
      • Case 1 : 베이스 클래스의 가상 함수를 오버 라이딩하는 경우
        • pulbic 상속과는 다르게, 가상 함수는 베이스 타입의 포인터를 가지고 파생 클래스의 구현을 호출하는데 유용하게 쓸 수 있다. 그러나 private 상속에서는, 파생 클래스 객체는 베이스 타입의 포인터로 나타낼 수 없다. 가상 함수는 파생 클래스가 베이스 클래스의 비가상 함수를 호출할 때, 비가상 함수가 베이스 클래스의 가상 함수를 호출할 때, 의미가 있다. 파생 클래스는 가상 함수를 비가상 함수로 오버라이드 할 수 있다.
      • Case 2 : 빈 클래스를 상속받는 경우
        • 빈 클래스에는 데이터 멤버와 정적 변수, 가상 함수가 없다. typedef 나 enum을 포함한다.
//Case 1
class B{
public:
    void display() { draw();}
private:
    virtual void draw() = 0;	//순수 가상함수
};

class D : private B{
public:
    void show() { display(); }	//베이스 클래스의 비가상 함수를 호출
private:
    virtual void draw();	//베이스 클래스의 가상함수를 오버라이드
};

D A(...);
A.show();	//B::display() 를 호출하고 D::draw() 를 호출한다.

 

 

Standard C++

 

isocpp.org

  • 이름으로만 스코프 검색
    • C++ 컴파일러는 매개변수 타입이 아닌, 오직 이름을 통해서 함수를 인식한다. 즉, 오버로드 시 스코프 계층구조를 고려해야 한다.
class Base{
public:
    virtual void mf1() = 0;
    virtual void mf1(int);
    virtual void mf2();
    virtual void mf3(double);
};

//나쁜 예 : mf1(int)는 파생클래스에 이식될 수 없다. 
class Derive : public Base{
public:
    virtual void mf1();
    void mf3();
};

Derive d;
int x;
d.mf1();	//Derive::mf1()을 호출
d.mf1(x);	//에러. 함수 오버로딩은 파생클래스 인터페이스의 mf1()에 의해 막힘
d.mf2();	//Base::mf2()를 호출
d.mf3(1.0);	//에러, Base::mf3(double) 은 Derive::mf3()에 의해 막힘

//좋은 예
class Derive : public Base{
public:
    using Base::mf1;
    using Base::md3;
    virtual void mf1();
    void mf3();
};

Derive d;
int x;
d.mf1();	//Derive::mf1() 를 호출
d.mf1(x);	//파생 클래스의 스코프에서도 접근이 가능
d.mf2();	//Base::mf2() 를 호출
d.mf3(1.0);	//파생 클래스의 스코프에서도 접근이 가능

 

  • 인터페이스에는 public 상속, 구현에는 private 상속을 조합한 다중 상속
class IPerson{
public:
    virtual ~IPerson();
    virtual std::string Name() const = 0;
    virtual std::string Birth() const = 0;
};

class PersonInfo{
public:
    virtual ~PersonInfo();
    virtual const char* theName() const{
        ...
        formatPrint();
        ...
    }
    virtual const char* theBirth() const;
private:
    virtual const char* formatPrint() const;
};

class Person : public IPerson, private PersonInfo{
public:
   virtual std::string Name(){
       PersonInfo::theName();
   }
   virtual std::string Birth() { PersonInfo::theBirth(); }
private:
    virtual const char* formatPrint() { return ""; }
};

 

14. 인터페이스 상속과 구현 상속

  • 인터페이스만 상속
class Base{
    virtual void display() = 0;	//순수 가상함수를 선언
};

class Derived : public Base{
    virtual void display() { ... }	//파생 클래스는 display()를 구현해야함, 그렇지 안으면 컴파일 에러 발생
};

Derived D;	//에러 : 가상 클래스를 인스턴스화함

 

  • 인터페이스 상속과 디폴트 구현
    • 단순 가상 함수는 인터페이스뿐만 아니라 디폴트 구현을 제공한다. 그래서 가상 함수는 다수의 파생 클래스가 공통의 특징을 상속받을 때 유용하다.
class Base{
    virtual void display() { printf("Base"); }
};

class DerivedA : public Base{
    virtual void display() { ... }
};

class DerivedB : public Base{
    ...
};

class DerivedC : public Base{
    ...
};

DerivedA A;
A.display();	//DerivedA::display() 를 호출

Derived B;
B.display();	///Base::display() 를 호출

Derived C;
C.display();	//Base::display() 를 호출
class Base{
    virtual void display(const Widget& w) = 0;	//순수 가상함수
};

void Base::display(const Widget& w){
    ...
};

class DerivedA : public Base{
    virtual void display(const Widget& w){
        Base::display(w);	//명시적으로 인터페이스 함수를 호출
    }
};

class DerivedB : public Base{
	//명시적으로 display() 를 구현한다. 그러나 암시적인 상속만 일어난다.
    virtual void display(const Widget& w){
        ...
    }
};

 

 

15.  상속받은 함수의 오버 라이딩

  • 절대로 상속받은 비가상 함수를 재정의하지 않는다.
    • 가상 함수는 함수호 출시 포인터와 레퍼런스의 타입이 아니라, 오직 객체에 타입에 의존한체 동적 바인딩된다.
    • 비가상 함수는 함수호출시 포인터와 레퍼런스의 타입에 정적 바인딩된다.
    • 파생 클래스가 비가상 함수를 오버라이드 할 경우, 동일한 객체라도 직관에 반하는 행동을 할 수 있다. 또한 파생 클래스의 함수와 베이스 클래스의 함수와 다를 때, public 상속의 is-a 규칙을 위반할 수 있다.
class B{
public:
    void mf();
};

class D : public B{
public:
    void mf();
};

D x();
//두 개의 포인터가 같은 객체를 가리키고 있음
B *pb = &x;
D *pd = &x;

//나쁜 예 : 서로 다른 행동을 하는 두 함수
pb->mf();	//B::mf();
pd->mf();	//D::mf();

 

  • 절대로 상속받은 디폴트 변수를 재정의하지 않는다.
    • 파생 클래스에서 가상 함수에 대해서 다른 디폴트 매개변수를 정의하면, 의도하지 않은 동작을 야기할 수 있다. 매개변수가 정적 바인딩되어 있기 때문에, 디폴트 매개변수는 고정되어있다. 따라서 디폴트 변수는 변하지 않는다.
class Shape{
public:
    enum Color{ Red, Black, White };
    virtual void draw(const Color& c = Red) const = 0;
};

class Rectangle : public Shape{
public:
    virtual void draw(const Color& c = Black) {...}	//새로운 디폴트 매개변수를 정의
};

Shape *p = new Rectangle();
p->draw();	//Rectangle::draw(c = Red) 가 호출됨
//단점 base 클래스의 가상함수의 매개변수가 변겯되면 모든 계층의 함수도 바꾸어야함
class Shape{
public:
    enum Color {Red, Black, White};
    virtual void draw(const Color& c = Red) const = 0;
};

class Rectangle : public Shape{
public:
    virtual void draw(const Color& c = Red) { ... }	//같은 디폴트 매개변수를 가짐
};

Shape *p = new Rectangle();
p->draw();	//Rectangle::draw(c=Red) 가 호출
class Shape{
public:
    enum Color{ Red, Black, White };
    void draw(const Color& c = Red){
        doDraw(c);
    }
private:
    virtual void doDraw(const Color& c) const = 0;
};

class Rectangle : public Shape{
private:
    virtual void draw(const Color& c) { ... }
};

Shape *p = new Rectangle();
p->draw();	//Shape::draw(red) 가 호출된다. 그러나 Rectangle::doDraw() 도 유효하다.

 

16.  가상 함수의 대안

  • 비가상 인터페이스(Non-Virtual Interface(NVI))
    • 비가상 인터페이스의 철학은 가상 함수를 private으로 숨기고, 비맴버함수를 통해서 호출하는 것이다. 이것의 이점은 사용자가 공통 코드를 베이스 클래스에 넣고, 공통 코드는 가상함수가 호출되기 전에 콘텍스트를 세팅하고, 가상함수 호출후 컨텍스트를 정리할 수 있다는 것이다.
class B{
public:
    int computeValue(const int a) const{
        ...	//컨텍스트를 설정
        doComputation(a);	//파생 클래스에서 오버라이드 가능
        ...	//컨텍스트를 정리
    }
private:
    virutal int doComputation(const int a) const {...}
};

 

  • 함수 포인터를 사용하기
    • 클래스 안에 함수 포인터를 두고, 다른 객체에 대해서 서로 다른 동작을 하게 만드는 것이다.
class B{
public:
    typedef int(*doComputation)(const int);
    explicit B(doComputation f = defaultCompute) : comFunc(f) {}	//명시으로 생성자를 사용함
private:
    doComputation compFunc;
};

//서로다른 객체에 대해서, 다른 연산을 함
B inst1(computeA);
B inst2(computeB);

 

 

17. 가능한 변수 정의를 늦춰라.

  • 코드 수정, 예외 etc에 의해서 사용하지 않는 변수가 생성될 수 있다.
std::string encrypt;

if(password > THRESHOLD){
    throw logic_error("...");
    //예외가 던져지면, encrypt는 사용되지 않으며, 쓸데없이 생성자와 소멸자를 호출하게 된다.
}

encrypt = password;

 

18. 가능한 캐스팅을 최소화하라.

 

 

19. 내부 객체에 대한 핸들을 반환하는 것을 피하라.

  • 핸들은 캡슐화와 논리적 상수 성을 파괴할 수 있다.
class A{
public:
    A(int a) : ma(a) {}
    int& get() { return ma; }
private:
    int ma;
};

//직접적인 접근은 캡슐화를 파괴하며, private 맴버를 public 으로 노출시킨다
A ta(1);
int &c = ta.get();	//사용자는 c 통해서 ma를 수정할 수 있다.
c = 2;

//논리적 상수성도 파괴할 수 있다.
const A tb(1);
int &d = tb.get();	//상수가 수정이 된다.

//솔루션은 핸들에 read-only 권한만 허용하는 것이다.
class A{
public:
    ...
    const int& get() { return ma; }
    ...
};

 

  • 핸들은 댕글링 포인터를 야기할 수 있다.
class B{
public:
    const int* get() { return &ma; }
private:
    int ma;    
};

//func(int) 는 B 타입의 인스턴스를 반환한다.
const B func(int a) { ... }

//func(1)은 임시 B 객체와 내부 핸들을 생성한다. 
//그러나 임시 객체가 사라지면,  댕글링 포인터만이 남는다.
const int* handle = func(1).get();

 

 

20. inline을 정확히 알고 사용하자.

  • 명시적/암시적 인라인
    • 인라인은 명령이 아니라 컴파일러에 대한 요청이다. 인라인은 주로 컴파일 타임(혹은 링크 타임)에서 수행되기 때문에, 인라인 함수는 컴파일러를 지원하기 위해 헤더 파일 안에 있어야 한다.
class Person{
public:
    //클래스 정의부에서 함수를 구현하는 것은 암시적 인라인 요청이다.
    int age() { return mAge; }
};

//명시적 인라인 요청
inline const in Max(int a, int b) { return a > b ? a: b; }

 

  • 인라인에 신중을 가한다.
    • 생성자와 소멸자는 인라인화는 좋은 선택이 아니다. 생성자가 비어있다고 하더라도, 컴파일러는 베이스 생성자를 호출하고 다른 복잡한 예외처리를 자동으로 한다. 빈 생성자일지라도 실제로는 내부적으로는 코드가 들어있다.
    • 인라인 함수는 수정할 경우 모든 클라이언트들이 리컴 파일 할 필요가 있어지지만, 아우트라인 함수는 자체적으로 리컴 파일/리링크 할 수 있다.

 

21. 파일 사이의 컴파일 의존성을 최소화하자.

  • 큰 시스템이서는 컴파일 의존성을 줄이는 것이 매우 중요하며, 그렇지 않을 경우, 작은 수정만으로 전체 시스템의 리컴파일을 야기 할 수 있다.

 

  • 핸들 클래스로 컴파일 의존성을 줄이기
    • "pimple implementation(pimpl idiom)", 구현 포인터는 포인터와 전방선얼을 활용한다. 클래스가 파일에서 인스턴스화 되지 않는한, C++ 컴파일러는 컴파일 할 때, 클래스 정의를 알 필요가 없어진다.
      • 1. 모든 구현과 디테일을 클라이언트에게서 숨긴다.
      • 2. 구현 클래스에대한 포인터만을 포함하는 인터페이스 클래스를 만든다. 구현 객체를 인스턴스화 하지 않고 구현 메소드를 호출하여 헤더파일을 의존성에서 제거한다.
      • 3. 인터페이스 클래스만을 클라이언트에 노출시킨다.
//PersonImpl.h
class PersonImpl{
public:
    PersonImpl(std::string, int);
    void showAge();
private:
    std::string name;
    int age;
};

//PersonImpl.cpp
#include "PersonImpl.h"
PersonImpl::PersonImpl(std::string Name, int Age) : name(Name), age(Age) {}
PersonImpl::showAge() { std::cout << age << std::endl; }
//Person.h
class PersonImpl;
class Person{
public:
    Person(std::string, int);
    void showAge();
private:
    std::shared_ptr ptr;
};

//Person.cpp
#include "PersonImpl.h"
#include "Person.h"

Person::Person(std::string Name, int Age) : ptr(new PersonImpl(Name, Age)) {}

Person::showAge(){
    ptr->showAge();
}
#include "Person.h"

int main(){
    Person A("John", 23);
    A->showAge();
    return 0;
}

 

  • 인터페이스 클래스로 의존성을 줄이기
    • 인터페이스 클래스를 사용하여 의존성을 술일 수 있다. "Person.h" 만을 인클루드한다.
//Person.h
class Person{
public:
    virutal ~Person();
    virtual void showAge() const = 0;
    virutal void showName() const = 0;
    static std::shared_ptr create(std::string, int);
};

//PersonImpl.h
#include "Person.h"
class PersonImpl : public Person{
public:
    PersonImpl(std::string, int);
    virtual ~PersonImpl();
    virtual void showAge();
    virtual void showName();
private:
    std::string Name;
    int Age;
};

//Person.cpp
#include "PersonImpl.h"
std::shared_ptr create(std::string Name, int Age){
    return std::shared_ptr(new PersonImpl(Name, Age));
}

//PersonImpl.cpp
#include "PersonImpl.h"
void showAge() { std::cout << Age << std::endl; }
void showName() { std::cout << Name << std::endl; }

//Client.cpp
#include "Person.h"
int main(){
    std::shared_ptr ptr = create("John", 23);
    ptr->showAge();
    ptr->showName();
}

 

  • 비고
    • 핸들 클래스는 포인터를 통하여 모든 것을 간접적으로 만들고, 동적 메모리 할당 오버헤드를 발생시킨다.
    • 인터페이스 클래스는 가상함수에 대한 호출을 수행할때, 간접적인 점프 비용을 요구한다.

 

22. 템플릿에서 typename 사용하기.

  • 인클래스 타입에서 typename 사용하기
template<typename T> void print(const T& obj){
    //에러 : 컴파일러는 T::const_iterator 를 타입이 아니라 데이터 멤버로 인식한다.
    T::const_iterator it(obj.begin());
    
    //옳음 : typename을 명시적으로 선언해야함
    typename T::const_iterator it(obj.begin());
    while (it != obj.end()){
        std::cout << *it << std::endl;
    }
}

 

23. 템플릿 클래스 상속하기

  • 완전 템플릿 특수화
    • 템플릿 함수/클래스 외에도 특별한 경우 템플릿을 쓸 수 있다.
//일반적인 템플릿 클래스
template<typename T>
class MsgSender{
public:
    ...	//생성자, 소멸자 ...
    void sendMsg(std::string msg){
        T c;
        c.send(msg);
    }
    void sendEncrypt(std::string);
}

//타입 Z를 위한 템플릿 클래스를 만든다.
//키워드 typename이 없는 특별한 템플릿 클래스
template<>
class MsgSender<Z>{
public:
    ...	//생성자, 소멀자 ...
    void sendMsg(std::string msg){
        Z c;
        c.send(msg);
    }
};

MsgSender<A> m;
m.sendEncrypt("msg");
MsgSender<Z> n;
m.sendMsg("msg");
m.sendEncrypt("msg");	//오류

 

  • 템플릿 클래스의 상속
    • 만얀에 베이스 클래스가 템플릿 클래스이라면, 싱속을 위해 구현을 블랙박스로 취급한다. 모든 인터페이스는 기본적으로 상속되지 않는다.
template<typename T>
class MsgSender{
public:
    ...	//생성자, 소멸자 ..
   void sendMsg(std::string msg){
       T c;
       c.send(msg);
   }
   void sendEncrypt(std::string);
};

template<typename T>
class MsgLogger : public MsgSender{
public:
    void sendSecret(std::string msg){
        sendEncrypt(msg);	//컴파일 에러 : sendEncrypt 를 찾을 수 없다.
        //템플릿 베이스 클래스의 명시적인 케이스를 찾을 수 없기 때문에, 상속된 멤버 함수를 찾는 것을 
        //포기한다.
    }
};

//Solution-1 : this 포인터를 사용
template<typename T>
class MsgLogger : public MsgSender{
public:
    void sendSecret(std::string msg){
        this->sendEncrypt(msg);
    }    
};

//Solution-2 : using 키워드를 사용
template<typename T>
class MsgLogger : public MsgSender{
public:
    using MsgSender::sendEncrypt;	//명시적 선언
    void sendSecret(std::string msg){
        sendEncrypt(msg);
    }
};

//Solution-3 : 베이스 클래스의 스코프를 사용, 부작용으로 virtual 호출을 무시함
template<typename T>
class MsgLogger : public MsgSender{
public:
    void sendSecret(std::string msg){
        MsgSender::sendEncrypt(msg);
    }
};

 

 

24. 독립된 매개변수를 템플릿에서 분리시켜라.

  • 다른 타입의 변수들이 같은 템플릿 코드를 공유할지라도, 컴파일러는 다른 타입의 매개변수를 복사한다. 예를들어 int 와 char 가 템플릿에 적용이 됬다면, 두개의 템플릿 코드의 복사본이 바이너리 타입으로 생성된다. 이것을 "암시적 코드 복제"라고 한다.

 

  • 논타입(non-type) 매개변수는 불필요한 코드 복제를 야기한다.(code bloat : 코드 부풀림)
template<typename T, size_t n>
class Matrix{
public:
    ...
    void invert();
};

 

  • 논타입(non-type) 매개변수를 파생클래스로 분화시켜라
    •  타입 매개변수는 베이스 클래스로 논타입 매개변수는 private 상속을 받는 파생 클래스로 구성한다. 각각의 타입에 대해서, 컴파일러는 베이스클래스의 복사본을 만들지만, 모든 다른 타입의 논타입 매개변수는 같은 클래스의 복사본을 공유한다. 이 상태에서는 변수 'n' 과 관계된 함수들만 복사된다. (inline으로 퍼포먼스 향상 가능) 'n'이 상수라면 더 좋은 최적화가 된다. 그러나 코드의 사이즈가 커진다.
template<typename T>
class MatrixBase{
protect:
    void invert(size_t);
};

template<typename T, size_t n>
class Matrix : private MatrixBase{
public:
    using MatrixBase<T>::invert;
    void invert() { invert(n); }	//암시적인 inline 호출
};

 

  • 논타입(non-type) 매개변수를 함수 매개변수나 데이터 멤버로 분화시켜라
    • 바로 위의 경우와 비교했을때, 높은 최적화를 기대할수 없지만, 코드 사이즈가 작다.(캐시 힛 메모리)
template<typename T>
class Matrix{
public:
    Matrix(size_t n) { _n = n; }
    void set(size_t n) { _n = n; }
    void invert();
    void invert(size_t);
private:
    size_t _n;
};

 

 

25. 템플릿의 타입 호환성/변환

  • 템플릿화된 멤버 함수의 암시적 타입 변환
    • 컴파일러가 베이스 클래스와 파생 클래스의 상속관계를 무시할 수 있기 때문에, 암시적 형변환도 무시될 수 있다.
    • 템플릿 복사 생성자를 사용하면 암시적인 타입변환을 지원하게 할 수 있다.
template<typename T>
class SmartPtr{
public:
    SmartPtr(const SmartPtr& p);
    ...
private:
    T *_ptr;
};

template<typename T>
SmartPtr<T>::SmartPtr(const SmartPtr &C) : _p(C.get()) {}

SmartPtr<Base> ptr = SmartPtr<Base>(new Base());

//에러 : SmartPtr<Base> 에서 SmartPtr<Derived> 로의 형변환이 허용되지 않음
SmartPtr<Base> ptr = SmartPtr<Derived>(new Derived());
template<typename T>
class SmartPtr{
public:   
    template<typename U>
    SmartPtr(const SmartPtr<U> &C);
    
    T* get() const { return _ptr; }
    ...
private:
    T *_ptr;
};

template<typename T>
template<typename U>
SmartPtr<T>::SmartPtr(const SmartPtr<U> &C) : _ptr(C.get()) {}

//U = T = Base
Smart<Base> p1 = SmartPtr<Base>(new Base());

//T = Base, U = Derived 이므로 Derived* => Base* 가 허용됨
Smart<Base> p2 = SmartPtr<Derived>(new Derived());

//에러 T = Derived, U = Base 이므로, Base* => Derived* 가 허용됨
SmartPtr<Derived> p3 = SmartPtr<Base>(new Base());

 

  • 암시적 형변환을 지원하는 함수를 정의한다.
    • 비템플릿 클래스에서는, 클래스 안에서만 함수/연산자를 정의하는 것만으로 충분하지 않을 수 있다.
    • 그러나 템플릿화 된 클래스에서는 , 비멤버 함수 솔루션이 먹히지 않을 수 있다.
class Rational{
public:
    Rational(int numerator = 0, int denominator = 1);
    const Rational operator*(const Rational &rhs) const;
private:
    int _numerator;
    int _denominator;
};

Rational half(1, 2);
Rational r0 = half * 2;	//암시적 생성자가 호출되며, 2는 Rational(2)로 변환된다.
Rational r1 = 2 * half;	//오류 : int(2) * 는 Rational 타입으로 형변환될 수 없다.

//Solution : 연산자를 비멤버함수로 정의한다.
const Rational operator*(const Rational &lhs, const Rational &rhs) { ... }
Rational r1 = 2 * half;
template<typename T>
class Rational{
public:
    Rational(T numerator = 0, T denominator = 1);
    const Rational operator*(const Rational &rhs) const;
private:
    T _numerator;
    T _denominator;
};

//Problem : 단순히 비멤버 함수로 정의하는 것으로 먹히지 않는다.
template<typename T>
const Rational<T> operator*(const Rational<T> &lhs, const Rational<T> &rhs) { ... }

Rational half(1, 2);
Rational r0 = half * 2;	// 에러

//Solution : 비멤버 friend 함수
class Rational{
public:
    friend const Rational operator*(const Rational &lhs, const Rational &rhs) { ... }
    ...
};

Rational half(1, 2);
Rational r0 = harlf * 2;

 

참고 사이트

https://tainaandme.tistory.com/entry/Effective-C-%EC%9A%94%EC%95%BD

http://ajwmain.iptime.org/programming/book_summary/%5B00%5Deffective_cpp/effective_cpp.html#I03