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

2024. 12. 17. 17:14·책/Effective C++

1. const

const는 객체가 수정되지 않아야 한다는 의미적인 제약을 명확히 표현할 수 있습니다. 이 제약을 선언하면 컴파일러가 이를 강제하기 때문에 실수를 줄이고 더 안전한 코드를 작성할 수 있습니다.

 

const를 사용할 수 있는 위치

  1. 변수와 객체
    • 전역, 네임스페이스, 정적 변수에 적용 가능합니다.
  2. 포인터
    • 포인터 자체, 포인터가 가리키는 값에 각각 const를 적용 가능합니다.
  3. 함수
    • 함수의 매개변수, 반환값, 멤버 함수에 const를 사용할 수 있습니다.

 

2. const와 포인터

char greeting[] = "Hello";

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

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

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

// 4. 상수 포인터, 상수 데이터
const char * const p = greeting;
  • const가 * 왼쪽에 있으면 포인터가 가리키는 대상이 상수.
    • const를 타입 앞에 붙이냐, 뒤에 붙이냐는 취향 차이. 의미적인 차이는 없습니다.
void f1(const Widget *pw); 
void f2(Widget const *pw);
  • const가 * 오른쪽에 있으면 포인터 자체가 상수.
  • 둘 다 상수인 경우 const를 양쪽에 선언.

 

3. const와 STL 반복자

STL 반복자(iterator)는 포인터처럼 동작합니다.

  • const iterator: 반복자 자체는 변경 불가, 가리키는 데이터는 변경 가능
std::vector<int> vec;
...
const std::vector<int>::iterator iter = vec.begin();
*iter = 10;   // OK, 데이터 수정 가능
++iter;       // Error! iter는 상수이므로 반복자 이동 불가
  • const_iterator: 가리키는 데이터는 변경 불가, 반복자 자체는 이동 가능
std::vector<int>::const_iterator cIter = vec.begin();
*cIter = 10;  // Error! *cIter는 상수이므로 데이터 수정 불가
++cIter;      // OK, 반복자 이동 가능

 

4. 함수에서의 const

1) const 매개변수와 반환값

함수의 매개변수나 반환값에 const를 사용하면 불필요한 수정 방지와 코드 가독성 향상에 도움이 됩니다.

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

만약 const를 반환하지 않는 경우

Rational a, b, c;
...
(a * b) = c; // 잘못된 코드! a * b의 결과에 대입 불가

말도 안되는 코드지만 operator*의 반환값이 const가 아니면 ****(a * b)를 수정할 수 있습니다. 즉, 컴파일러는 a * b의 반환값에 대해 대입 연산(=)을 허용해 버립니다.

 

2) const 멤버 함수

const를 멤버 함수에 붙이면 객체를 변경하지 않는 함수라는 것을 의미합니다.

 

두 가지 주요 이유

  1. 클래스 인터페이스의 명확성 : 어떤 함수가 객체를 수정하는지, 수정하지 않는지를 명확히 알 수 있습니다.
  2. const 객체와의 호환성 : const 멤버 함수가 정의되어 있어야 const 객체를 안전하게 사용할 수 있습니다.

const 키워드가 있고 없고 차이만 있는 멤버 함수들은 오버로딩이 가능합니다. 즉, 상수 멤버 함수는 비상수 멤버 함수와 구별되며, 둘은 서로 다른 함수로 간주됩니다.

class TextBlock {
public:
		// 상수 객체에 대한 operator[]
    const char& operator[](std::size_t position) const { return text[position]; } 
    // 비상수 객체에 대한 operator[]
    char& operator[](std::size_t position) { return text[position]; } 
private:
    std::string text;
};

TextBlock tb("Hello");
std::cout << tb[0];   // TextBlock::operator[]의 비상수 멤버를 호출
tb[0] = 'x';  // 비상수 버전 -> 값 수정 가능

const TextBlock ctb("World");
std::cout << ctb[0];  // TextBlock::operator[]의 상수 멤버를 호출
ctb[0] = 'x';  // 컴파일 에러! const 객체 수정 불가
  • 상수 객체: 상수 버전의 operator[] 호출.
  • 비상수 객체: 비상수 버전의 operator[] 호출.

 

3) 비트수준 상수성(Bitwise Constness) vs 논리적 상수성(Logical Constness)

비트수준 상수성

멤버 함수가 그 객체의 어떤 데이터 멤버도 건드리지 않아야 한드는 뜻으로, 컴파일러는 데이터 멤버에 대해 대입 연산이 수행되었는지만 보면 됩니다.

C++ 에서 정의하고 있는 상수성이 비트수준 상수성

class CTextBlock
{
  public:
    // 부적절한 (그러나 비트수준 상수성이 있어 허용되는) operator[] 의 선언
    char& operator[](std::size_t position) const
    { return pText[position]; }
    
  private:
    char *pText;
};

pText에 대한 포인터 자체는 변경되지 않아 비트수준 상수성을 만족해 컴파일러 수준에서 문제가 발생하지 않습니다.

// 상수 객체를 선언
const CTextBlock cctb("Hello");

// 상수 버전의 operator[] 를 호출하여 cctb 의 내부 데이터에 대한 포인터를 얻습니다
char *pc = &cctb[0];

// cctb 는 이제 "Jello" 라는 값을 갖습니다.
*pc = 'J';

operator[]는 const로 선언되었지만, 내부적으로 pText가 가리키는 값을 수정할 수 있습니다. 이는 비트 단위로 객체 자체는 변하지 않았지만 논리적으로 객체가 수정된 상황입니다. (비트 단위로 보면 객체의 멤버 변수(포인터 pText)는 수정되지 않았습니다. 하지만 논리적으로 보면 객체의 데이터(pText가 가리키는 문자열 내용)는 변경되었습니다.)

 

