스토리텔링 개발자

[More Effective C++] 32. 미래 지향적 프로그래밍 본문

개발/More Effective C++

[More Effective C++] 32. 미래 지향적 프로그래밍

김디트 2024. 10. 16. 11:29
728x90

항목 32. 미래 지향적인 프로그래머가 되자

 

 

 

미래 지향적인 프로그래밍
  • 사용하고 있는 라이브러리에 함수가 새로 추가될 수 있으며
    • 오버로딩도 추가로 이루어질 수 있다는 사실을 인식하고,
    • 이 사실로 인해 모호한 함수 호출이 있을 수 있다는 점을 조심하자.
  • 기존의 클래스 계통 구조에 새 클래스가 추가될 수 있고,
    • 오늘의 파생 클래스가 내일의 기본 클래스가 될 수 있다는 사실을 인식하고 대비하자.
  • 새로운 애플리케이션이 만들어질 수 있고,
    • 특정 함수의 호출환경이 달라질 수 있다는 사실을 기억하고,
    • 이런 함수가 어느 경우에서든 제대로 동작하도록 만들자.
  • 소프트웨어 유지 보수를 만든 사람이 맡지 않는 경우가 다반사이므로,
    • 다른 사람이 이해하고, 수정하고, 개선하는 데 큰 문제를 일으키지 않는 방법으로 설계하자.

 

 

 

주석이나 문서화 대신 C++의 특징을 사용하자.
  • 예컨대 어떤 클래스가 절대로 파생되지 않는 클래스라면
    • 그 클래스에 대한 헤더 파일에 주석문을 넣기보다는
    • C++ 특성을 써서 아예 파생을 못하게 막아버리자.(항목 26 참조)
  • 어떤 클래스는 반드시 힙에 인스턴스가 생성되어야 한다면,
    • 사용자에게 문서로 알리지 말고
    • 진짜로 힙에만 만들어지도록 하는 장치를 만들자.(항목 27 참조)

 

 

 

사용해 줄 사람을 기다리는(demand-paged) 가상함수를 만들지 말자.
  • 필요한 게 아니라면 멀쩡한 함수를 가상 함수로 만들지 말자.
  • 항상 그 변화는 전체 클래스와 그 클래스가 나타나는 추상화의 한도 내에서 의미를 갖도록 하자.

 

 

 

대입과 복사 생성은 모든 클래스에 대해 처리해 두자.
  • 복사나 생성을 사용하는 사람이 없을 것 같아도 그렇게 해야 한다.
    • 나중에는 언젠가 사용될 수가 있기 때문이다.
  • 복사나 생성 처리가 까다로우면 복사 생성자, 대입 연산자를 private 선언 하자.

 

 

 

황당함 최소화의 원칙(the principle of least astonishment)
  • 어떤 클래스의 연산자와 함수는, 다른 사람들이 자연스럽게 사용할 수 있는 문법과 직관적인 의미구조를 갖도록 하자.
  • C++의 기본 제공 타입과 동일한 동작원리를 갖도록 하자.
    • 아리송하면 int의 동작원리대로 만들어라.(when in doubt, do as the ints do.)

 

 

 

누구라도 일으킬 수 있는 일은 반드시 터진다.
  • 누구라도 일으킬 수 있는 일이란?
    • 예외를 일으킬 수 있다.
    • 그 객체가 자기 자신을 대입할 수 있다.
    • 객체에 값을 주기도 전에 그 객체를 사용할 수 있다.
    • 객체에 값을 주고 나서 사용하지 않기도 한다.
    • 터무니 없이 큰 값을 줄 수도 있다.
    • 널 값이 주어진다.
  • 즉, 컴파일러가 별 불평을 하지 않는 동작은 누군가가 반드시 하게 된다.
  • C++ 클래스를 만들 때에는, 맞게 사용하기에는 쉽고 틀리게 사용하기에는 어렵도록 만들어야 한다.(항목 33 참조)

 

 

 

코드의 이식성
  • 이식이 안 되는 코드보다 이식이 잘 되는 코드를 작성하기가 훨씬 어렵다.
  • 이식이 안 되는 코드를 꼭 써야 할 정도로 수행 성능의 차이가 현격하게 나는 경우도 드물다.(항목 16 참조)
  • 심지어 특수 목적으로 제작된 프로그램도, 시간이 어지간히 흐른 뒤에는 이식되는 것이 보통이다.
  • 이식성을 갖춘 코드를 만들면
    • 플랫폼이 바뀌더라도 사용할 수 있고,
    • 여러분의 코드를 사용하는 사용자 기반도 많이 확보할 수 있으며,
    • 개방형 시스템을 지원한다는 장점을 누릴 수 있다.
    • 주력 운영체제가 망해도 얼마든지 다른 운영체제로 갈 수 있다.

 

 

 

