이펙티브 C++
https://www.yes24.com/Product/Goods/17525589
페이지 (183p ~ 190p)
이펙티브 C++(183p ~ 190p)
요약
5. 구현
항목 26: 변수 정의는 늦출 수 있는 데까지 늦추는 근성을 발휘하자
- 생성자 혹은 소멸자를 끌고 다니는 타입으로 변수를 정의하면 반드시 물게 되는 비용이 두 개 있다.
- 프로그램 제어 흐름이 변수의 정의에 닿을 때 생성자가 호출
- 그 변수가 유효범위를 벗어날 때 소멸자가 호출
- 이 경우에는 변수가 정의됐으나 사용되지 않은 경우에도 비용이 부과된다.
- 다음 함수는 주어진 비밀번호가 충분히 길 경우에 해당 비밀번호를 암호화하여 반환하는 함수이다.
- 비밀번호가 너무 짧으면 logic_error 타입의 예외를 던진다.(표준 C++에 정의되어 있음)
std::string encryptPassword(const std::string& password)
{
using namespace std;
// encrypted 변수를 너무 일찍 정의함
string encrypted;
if (password.length() < MinimumPasswordLength ) {
throw logic_error("Password is too short");
}
return encrypted;
}
encrypted
변수는 예외가 발생하면 사용되지 않기 때문에 이 변수를 정의하는 시점을 꼭 필요해지는 시점으로 미루는 것이 좋을 것 같다.
std::string encryptPassword(const std::string& password)
{
using namespace std;
if (password.length() < MinimumPasswordLength ) {
throw logic_error("Password is too short");
}
string encrypted;
// ..
return encrypted;
}
- 이렇게
encrypted
의 사용 시점을 이동시켜도 별로 좋아 보이지는 않는다.- 변수가 정의될 때 초기화 인자가 하나도 없기 때문에 기본 생성자가 호출될 것이다.
- 그런데 어떤 변수에 값을 주기 위해서는 대입 연산을 사용해야 하는데, 생성자가 호출되고 그 다음으로 값이 대입되기 때문에 효율적인 측면에서 별로다.
std::string encryptPassword(const std::string& password)
{
using namespace std;
if (password.length() < MinimumPasswordLength ) {
throw logic_error("Password is too short");
}
// 변수를 정의함과 동시에 초기화(복사 생성자 사용)
string encrypted(password);
// ..
return encrypted;
}
- 따라서 변수를 정의를 늦추는 것은 기본이고, 변수의 초기화까지 한 번에 진행할 수 있어야 ‘늦출 수 있는 데까지’ 늦추는 것이다.
- 이렇게 해야 쓰지도 않을 객체의 생성자, 소멸자 호출 비용을 아낄 수 있으며, 정의 후 대입에 필요한 복사 비용도 아낄 수 있다.
- 반복문의 경우에는 다음과 같은 경우가 발생할 수 있다.
// A
Widget w;
for(int i=0; i<n; ++i)
{
w = i;
}
// B
for(int i=0; i<n; ++i)
{
Widget w(i);
}
- 방법 A는 생성자 1번, 소멸자 1번, 대입 n번
- 방법 B는 생성자 n번, 소멸자 n번
- 클래스 중에는 대입에 들어가는 비용이 생성자, 소멸자 쌍보다 적게 나오는 경우가 있는데, Widget 클래스가 이런 경우라면 A 방법이 일반적으로 훨씬 효율이 좋다.
- 그렇지 않은 경우에는 B 방법이 더 좋을 것이다.
- 여기서 주의해야 할 부분은 A 방법을 쓰면 w 변수의 유효범위가 B 방법을 쓸 때보다 넓어지기 때문에 프로그램의 이해도와 유지보수 측면에서 더 안 좋을 수 있다.
- 따라서 대입이 생성자-소멸자 쌍보다 비용이 덜 들고, 전체 코드에서 수행 성능에 민감한 부분을 건드리는 중이 아니라면 B 방법을 사용하는게 좋다.
항목 27: 캐스팅은 절약, 또 절약! 잊지 말자
- C++은 어떤 일이 있어도 타입 에러가 생기지 않도록 보장한다는 동작 규약을 바탕으로 설계되었다.
- 이론적으로 C++ 프로그램은 컴파일만 깔끔하게 끝나면 그 이후엔 어떤 객체에 대해서도 불안전한 연산이나 말도 안 되는 연산을 수행하려 들지 않지만, 캐스트(cast)가 있기 때문에 항상 조심해야 한다.
- C++의 캐스팅 문법은 다음과 같다.
// C 스타일의 캐스트
(T) 표현식 // 표현식 부분을 T 타입으로 캐스팅함
// 함수 방식 캐스트
T(표현식) // 표현식 부분을 T 타입으로 캐스팅함. 문법이 함수 호출문 같음
- 위의 캐스팅 문법은 어떻게 쓰든 동일하며, 괄호를 어디에 썼느냐만 다르다. 이런 형태를
구형 스타일의 캐스트
라고 부른다. - 다음은
신형 스타일의 캐스트
혹은C++ 스타일의 캐스트
이다.
const_cast<T>(표현식)
dynamic_cast<T>(표현식)
reinterpret_cast<T>(표현식)
static_cast<T>(표현식)
- const_cast(표현식): 객체의 상수성을 없애는 용도로 사용된다.
- dynamic_cast(표현식): 안전한 다운캐스팅을 할 때 사용하는 연산자이다. 즉, 주어진 객체가 어떤 클래스 상속 계통에 속한 특정 타입인지 아닌지를 결정하는 작업에 사용된다.
- reinterpret_cast(표현식): 포인터를 int로 바꾸는 등의 하부 수준 캐스팅을 위해 만들어진 연산자로, 적용 결과는 구현환경에 의존적이다. 이 코드는 하부 수준 코드 외에는 거의 없어야 한다.
- static_cast(표현식): 암시적 변환을 강제로 진행할 때 사용한다.
- 구형 스타일의 캐스트도 정상적으로 작동하지만 C++에서는 C++ 스타일의 캐스트를 쓰는 것이 바람직하다.
- 가독성
- C++의 타입 시스템이 망가진 부분을 찾기 쉬움
- 캐스트를 사용할 목적을 더 좁혀서(명확하게) 지정하기 때문에 컴파일러에서 사용 에러를 진단할 수 있음
- 구형 스타일의 캐스트는 다음과 같은 경우에 사용한다.
- 객체를 인자로 받는 함수에 객체를 넘기기 위해 명시호출 생성자를 호출하고 싶을 경우
class Widget {
public:
explicit Widget(int size);
}
void doSomeWork(const Widget& w);
// 함수 방식 캐스트 문법으로 int -> Widget 생성
doSomeWork(Widget(15));
// C++ 방식 캐스트로 int -> Widget 생성
doSomeWork(static_cast<Widget>(15));
- C++에서는 일단 타입 변환이 있으면 이로 말미암아 런타임에 실행되는 코드가 만들어지는 경우가 적지 않다.
// x가 double로 캐스팅된 후에 y와 나눗셈을 한다.
// static_cast를 통해 캐스팅한 부분에서 코드가 생성된다.
int x, y;
double d = static_cast<double>(x) / y;
- 클래스 구조에서도 이런 현상을 볼 수 있다.
class Base {};
class Derived: public Base {};
Derived d;
// Derived* -> Base*로의 암시적 변환 발생
Base *pb = &d;
- 파생 클래스 객체에 대한 기본 클래스 포인터를 만드는 간단한 코드이다.
- 그런데 이 과정에서 두 포인터의 값이 같이 않은 경우도 있다.
- 이 경우에는 포인터의 변위를 Derived* 포인터에 적용하여 실제의 Base* 포인터 값을 구하는 동작이 런타임에 이루어진다.
- 객체 하나가 가질 수 있는 주소는 하나가 아니라 그 이상이 될 수 있다.(Base* 포인터로 가리킬 때의 주소와 Derived* 포인터로 가리킬 때의 주소가 다름)
- C++에서는 다중 상속이 사용되면 이런 현상이 항상 생기지만, 단일 상속인데도 이렇게 되는 경우가 존재한다.
- 따라서 C++에서는 데이터가 어떤 식으로 메모리에 박혀 있을 거라는 섣부른 가정을 피해야 하며, 가정에 기반한 캐스팅은 절대 사용하면 안 된다.
- 객체의 메모리 배치구조를 결정하는 방법과 객체의 주소를 계산하는 방법은 컴파일러마다 천차만별이기 때문에 어떤 플래폼에서 멀쩡한 캐스팅이 다른 플랫폼에서 문제가 될 수 있다.
- 포인터 변위 역시 마찬가지로 통하지 않는 경우가 있을 수 있다.
메모
항목 26: 변수 정의는 늦출 수 있는 데까지 늦추는 근성을 발휘하자
- 변수 정의는 늦출 수 있을 때까지 늦춰야 한다. 프로그램이 더 깔끔해지며 효율도 좋아진다.
'책 > 이펙티브 C++' 카테고리의 다른 글
이펙티브 C++(201p ~ 210p) (0) | 2024.01.25 |
---|---|
이펙티브 C++(191p ~ 200p) (0) | 2024.01.25 |
이펙티브 C++(169p ~ 182p) (2) | 2024.01.22 |
이펙티브 C++(160p ~169p) (1) | 2024.01.21 |
이펙티브 C++(149p ~ 160p) (0) | 2024.01.19 |