논리적 상수성

객체의 상태를 외부에서 보기에만 변경되지 않도록 하는 개념입니다.

class CTextBlock {
public:
    std::size_t length() const {
        if (!lengthIsValid) {            
            textLength = std::strlen(pText); // 에러!
            lengthIsValid = true;
        }
        return textLength;
    }
private:
    char* pText;
    std::size_t textLength; // 바로 직전에 계산한 텍스트 길이
    bool lengthIsValid;     // 이 길이가 현재 유효한가?
};

textLength 및 lengthIsValid가 바뀔 수 있으므로 비트수준 상수성과는 멀리 떨어져 있습니다. 그렇지만 CTextBlock의 상수 객체에는 아무런 문제가 없습니다.

이를 컴파일러 오류가 안나게 하기 위해서는 mutable 키워드를 사용합니다.

 

4) mutable 키워드를 활용한 논리적 상수성

mutable 키워드는 const 멤버 함수에서도 멤버 변수를 수정할 수 있도록 허용합니다.

class CTextBlock {
public:
    std::size_t length() const {
        if (!lengthIsValid) {
            textLength = std::strlen(pText);  
            lengthIsValid = true;
        }
        return textLength;
    }
private:
    char* pText;
    **mutable** std::size_t textLength; // mutable 키워드
    **mutable** bool lengthIsValid;
};

 

5. const와 코드 중복 제거

const와 비const 멤버 함수가 거의 동일한 기능을 수행할 때, 비const 함수가 const 함수를 호출하도록 구현하여 코드 중복을 방지할 수 있습니다.

 

문제점 : 두 함수에 동일한 코드가 반복되므로 코드 중복이 발생합니다.

class TextBlock {
public:
    const char& operator[](std::size_t position) const {
        ... // 경계 검사, 접근 데이터 로깅, 자료 무결성 검증 등
        return text[position];
    }

    char& operator[](std::size_t position) {
        ... // 경계 검사, 접근 데이터 로깅, 자료 무결성 검증 등
        return text[position];
    }

private:
    std::string text;
};

 

해결 방법 : 중복을 피하기 위해 비상수 버전의 operator[]가 상수 버전의 operator[]를 호출하게 만듭니다. 이때, 두 가지 캐스팅을 사용합니다:

  1. static_cast로 this를 const 객체로 변환
  2. const_cast로 const 반환값의 const 속성 제거
class TextBlock {
public:
		// 이전과 동일
    const char& operator[](std::size_t position) const {
        ...
        return text[position];
    }
    
    // 상수 버전 op[] 를 호출하고 끝
    char& operator[](std::size_t position) {
        return 
	        // op[] 의 반환 타입에 캐스팅을 해서 const 를 떼어냅니다
	        const_cast<char&>(
		        // *this 타입에 const 를 붙입니다. op[] 의 상수 버전을 호출합니다.
            static_cast<const TextBlock&>
	            (*this)[position]
        );
    }
private:
    std::string text;
}

 

중요!

  • const 를 붙여 선언하면 컴파일러가 사용상의 에러를 잡아내는 데 도움을 줍니다. const 는 어떤 유효범위에 있는 객체에도 붙을 수 있으며, 함수 매개변수 및 반환 타입에도 붙을 수 있으며, 멤버 함수에도 붙을 수 있습니다.
  • 컴파일러 쪽에서 보면 비트수준 상수성을 지켜야 하지만, 여러분은 개념적인(논리적인) 상수성을 사용해서 프로그래밍해야 합니다.
  • 상수 멤버 빛 비상수 멤버 함수가 기능적으로 서로 똑같게 구현되어 있을 경우에는 코드 중복을 피하는 것이 좋은데, 이때 비상수 버전이 상수 버전을 호출하도록 만드세요.

 

 

'책 > Effective C++' 카테고리의 다른 글

[Effective C++] 항목 2: #define을 쓰려거든 const, enum, inline을 떠올리자  (2) 2024.12.16
[Effective C++] 항목 1: C++를 언어들의 연합체로 바라보는 안목은 필수  (1) 2024.12.05
'책/Effective C++' 카테고리의 다른 글
  • [Effective C++] 항목 2: #define을 쓰려거든 const, enum, inline을 떠올리자
  • [Effective C++] 항목 1: C++를 언어들의 연합체로 바라보는 안목은 필수
개발자 밍
개발자 밍
dev0404 님의 블로그 입니다.
  • 개발자 밍
    Developer
    개발자 밍
  • 전체
    오늘
    어제
    • 분류 전체보기 (88)
      • 강의 (8)
        • UE Climbing System (3)
        • UE Dungeon (1)
        • HCI (4)
      • 책 (18)
        • 객체지향의 사실과 오해 (5)
        • Effective C++ (3)
        • 이득우의 게임 수학 (4)
        • 이것이 취업을 위한 컴퓨터 과학이다 (4)
        • 리뷰 (2)
      • C++ (2)
      • 알고리즘 (2)
      • 자료구조 (1)
      • Unreal (4)
      • 내일배움캠프 (52)
        • TIL (52)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    객체지향
    게임수학
    컴퓨터구조
    그래픽스
    Effective
    알고리즘
    컴퓨터 구조
    내일배움캠프
    자료구조
    c++
    언리얼
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
개발자 밍
[Effective C++] 항목 3: 낌새만 보이면 const를 들이대 보자!
상단으로

티스토리툴바