Effective C++ Today I Learn (Day1)
C++를 언어들의 연합체로 바라보는 안목은 필수
- C++는 다중패러다임 프로그래밍 언어로 불립니다.
- 절차적(procedural) 프로그래밍을 기본으로 하여
- 객체 지향(object-oriented)
- 함수식(functional)
- 일반화(generic)
- 메타프로그래밍(metaprogramming)
- C++는 한가지 프로그래밍 규칙 아래 있는 통합 언어가 아니라, 4개의 하위언어(sublanguage)를 제공하는 연합체로 구성되어 있습니다.
- C : C++는 여전히 C를 기본으로 하고 있습니다.
- 객체 지향 개념의 C++ : 클래스(생성자와 소멸자 개념), 캡슐화, 상속, 다형성, 가상 함수(동적 바인딩)
- 템플릿 C++ : C++의 일반화 프로그래밍. 템플릿 메타프로그래밍(Template MetaProgramming : TMP)
- STL : 템플릿 라이브러리. 컨테이너, 반복자, 알고리즘, 함수 객체
- C++를 사용한 효과적인 프로그래밍 규칙은 경우에 따라 달라집니다.
- 그 경우란, 바로 C++의 어떤 부분을 사용하느냐입니다.
#define을 쓰려거든 const, enum, inline을 떠올리자
가급적 선행 처리자보다 컴파일러를 더 가까이 하자.
- #define ASPECT_RATIO 1.653
- define을 사용할 경우 사용자에겐 기호식 이름(symbolic name)으로 보이지만, 컴파일러는 선행 처리자가 숫자 상수로 바꾸기 때문에 ASPECT_RATIO라는 이름은 컴파일러가 쓰는 기호 테이블에 들어가지 않게 됩니다.
- 에러가 발생하게 될 경우 에러 메시지에 ASPECT_RATIO가 아닌 1.653이 나타나게 됩니다.
- 이 문제는 기호식 디버거(symbolic debugger)에서도 나타날 소지가 있습니다.
const double AspectRatio = 1.653;
- 이 문제를 해결하기 위해 매크로 대신 상수를 쓰는 것입니다.
- 언어 차원에서 지원하는 상수 타입의 데이터이기 때문에 기호 테이블에 들어갑니다.
- 컴파일을 거친 최종 코드가 #define을 썻을 때보다 작게 나올 수 있습니다.
- 매크로를 쓰면 코드에 ASPECT_RATIO가 등장하면 선행 처리자가 모두 1.653으로 바꾸면서 목적 코드 안에 1.653의 사본이 등장 횟수만큼 들어가게 되지만, 상수 타입의 경우는 아무리 여러 번 쓰더라도 사본은 딱 한 개만 생기기 때문입니다.
const char* const authorName = "Scott Meyers";
const std::string authorName("Scott Meyers");
- #define을 상수로 교체하려고 할 때 주의해야 할 점이 있습니다.
- 상수 포인터(constant pointer)를 정의하는 경우입니다.
- 상수 정의는 대게 헤더 파일에 넣는 것이 상례이므로, 포인터는 꼭 const로 선언해 주어야 하고, 이와 더불어 포인터가 가리키는 대상까지 const로 선언하는 것이 보통입니다.
- 클래스 멤버로 상수를 정의하는 경우, 즉 클래스 상수를 정의하는 경우 그 상수의 사본 개수가 한 개를 넘지 못하게 하고 싶다면 정적(static) 멤버로 만들어야 합니다.
- 상수의 정의는 헤더 파일이 아닌 cpp파일어 둡니다.
class GamePlayer { private: static const int NumTurns = 5; // 상수 선언 int scores[NumTurns]; //... } const int GamePlayer::NumTurns; // 상수 정의.
- 상수의 초기값은 해당 상수가 선언된 시점에서 바로 주어지기 때문에 정의(Declaration)에는 상수의 초기값이 있으면 안됩니다. - 선언될 당시 바로 초기화.
- #define은 유효범위란 무엇인지 모릅니다.
- 매크로는 정의되면 중간에 #undef되지 않는다면, 컴파일이 끝날 때까지만 유효합니다.
- 매크로는 어떤 형태의 캡슐화 혜택도 받을 수 없습니다.
- 상수 포인터(constant pointer)를 정의하는 경우입니다.
- 나열자(enumerrator) 타입의 값은 int가 사용되야 할 곳에서도 쓸 수 있는 것을 적극 활용해봅니다.
- 통칭 나열자 둔갑술(enum hack)으로 알려진 기법입니다.
- 동작방식은 const보다는 #define에 더 가깝습니다.
- const의 주소를 잡아내는 것은 합당하지만, enum의 주소를 취하는 것은 합당하지 않습니다.
- enum은 #define과 같이 어떤 형태의 메모리 할당도 하지 않습니다.
- enum은 상당히 많은 코드에서 이러한 기법이 사용되고 있고, 템플릿 메타 프로그래밍의 핵심 기법이기도 합니다.
class GamePlayer
{
private:
enum { NumTurns = 5; };
int scores[NumTurns];
}
- 함수처럼 쓰이는 매크로를 만드려면, #define보다는 인라인 함수를 우선적으로 생각합니다.
- 이런 매크로를 사용할 경우 인자마다 반드시 괄호를 씌워줘야 합니다.
- 또한 잘못된 사용이 발생할 수 있기 때문에 지양하고 있습니다.
- 이렇기 때문에 기존 매크로의 효율을 그대로 유지하면서, 정규 함수의 모든 동작 방식 및 타입 안전성까지 완벽하게 취할 수 있는 인라인 함수에 대한 템플릿을 준비하는 것입니다.
// T가 정확히 무엇인지 모르기 때문에, // 매개변수로 상수 객체에 대한 참조자를 사용합니다. template<typename T> inline void callWithMax(const T& a, const T& b) { f(a > b ? a : b); }
- 템플릿 함수는 동일 계열 함수군(Family of functions)를 만들어 냅니다.
// a와 b 중에 큰 것을 f에 넘겨 호출합니다.
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
int a = 5, b = 0;
CALL_WITH_MAX(++a, b); // a가 두 번 증가합니다
CALL_WITH_MAX(++a, b + 10); // a가 한 번 증가합니다.
낌새만 보이면 const를 들이대보자!
const의 의미로는
- 외부 변경을 불가능하게 하여 ‘의미적인 제약’을 소스 코드 수준에서 붙입니다.
- 어떤 값이 불변이어야 한다는 제작자의 의도를 컴파일러 및 다른 프로그래머와 나눌 수 있는 수단입니다.
char greeting[] = "Hello";
char *p = greeting; // 비상수 포인터, 비상수 데이터
const char *p = greeting; // 비상수 포인터, 상수 데이터
char * const p = greeting; // 상수 포인터, 비상수 데이터
const char * const p = greeting; // 상수 포인터, 상수 데이터
- 포인터가 가리키는 대상을 상수로 할 수도 있고, 포인터 자체를 상수로 할 수 도 있습니다.
- STL Iterator는 기본 동작 원리가 T* 포인터와 유사합니다.
- 단, 반복자 앞에 붙는 const는 T* const랑 의미가 동일합니다.
- 데이터 상수성(const T*)을 원한다면 const_iterator를 사용하면 됩니다.
std::vector<int> vec;
const std::vector<int>::iterator iter = vec.begin();
*iter = 10;
++iter; // 에러
std::vector<int>::const_iterator cIter = vec.begin();
*cIter = 10; // 에러
++cIter;
- 함수에서 const 강력한 존재입니다.
- 함수 반환값을 상수로 선언하면 안전성이나 효율을 포기하지 않고도 의도하지 않은 버그를 예방할 수 있습니다.
class Rational {//...};
const Rational operator*(const Rational* lhs, const Rational& rhs);
Rational a, b, c;
//...
(a * b) = c; // const 지정 때문에 대입 방지 가능.
// operator*의 결과에 operator=를 호출
- 상수 멤버 함수
- 해당 멤버 함수가 상수 객체에서도 호출 가능한 함수라는 뜻입니다.
- 함수 맨 뒤에 const를 붙여서 표현합니다.
- 클래스의 인터페이스를 이해하기 좋게 하는 효과가 있습니다.
- 이 키워드를 통해 상수 객체를 사용할 수 있게 하자는 뜻입니다.
- 상수 객체에 대한 참조자(reference-to-const)로 진행할 때 상수 멤버 함수가 준비되어 있어야 한다는 것이 포인트입니다.
- const 키워드가 있고 없고의 차이만 있는 멤버 함수들은 오버로딩이 가능합니다.
T& operator[] (std::size_t idx); const T& operator[] (std::size_t idx) const;
- 실제 프로그램에서 상수 객체가 생기는 경우
- 상수 객체에 대한 포인터로 객체가 전달될 때
- 상수 객체에 대한 참조자로 객체가 전달될 때
- 물리적 상수성[비트 수준 상수성]
- 어떤 멤버 함수가 그 객체의 어떤 데이터 멤버도 건드리지 않아야 그 멤버 함수가 ‘const’임을 인정하는 개념입니다.
- static 멤버는 제외합니다.
- 즉, 멤버 포인터가 가리키는 대상의 수정 여부는 판단하지 않습니다.
- 어떤 멤버 함수가 그 객체의 어떤 데이터 멤버도 건드리지 않아야 그 멤버 함수가 ‘const’임을 인정하는 개념입니다.
// 부적절하지만 비트수준 상수성이 있어서 허용되는 선언.. T& operator[] (std::size_t position) cosnt { return pText[position]; }
- 논리적 상수성
- 값이 몇개 바뀌어도 사용자가 그걸 알 수 없고, 논리적으로 상수라면, 그것은 상수입니다.
- mutable 키워드를 사용해 상수 객체에서도 수정 가능한 멤버를 생성할 수 있습니다.
- 컴파일러가 비트 수준 상수성을 지켜주지만, 개념적인(논리적인) 상수성을 사용하면서 프로그래밍 하는 사람이 되어야합니다.
- 상수 멤버 및 비상수 멤버 함수가 기능적으로 서로 똑같이 구현되 있을 경우, 비상수 버전이 상수 버전을 호출하게 해서 코드 중복을 피하도록 합니다.
- const_cast 를 통해 캐스팅을 하는 것은 일반적으로 썩 좋지 못한 아이디어입니다.(나중에 추가적으로 나올 얘기)
- *this의 타입캐스팅을 하는 방법을 이용해 캐스팅을 한번이 아니라 두번을 하도록 합니다.
- 첫번째로 *this에 const를 붙이는 캐스팅을 하고,
- 두번째로 상수 반환 값에서 const를 떼어내는 캐스팅을 합니다.
- 상수 멤버 함수에서 비상수 멤버 함수를 호출하게 되면, 데이터를 수정하지 않겠다고 약속한 객체를 배신하는 것이 되기 때문에 틀리다는 얘기가 나오는 것입니다.
char& operator[] (std::size_t position)
{
return const_cast<char&>(
static_cast<cosnt TextBlock&>
(*this)[position]
);
}
-
KeyPoint
- const를 붙여 선언하면 컴파일러의 도움을 받을 수 있습니다 const는 어떤 유효범위에 있는 개체, 함수 매개변수 및 반환 타입 그리고 멤버 함수에도 붙을 수 있습니다.
- 컴파일러 쪽에서 보면 비트수준 상수성을 지켜야 하지만, 작성자인 우리는 논리적인 상수성을 사용해서 프로그래밍 해야합니다.
- 상수 멤버 및 비상수 멤버 함수가 기능적으로 서로 똑같게 구현되어 있을 경우에는 코드 중복을 피하는 것이 좋은데, 이때 비상수 버전이 상수 버전을 호출하게 해야합니다.
Date: May 19, 2021
Tags:
Programming