[Effective C++] 항목 3 : 낌새만 보이면 const를 들이대 보자!

수민이슈·2023년 3월 14일
0

Effective C++

목록 보기
3/30
post-thumbnail

스콧 마이어스의 Effective C++을 읽고 개인 공부 목적으로 요약 작성한 글입니다!

💡 const를 붙여 선언하면 컴파일러가 사용상의 에러를 잡아내는데 도움을 준다!
💡 const는 어떤 유효범위에 있는 객체라도, 함수 매개변수 및 반환 타입에도, 멤버 함수에도 붙을 수 있다!
💡 컴파일러 입장에서 보면 비트수준 상수성을 지켜야 하지만, 논리적인 상수성을 사용해서 프로그래밍해야 한다!
💡 상수 멤버 및 비상수 멤버 함수가 기능적으로 서로 똑같이 구현되어 있을 경우, 코드 중복을 피하기 위해 비상수 버전이 상수 버전을 호출하도록 만들면 된다!

🖊️ const가 모애옹

const 키워드가 붙은 객체는 외부 변경이 불가능하다!

의미적인 제약을 소스코드 수준에서 할 수 있다.
(((const == 외부 변경 불가능))) <- 이 제약을 컴파일러가 지켜준다.

const가 사용될 수 있는 곳들,,

  • 클래스 밖에서 전역, namespace 유효 범위의 상수 선언
  • 클래스 내부 정적, 비정적 데이터 멤버 변수의 상수 선언
  • 파일, 함수, 블록 유효 범위에서 static으로 선언한 객체
  • 포인터 자체, 포인터가 가리키는 대상
char greeting[] = "Hello";

char* p = greeting;					// 비상수 포인터, 비상수 데이터
									
const char* p = greeting;			// 비상수 포인터, 상수 데이터

char* const p = greeting;			// 상수 포인터, 비상수 데이터

const char* const p = greeting;		// 상수 포인터, 상수 데이터
const * : 포인터가 가리키는 대상(데이터)이 상수
* const : 포인터 자체가 상수

포인터가 가리키는 대상이 상수일 경우, const를 Type 앞 뒤 아무데나 붙여도 상관없음!

void f1(const Widget *pw);

void f2(Widget const *pw);

// 두 개는 같은 의미 

STL 반복자 (iterator)

동작원리가 T* 포인터와 흡사하다.

반복자를 const로 선언 == 포인터를 상수로 선언하는 것
반복자 : 자신이 가리키는 대상이 아닌 것을 가리키기 X
but 반복자가 가리키는 대상 변경 O

변경이 불가능한 객체를 가리키는 반복자 -> const_iterator

iterator == T* const 포인터 (ptr가 const)
const_iterator == const T* 포인터 (대상이 const)

어휴,,

vector<int> vec;

const vector<int>::iterator iter = vec.begin();

*iter = 10;				// OK! 가리키는 대상을 변경할 수 있다
++iter;					// ERROR! (iter == 상수)

vector<int>::const_iterator cIter = vec.begin();
*cIter = 10;			// ERROR! *cIter == 상수
++cIter;				// OK! cIter를 변경하는건 가능

🖊️ 함수에서의 const

함수 리턴값, 매개변수, 멤버 함수, 함수 전체에 const 가능

리턴값이 const인 경우

어이없는 상황에서의 불필요한 에러를 없앨 수 있다

class Rational { ... };
const Rational operator*(const Rational& lhs, const Rational& rhs);

Rational a, b, c;
...
(a * b) = c;		// a * b의 결과는 const Rational인데
					// operator=를 호출해버렸다

웬만하면 매개변수, 리턴값에 const를 붙이자..

상수 멤버 함수

멤버 변수를 변경시키지 않는 멤버함수!!
상수 객체가 호출할 수 있는 함수

중요한 이유
1. 클래스의 인터페이스를 이해하기 좋게 하기 위해

  • 그 클래스로 만들어진 객체를 변경할 수 있는, 없는 함수는 무엇?
  1. const 키워드를 통해 상수 객체를 사용할 수 있게 하자

객체 전달을 상수 객체에 대한 참조자 (Call-By-Reference)로 하는 것이 당연히 좋다
(Call by Value 하면 복사생성자 호출됨,, 성능저하의 주범)
-> 상수 상태로 전달된 객체를 조작할 수 있는 상수 멤버 함수가 필요하다!

상수 객체가 생기는 경우

  1. 상수 객체에 대한 포인터
  2. 상수 객체에 대한 참조자로 객체가 전달될 때

const의 유무에만 차이가 있는 함수들은 오버로딩하자

근데 두 함수의 내용은 똑같을거고,, 상수인지 아닌지에만 차이가 있다.
그냥 똑같은 내용의 함수 2개를 놓기는 좀 그렇다
사실 근데
상수 -> 상수함수 (O)
상수 -> 비상수 함수 (X) 상수 객체가 변경될 수도 있따.
비상수 -> 상수 함수 (O)
비상수 -> 비상수함수 (O)
라서.. 그냥 비상수가 상수 함수를 호출하도록 만들면 됨

class TextBlock {
public:
	...
    // 상수 함수
    const char& operator[] (std::size_t position) const {
    	...
    	return text[position];
    }
    
    // 비상수 함수
    char& operator[] (std::size_t position) {
    	return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
    }
    ...

비상수 operator[]에 대해서
1. static_cast<>를 통해 const로 만든다
2. 상수 버전의 함수를 호출한다
3. const_cast<>를 통해 리턴값의 const를 제거하여 리턴한다

끝!

🔥 주의!
비상수 객체 -> 상수 객체는 안전하기 때문에 static_cast를 써도 되지만,
상수 객체 -> 비상수 객체의 경우 반드시 const_cast를 써야만 한다.

비트수준 상수성과 논리적 상수성

비트수준 상수성

== 물리적 상수성
: 멤버함수는 객체를 구성하는 비트들 중 어떤 것도 바꾸면 안된다!

컴파일러는 비트수준 상수성만을 따진다 (= 대입 연산자가 있는지만 확인한다)

-> 어떤 포인터가 가리키는 대상을 수정하는 멤버함수가 가끔 이 조건을 통과해버림
const의 역할을 하지 못하는데도,,

class CTextBlock {
public:
	...
    char& operator[](std::size_t position) const {
    	return pText[position];
    }
    
private:
	char* pText;
}; 

...

const CTextBlock cctb("Hello");
char* pc = &cctb[0];
*pc = 'J';

//-> cctb == "Jello"

그래서 논리적 상수성이 등장하게 됨!

논리적 상수성

객체의 일부 몇 비트 정도는 바꿀 수 있되, 그 것을 사용자 측에서 알아채지 못하면 const이다!

mutable

: 비정적 데이터 멤버를 비트수준 상수성의 족쇄에서 풀어준다!
mutable 키워드가 붙은 데이터 멤버들은 어떤 순간에도 수정할 수 있다!!!!


😊느낀점

나한테는 const가 약간.. 좀 어려웠다
건들면 안되는 걸 의미하는 const가.. 말 그대로 건들면 안되는 기분?ㅋㅅㅋ
그래서 const를 진짜 내 손으로 쓰는 일이 거의 없었다
바보였지,,,
상수와 조금 더 친해진 기분이다
항목 하나씩 읽어갈 수록 조금씩 퍼즐이 끼워맞춰지는 기분?
이제서야,,

0개의 댓글