변수 정의를 늦추는 이유를 알아보자 😉

안녕하세요! 두두코딩 입니다 ✋
오늘은 Effective C++ 항목 26번째에 대해 알아보겠습니다.

🖇 소스코드에 마우스를 올리고 copy 버튼을 누를 경우 더 쉽게 복사할 수 있습니다!

궁금한 점, 보안점 남겨주시면 성실히 답변하겠습니다. 😁
+ 감상평 댓글로 남겨주시면 힘이됩니다. 🙇

구현

이번 포스팅 부터는 Chapter5. 구현 부분에 대해서 알아보도록 한다.

요구사항에 맞추어 클래스(및 클래스 템플릿) 집합의 정의를 마치고 사용할 함수의 선언문까지 끝마치고 나면, 코드를 구현하는 작업을 진행한다. 코드의 살을 붙이는 구현 작업은 별로 어렵지 않지만, 당황스러운 경우가 몇 가지 존재한다.

  1. 변수를 앞서서 정의해 성능이 떨어지는 경우
  2. 캐스트 남용으로 버그 발생
  3. 내부 hanle로 인한 캡슐화 파괴
  4. 유효하지 않는 핸들 부유현상
  5. 예외발생으로 인한 자원 유출

위와 같은 5가지 이외에도 여러 케이스가 구현에서 발생하는데, 우리가 구현 시 주의해야될 부분을 Chapter5에서 다뤄보도록 하자.

변수 정의 비용

우리가 객체를 정의하면, 항상 생성자소멸자에 대한 비용을 고려해야한다. 생성자 비용은 프로그램 제어 흐름이 변수의 정의에 닿을 때 생성자가 호출되는 비용을 말하고, 소멸자 비용은 변수가 유효범위를 벗어날 때 소멸자가 호출되는 비용을 말한다.

위 비용들은 변수가 정의 됐으나 사용되지 않을 경우에도 부과되는데, 왠만한 사고흐름이라면 이런 비용은 피하는 것이 좋다고 생각할 것이다.

이런 비용을 누가 지불하고 싶겠나? 우리는 이런 변수를 만들지 않는다!! 라고 큰소리 치고 싶겠지만, 우리가 생각하지 못한 부분에서 해당 비용들은 발생하게 된다. 아래의 예시를 보도록 하자.

void encrypt(std::string& s) {
	...
}

std::string encryptPassword(const std::string& password)
{
	using namespace std;

	string encrypted;
	encrypted = password;

	if (password.length() < MiniumPasswordLength) {
		throw logic_error("Password is too short!! ");
	}
	// 주어진 비밀번호를 갖고 암호화 하는 작업을 여기서 한다.
	encrypt(encypted);
	...

	return encrypted;
}

위의 코드를 보면, password를 전달받아 encrypted 문자열 객체를 활용해 암호화 하는 함수를 구현했다. 다만, password의 길이가 너무 짧으면 throw 즉, 예외로 처리하도록 한다. 이 경우에 만약 사용자가 전달하는 password가 너무 짧을 경우 예외 처리 되는데 encrypted는 사용도 못해보고 함수는 끝나 버린다. 즉, encrypted 문자열 객체는 사용을 위한 생성자 비용을 지불했는데, 사용도 못해고 예외처리되면서 소멸자 비용을 추가로 제출하는 현상이 발생되게 된다.

즉, 위에서 우리가 언급한 상황 사용하지 않는데 생성자 비용소멸자 비용을 지불하는 현상이 발생하게 되는 것이다.

이를 막기 위해서는 encrypted 객체를 정말 사용할 때 즉, 최대한 미루게 되면 해결할 수 있다.

void encrypt(std::string& s) {
	...
}

std::string encryptPassword(const std::string& password)
{
	using namespace std;

	if (password.length() < MiniumPasswordLength) {
		throw logic_error("Password is too short!! ");
	}

	/////////////////////
	//  encrypted 함수를 최대한 미룹시다!!!
	/////////////////////
	string encrypted;
	encrypted = password;

	// 주어진 비밀번호를 갖고 암호화 하는 작업을 여기서 한다.
	encrypt(encypted);
	...

	return encrypted;
}

위의 코드를 통해 encrypted를 최대한 미뤄 비용 절감을 했다. 하지만 어딘가 최적화 할 부분이 더 남았다고 생각하지 않는가? 우리는 항목 4를 통해 대입연산자하는 것보다 복사생성자를 호출하는 것이 더 낫다라는 것을 알 수 있다.

따라서, 우리는 아래와 같이 의미도 없고 비용도 만만치 않은 듯한 기본생성자 호출은 과감히 건너 뛰고, 복사생성자를 활용한 초기화를 해야한다.

std::string encryptPassword(const std::string& password)
{
	...
	string encrypted(password);

	// 주어진 비밀번호를 갖고 암호화 하는 작업을 여기서 한다.
	encrypt(encypted);
	...

	return encrypted;
}

항목26에 적힌 늦출 수 있는 데 까지는 어떤 변수를 사용해야 할 때가 오기 전까지 그 변수의 정의를 늦추는 건 기본이고, 초기화 인자를 손에 넣기 전까지 정의를 늦출 수 있는지도 살펴봐야 하는다는 것이다.

즉, 불필요한 기본 생성자 호출이 일어나는 것을 최대한 막자라는 것이고, 변수의 쓰임새를 적재적소에 사용하자는 것이다.

루프에서 사용하는 변수

그렇다면, 우리가 흔히 사용하는 반복문(루프)에서 사용하는 변수는 어떻게 정의해야할까?

어떤 변수가 루프안에서만 사용하는 경우라면 루프 바깥에서 정의하고 사용하는 것이 좋을까? 아니면 루프 내에서 정의하는 것이 좋을까?

두가지 방법의 코드를 보면 아래와 같다.

// A 방법 - 루프 바깥쪽 정의
void outOfLoop() {
	...

	Widget w;
	for (int i = 0; i < n; i++) {
		w = w(i);
		...
	}
}

// B 방법 - 루프 안쪽 정의
void inOfLoop() {
	...

	for (int i = 0; i < n; i++) {
		Wight w(i);
		...
	}
}

🌱 A 방법을 사용할 경우

A방법을 사용할 경우 생성자 1번 + 소멸자 1번 + 대입연산자 n번 이 호출됨

🌱 B 방법을 사용할 경우

B방법을 사용할 경우 생성자 n번 + 소멸자 n번 이 호출됨

위 경우는 대입연산자의 성능에 따라 달라질 수 있다. 클래스 중 대입연산자가 생성자,소멸자 쌍으로 불리는 경우보다 비용이 적게 나오는 경우가 있는데, 이럴 경우는 A 방법을 택해야한다. 그렇지 않은 경우 B 방법을 택해야한다.

또, 비용이 비슷할 경우 A 방법을 사용할 경우 루프 밖에서 Wiget이라는 객체를 활용할 수 있다는 장점이 있으나, 굳이 루프 밖에서 사용할 필요가 없다면 B 방법을 택해도 된다.

기억할 점

👉 변수 정의는 늦출 수 있을때 까지 늦추자. 프로그램이 깔끔해지고, 효율도 좋아진다.

Reference

Effective C++