SW 그리고 아빠 엔지니어링 중...

아는 만큼 보이고, 경험해봐야 알 수 있고, 자꾸 써야 내 것이 된다.

L C++

애증의 const 사용하기 (1)

보리남편 김 주부 2024. 5. 11. 17:57

const 잘 쓰시나요?

💬 나는 잘 사용하지 않는다. const를 사용하다 보면 에러가 자주 발생하고, 개발하는데 적지 않은 인터럽트가 걸린다. 이런 경험들이 반복되면서 자연스럽게 const 사용하지 않게 되었다. 하지만 '에러가 잘 발생한다'는 것은 반대로 얘기하면 의도하지 않은 실수를 방지하기 위한 알람이기에 잘 쓰는 것이 좋다. (상수 장점 : 실수방지, 멀티스레드 안전)
이번기회에 const를 잘 이해하고 써보자.

 
C++ 대체 언어로 거론되는 Rust는 C++과 반대로 기본이 상수이고, 변수로 사용하려면 키워드를 사용해야 한다. 다시 말해 대세란 말씀!

C/C++

C/C++
int n = 0; //변수
const int c = 0; // 상수


RUST

let mut n = 0; // 변수
let c = 0; // 상수

 

const 멤버함수 원리 이해

아래 예제코드는 const 멤버함수 원리를 이해하기 위한 코드이다. 주석에 정상적으로 동작하는지와 그 이유도 생각해 보자.

#include <iostream>

class Point
{
public:
	int x, y;

	Point(int a, int b) : x(a), y(b) {}

	// 사용자 코드              //컴파일러가 변경한 코드
	void set(int a, int b)      // void set(Point* this, int a, int b)
	{ 
	    x = a;                  // this->x = a;
	    y = b;                  // this->y = b;
	}

	//void print()              // void print(Point* this)
	void print() const          // void print(const Point* this)
	{
		std::cout << x << ", " << y << std::endl;
		//std::cout << this->x << ", " << this->y << std::endl;
	}
};

int main()
{
	const Point pt(1, 2);

	pt.x = 10;          //1) OK or error
	pt.set(10, 20);     //2) OK or error
	pt.print();         //3) OK or error
}

 

pt.x = 10;
error: assignment of member 'Point::x' in read-only object
x는 변수이지만 pt가 상수 객체라 읽기만 가능할 뿐 값 변경은 불가능하다.
pt.set(10, 20);
error: passing 'const Point' as 'this' argument discards qualifiers [-fpermissive]
함수의 경우, 드러나 있지 않지만 함수에 this가 생략되어 있다.
void set(int a, int b)의 경우 컴파일러가 void set(Point* this, int a, int b)로 변경한다.
위 에러의 의미는 상수객체인데 const가 빠져있는 객체의 this를 보냈다는 의미이다. 
void set(const Point* this, int a, int b)
다시 말해 컴파일러가 기대한 this는 const  Point* this인데 이 함수는 const가 빠져있어서 에러가 발생한 것이다.(상수에 값을 대입하려다 발생한 에러가 아님)
정리하면, 상수 객체는 상수 멤버 함수만 호출할 수 있다.

 
 

pt.print();
상수 멤버함수는 아래와 같이 한정자 const를 함수 바깥에 위치하면 되고 컴파일러는 주석처럼 변경을 한다.
void print() const  // void print(const Point* this)

함수 안에 아래 코드는 컴파일러가
std::cout << x << ", " << y << std::endl;

아래 코드처럼 변경하지만 읽기만 하기에 문제없이 출력된다.
std::cout << this->x << ", " << this->y << std::endl;

 

💬 생각해 보면 C++ core guidelines에서는 오버헤드를 줄이기 위해 user define type을 const&로 인자를 받는 것을 추천하고 있기에 이 가이드를 따른다면 const를 안 쓸 수 없다.
F.16: "입력(in)" 매개변수는 복사 비용이 적게 드는 타입의 경우 값으로 전달하고, 그 외에는 상수 참조형으로 전달하라
F.16: For “in” parameters, pass cheaply-copied types by value and others by reference to const

 
왜 그런지 아래 예제를 통해 이해해 보자.
아래 예제에 foo 함수를 보면, 가이드에 따라 Rect를 const & 로 인자를 전달했더니 foo함수를 사용하지 않더라도 에러를 발생시킨다. 왜 그럴까? 🤔

class Rect
{
    int x, y, w, h;
public:
    Rect(int x, int y, int w, int h) : x{ x }, y{ y }, w{ w }, h{ h } {}

    int get_area()  { return w * h; }
};

void foo(const Rect& r) //상수 참조로 인자를 전달
{
    int n = r.get_area(); // error
}