변경이 필요할 때 그 변경의 영향이 제한된 부분에만 미치도록 할 것
  • 힘 닿는 데까지 캡슐화를 하고, 구현에 관련된 상세한 부분은 외부에 노출시키지 말 것.
    • 사용하는 개발 도구가 지원하는 기능에 맞추어 이름 없는 네임스페이스를 사용하든지,
    • 파일 범위(file-scope)의 정적 변수(객체)나 정적 함수를 선언하도록 하자.(항목 31 참조)
  • 가상 기본 클래스가 필요한 설계는 피하는 것이 좋다.
    • 이 클래스를 초기화할 수 있는 클래스는 파생 클래스 뿐이기 때문이다.(항목 4 참조)
  • if-then-else 문을 줄줄이 늘어놓는 RTTI 기반의 설계도 피하자.(항목 31 참조)
    • 클래스 계통 구조가 바뀔 때마다 유지보수를 해줘야 하는데, 컴파일러는 경고해주지 않는다.

 

 

 

현재 지향적인 사고(present-tense thinking)
  • B*가 실제로 D를 가리키는 경우에만 B에 대한 가상 소멸자가 필요하다?
    • 아래와 같을 경우에는 B에 가상 소멸자를 둘 필요가 없다는 의미이다.
    • class B { ... };
      class D : public B { ... };
      B* pb = new D;
    • 하지만, 다음과 같은 문장이 추가되면 상황이 바뀐다.
    • delete pb; // 이제는 B에 가상 소멸자가 필요하다.
    • 사용자 코드가 약간이라도 바뀌면 정의 코드를 바꿔야 한다는 건 어불성설이다.
  • 기본 클래스에 가상 소멸자가 들어 있지 않으면, 파생 클래스나 파생 클래스의 멤버에는 (가상) 소멸자가 들어가선 안된다?
    • 다음의 코드는 별 문제가 없지만,
    • class string
      {
      public:
          ~string(); // 소멸자 처리가 필요한 객체
      };
      
      class B { ... }; // 가상 소멸자도 필요 없고, 소멸자를 가진 멤버도 필요 없다.
    • 아래처럼 B가 파생되어 새로운 클래스가 만들어지면 가상 소멸자가 필요하다는 말이다.
    • class D : public B
      {
          string name; // 이제 string 소멸자 처리를 위해 ~B는 가상 소멸자여야 한다.
      };
    • B 사용 방식에 약간의 변화가 생겼을 뿐인데 엄청난 코드 변화가 일어난다.
    • 기본 클래스에 애초에 가상 소멸자를 넣어두었다면 이야기할 필요 없는 이슈이다.
  • 다중 상속이 이루어진 클래스 계통 구조에 소멸자가 한 곳이라도 끼어 있으면, 모든 기본 클래스에는 가상 소멸자가 들어가야 한다?
    • 미래 지향적인 사고 방식상으론, 하위에 소멸자가 하나도 없더라도 기본 클래스는 가상 소멸자가 필요하다.
  • 즉 위의 의견들은 아래와 같은 사고 방식이다.
    • '지금' 포인터 조작을 어떻게 하고 있는가?
    • '지금' 어떤 클래스 멤버에 소멸자가 들어 있는가?
    • '지금'은 클래스 계통 구조 안의 어떤 클래스에 소멸자가 들어 있는가?
  • 상용 라이브러리 중 어떤 문자열 클래스는 가상 소멸자가 들어있지 않았다.
    • 아래는 제작자의 설명.
    • String 클래스의 소멸자를 가상 소멸자로 만들지 않은 이유는, vtbl을 추가하고 싶지 않았기 때문입니다. String*을 사용하는 경우를 애초부터 가정하지 않았기 때문에, 이 부분은 별 문제가 아닙니다. 본사는 String*을 썼을 경우에 머리 아픈 문제가 생길 수 있다는 점을 알고 있습니다.
    • 가상 함수 테이블이 있느냐 없느냐에 따라 20% 정도의 수행 성능 차이를 보이므로 제작자의 의도가 이해가 가지 않는 것은 아니다.(항목 16 참조)
    • 하지만 String 클래스를 사용하는 사용자의 수준은 천차만별이며 어떻게 사용할지 가늠할 수 없다는 문제가 있다.
    • 그러므로 C++ 자체 기능으로 파생을 하지 못하게 막았다면 좋았을 것이다.(항목 26 참조)
    • auto_ptr<String> ps(String::makeString("Future tense C++"));
      // ps를 String 객체의 포인터로 다루지만,
      // 이제 이 객체의 삭제에 대해 걱정하지 않아도 된다.
    • 물론 기존 사용법보단 불편하지만, 위험을 줄일 수 있다면 가치가 있다.
  • 현재 지향적인 사고가 틀려먹었다는 뜻은 아니다.
    • 지금 지원하는 하드웨어와 플랫폼을 고려하여 구현해야 하기 때문이다.
    • 컴파일러가 최신 기능을 구현해줄 때까지 손놓고 있을 수는 없다.

 

 

 

정리 : 미래 지향적인 사고란
  • 클래스는 완벽하게 만든다. 어떤 부분은 지금 사용되지 않아도 완벽하게 만든다.
  • 인터페이스를 설계할 때에는 자주 사용하는 기능을 모두 포함하고, 자주 일어나는 실수는 막는다.
    • 맞게 사용하기는 쉽고, 틀리게 사용하기는 어렵게 만들 것.
  • 코드를 일반화하더라도 별 손실이 없는 경우라면, 코드는 일반화한다.
    • 예를 들어, 트리 횡단(tree traversal)에 필요한 할고리즘은 방향성 비순환 그래프(directed acyclic graph)에 쓸 수 있도록 일반화한다.
728x90
Comments