센로그

[EC++] 3. 낌새만 보이면 const를 들이대 보자! 본문

Effective/Effective C++

[EC++] 3. 낌새만 보이면 const를 들이대 보자!

seeyoun 2024. 11. 13. 01:50

const 키워드의 장점

  • 가독성: 값을 변경하지 않겠다는 제작자의 의도를 컴파일러 및 다른 프로그래머들과 나눌 수 있다.
  • 안정성 및 오류 방지: 변수 값의 실수로 인한 변경 방지
  • 함수에서 사용하는 것도 좋음
    • 상수 멤버함수: const와 non const 함수 오버로딩 지원
      • 즉 같은 이름으로 r/w 둘다 되게, 또는 r만 되게 만들 수 있음.
    • 반환값 및 매개변수도 상수화 가능

 


상수 포인터 vs 상수 객체를 가리키는 포인터

  • 쉽게 구분하는 법!
    • *와 포인터변수명 사이에 const가 붙은 경우만 상수 포인터를 의미함. 나머지는 다 상수 객체.

 


STL const iterator vs const_iterator

std::vector<int> vec = {1, 2, 3};
std::vector<int>::iterator const it1 = vec.begin(); // 상수 반복자 (const iterator)
*it1 = 10;       // 반복자가 가리키는 값을 수정할 수 있음
// it1++;       // 오류: 반복자를 변경할 수 없음

std::vector<int>::const_iterator it2 = vec.begin(); // 상수 반복자에서 가리키는 값이 상수 (const_iterator)
// *it2 = 10;    // 오류: 반복자가 가리키는 값을 수정할 수 없음
it2++;          // 반복자 자체는 이동 가능

 

 


함수에서의 const

1. 반환값의 const

class MyClass {
public:
    MyClass operator+(const MyClass& other) const {
        MyClass result;
        // 두 객체의 합을 계산하여 result에 저장
        return result;
    }
};

const MyClass a, b;
(a + b) = MyClass(); // 오류: const 반환값은 대입 불가
  • 반환값에 대입하려고 하는 실수 방지 가능.

 

2. 매개변수의 const

  • 원래 객체가 수정되지 않음을 보장

 

3. const 멤버 함수

  • const 객체인 경우, const 멤버함수만 호출이 가능하다.
  • const, non-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;
};
  • const TextBlock 객체인 경우 위의 연산자가, 일반 TextBlock 객체인 경우 아래의 연산자가 호출된다.
    • 둘다 read 로직은 잘 된다.
    • const 객체의 write의 경우, const 연산자의 반환값에 붙은 const로 인해 write는 안된다.

 

어떤 멤버 함수가 상수 멤버라는 것은 어떤 의미일까?

  • 비트 수준 상수성 (물리적 상수성)
    • 그 객체의 어떤 멤버 변수도 변경하지 않는다. (read만 하는 건 가능)
      • 한 비트도 건드리지 않으며, 이를 컴파일러가 검사한다.
    • 그러나 포인터 개념이 들어가는 순간 작동이 달라질 수 있다.
class CTextBlock {
public:
	char& operator[](std::size_t position) const  // 상수 멤버함수
	{
		return pText[position];	// pText를 write하지는 않으므로 이렇게 사용 가능
	}


private:
	char* pText;
};
  • 이 경우, 다음과 같은 코드가 작동이 가능하다.
const CTextBlock cctb("Hello");	 // 상수 객체 cctb를 만든다.
char *pc = &cctb[0];
*pc = 'J';	// cctb는 Jello가 된다.
  • 이런 예기치 못한 코드가 동작이 되어버린다.

 

  • 논리적 상수성
    • 객체를 전체 다 못건드리게 하는 것보다는, 객체의 논리적 상태를 유지하는 한, 이에 영향을 주지 않는 멤버는 수정할 수 있게 하는 것.
      • 대표적으로 캐시를 위한 함수. 
    • mutable 키워드를 사용한 멤버의 경우 상수 멤버 함수에서도 수정할 수 있다.
class Data {
public:
    Data(int v) : value(v) {}

    int getValue() const {
        if (!isCached) {
            cachedValue = value * 2; // 논리적 상태와 무관한 내부 캐시 수정
            isCached = true;
        }
        accessCount++; // 접근 횟수 기록
        return cachedValue;
    }

private:
    int value;
    mutable int cachedValue = 0; // mutable로 선언된 캐시 변수
    mutable bool isCached = false; // mutable로 선언된 캐시 상태 변수
    mutable int accessCount = 0; // mutable로 선언된 접근 횟수 기록 변수
};

 

 


const, non const 멤버 함수의 코드 중복을 피하는 방법

  • 상수, 비상수 멤버함수 간에 코드 중복이 심한 경우, 이를 피하는 방법이다.
  • 오버로딩을 통해 비상수 객체가 상수 멤버 함수를 호출하도록 만들면 된다.
    • 당연히, 반대로 하는 건 안전하지 않다. 상수 객체가 변경될 위험이 생기기 때문이다.
class TextBlock {
public:
    // 상수 객체에 대한 operator
    const char& operator[](std::size_t position) const
    {
        return text[position];
    }

    // 비상수 객체에 대한 operator
    char& operator[](std::size_t position)
    {
        return
            // 2. 상수성을 다시 제거하여 반환
            const_cast<char&>(
                // 1. 상수 객체로 캐스팅하여 상수 객체에 대한 연산자 호출
                static_cast<const TextBlock&>(*this)[position]	
            );
    }

private:
    std::string text;
};
  • static_cast<const TextBlock&>(*this)[position]를 통해 비상수 객체 자체를 상수 객체로 캐스팅하여, 상수 객체에 대한 []연산자를 호출한다.
  • 이후  const_cast<char&>를 통해 다시 상수성을 제거하고 반환한다.

 


+) 예제 코드

#include <iostream>

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

    // 비상수 객체에 대한 operator
    char& operator[](std::size_t position)
    {
        return
            // 2. 상수성을 다시 제거하여 반환
            const_cast<char&>(
                // 1. 상수 객체로 캐스팅하여 상수 객체에 대한 연산자 호출
                static_cast<const TextBlock&>(*this)[position]
                );
    }

private:
    std::string text;
};

int main() {
	
    const TextBlock ctb("Hello");
    TextBlock tb("Hello");

    // ctb[0] = 'J';  // 불가능
    tb[0] = 'J';	// 가능

    return 0;
}
Comments