int main()
{
    Rect r(1, 2, 3, 4);
}
get_area() 함수를 컴파일러는 get_area(Rect * this)로 변경하지만
r.get_area() 코드는 get_area(const Rect *this)로 변경되기에 this에 상수가 빠져 있다고 에러가 발생된다. (에러 해결을 위해 foo 함수에 const를 빼는 게 아닌) 아래와 같이 상수 멤버함수로 수정해야 한다.
int get_area() const { return w * h; }


다시 말해 객체의 상태를 변경하지 않는 모든 멤버함수 들(get_xxx)은 반드시 상수 멤버 함수로 해야 하고, 이렇게 사용하지 않는 코드는 나쁜 코드가 아니라 틀린 코드이다.
 

💡 상수 멤버함수 안에 변수를 사용하는 방법 2가지

만약 상수 멤버함수 안에서 값 변경이 필요하다면 어떻게 해야 할까?🤔
💬 본인은 급하다고 const를 뺐던 것 같은데(성능 저하, 코드 불명화, 오류발생 가능성 증가.. ) 나 같은 결정을 하는 사람을 예방하기 위해 2가지 방법을 소개한다.

  • mutable member 사용
  • 변해야 하는 것을 다른 포인터로 분리!


우선 아래코드는 객체의 상태 값을 출력하는 상수 멤버함수 to_string()를 가진 예제코드이다.  이 코드를 수정해서 2가지 방법을 알아보자.

#include <iostream>
#include <string>
#include <sstream>

class Point
{
	int x;
	int y;
public:
	Point(int a = 0, int b = 0) : x(a), y(b) {}
    
	std::string to_string() const
	{
		std::stringstream oss;
		oss << x << ", " << y;
		return oss.str();
	}
};

int main()
{
	Point pt(1, 2);
	std::cout << pt.to_string() << std::endl;
	std::cout << pt.to_string() << std::endl;
}


1) mutable 키워드 사용

📜 C++98 문법

mutable 키워드는 상수 멤버의 값을 변경을 가능하게 한다.
 
아래 예제코드는이전 코드에서 상수 멤버함수 내에 변수 cache 값을 두어 사용하는 코드로 수정되었다. 

#include <iostream>
#include <string>
#include <sstream>

class Point
{
	int x;
	int y;

	mutable std::string cache;
	mutable bool cache_valid = false;

public:
	Point(int a = 0, int b = 0) : x(a), y(b) {}

	std::string to_string() const
	{
		if (cache_valid == false)
		{
			std::stringstream oss;
			oss << x << ", " << y;
			cache = oss.str();
			cache_valid = true;
		}
		return cache;
	}
};

int main()
{
	Point pt(1, 2);
	std::cout << pt.to_string() << std::endl;
	std::cout << pt.to_string() << std::endl;
}

 

cache = oss.str();
cache_valid = true;
이 코드는 this->cache 가 되기에 수정이  불가하지만

mutable std::string cache;
mutable bool cache_valid = false;
이렇게 mutable 키워드가 달리면 에러를 발생시키지 않고 값이 변경된다.

 

2) 변해야 하는 것을 다른 포인터로 분리!

수정하는 data가 상수(data 영역) 영역에 있지 않으면 변경이 가능하기에 아래와 같이 수정하면 mutable 키워드를 사용하지 않고도 개선이 가능하다.

#include <iostream>
#include <string>
#include <sstream>

struct CACHE
{
	std::string data;
	bool cache_valid = false;
};

class Point
{
	int x;
	int y;

	CACHE* cache;

public:
	Point(int a = 0, int b = 0) : x(a), y(b)
	{
		cache = new CACHE;
		cache->cache_valid = false;
	}

	std::string to_string() const
	{
		if (cache->cache_valid == false)
		{
			std::stringstream oss;
			oss << x << ", " << y;
			cache->data = oss.str();
			cache->cache_valid = true;
		}
		return cache->data;
	}
};
int main()
{
	Point pt(1, 2);
	std::cout << pt.to_string() << std::endl;
	std::cout << pt.to_string() << std::endl;
}


CHCHE 구조체를 아래 그림으로 보면, cache의 주소는 변경되지 않고 cache 주소가 가리키는 data와 cache_value 값이 변경되기에 문제가 되지 않는다.

 

 

📜 위 내용을 C++ 핵심 가이드라인에서는 아래와 같이 가이드하고 있다. 참고해 보자.
Con.1: 기본적으로 객체를 변경 불가능하도록 만들어라
Con.2: 기본적으로 멤버 함수들은 const로 만들어라
Con.3: 기본적으로 포인터와 참조는 const로 전달하라

 

💬 다음번에서 많이 사용하는 const 멤버함수 및 C++23에 적용된 기술로 영향을 받는 const에 대해 정리하겠다.
 

참고


* 본 글에 예제 코드는 코드누리 샘플코드를 활용하였습니다.
 

C++ 핵심 가이드라인 한글화 프로젝트 링크

C++ Core Guidelines 원문 링크

 

 

728x90