C++

#define 보다는 const, enum, inline을 쓰자

C++ 프로그래밍을 할 때 우리는 종종 선행 처리기(Pre-processor)를 사용합니다. 보통 프로그램 전체에 걸쳐 사용되는 상수가 필요할 경우, 아래의 형식으로 사용되죠.

#define PI 3.14159265359

위의 전처리문으로 프로그램의 코드가 컴파일러에게 넘어가기 전에 선행 처리기가 코드에 등장하는 모든 PI3.14159265359로 바꾸어 버립니다. 그 결과로 컴파일러가 사용하는 기호 테이블에는 PI가 포함되지 않습니다. 이건 프로그래머에게 상당히 곤란한 상황을 만들어 낼 수 있습니다. PI가 사용된 코드에서 컴파일 에러가 난 상황이 바로 그 상황입니다. 분명 코드엔 PI가 있었는데, 컴파일 에러에선 PI가 보이지 않고 3.14159265359만이 보일 뿐입니다. 행여 PI가 정의된 파일이 자신이 작성한 것이 아니라면 이 문제는 더욱 심각해집니다. 이 경우엔 대체 3.14159265359이 어디서 왔는지 찾아내느라 시간을 낭비해야만 합니다.

이런 문제는 매크로 대신 상수를 사용해서 해결할 수 있습니다.

const double pi = 3.14159265359;

pi는 C++에서 지원하는 상수 타입의 데이터이기 때문에 컴파일러에게도 보이고 기호 테이블에도 포함됩니다. pi처럼 부동소수점 실수형 데이터인 경우엔 컴파일러가 뱉어낸 코드의 길이도 매크로를 썼을 때보다 짧습니다. 매크로를 썼을 땐 선행 처리기가 PI3.14159265359로 모조리 바꾸면서 그 데이터의 사본이 사용한 만큼 생깁니다. 하지만 const를 썼을 땐 아무리 많이 사용해도 사본이 생기지 않기 때문이죠. 우리가 const를 쓸 수 있을 땐 반드시 사용해야 하는 이유 중 하나입니다.

우리가 #defineconst로 교체할 때는 딱 두 가지만 조심하면 됩니다. 첫째는 상수 포인터를 쓰는 경우입니다. 상수 정의는 보통 헤더 파일에 하고 이때 포인터는 꼭 const로 선언합니다. 포인터의 여러 사본이 같은 대상을 가리키도록 하기 위해서 입니다. 또한 그 포인터가 가리키는 대상도 const로 선언합니다. 코드가 아래와 같이 되겠죠.

const double* const pi = 3.14159265359;

두번째 경우는 상수가 클래스 안에 정의되는 경우입니다. 어떤 상수가 클래스 안에서만 유효하도록 하고 싶을 때 이렇게 하는데, 이럴 땐 상수를 정적 멤버로 만들어야 합니다. 나중에 클래스의 인스턴스가 여러번 생겨날 때 굳이 상수의 사본이 여러개가 될 필요가 없기 때문이죠. 아래를 봅시다.

class Team {
private:
    static const int numOfMembers = 4;
    std::string names[numOfMembers];
}

numOfMembersstatic으로 선언되어 있습니다. 정의(Definition)이 아니라 선언(Declaration)입니다. 이 둘은 분명 다릅니다. C++에선 보통 선언 이후 정의가 필요합니다. 하지만 정적 정수 타입의 상수는 예외입니다. 하지만 여러분이 정의를 필요로 하는 컴파일러를 사용하고 있다면 이야기가 달라집니다. 아래와 같이 구현 파일에서 정의를 해주어야 합니다.

const double Team::numOfMembers;

여기에 초기값 4를 적으면 안됩니다. 초기화는 이미 선언과 동시에 이루어 지고 있습니다.

C++에서 선언은 주로 헤더 파일에 하고 정의는 구현 파일에 합니다. 다른 파일에서 그 헤더 파일을 쉽게 가져다 쓰도록 하기 위함이죠. 근데 구현 파일에선 선언과 정의를 둘 다 할 수 있지만 헤더 파일에선 선언만 할 수 있습니다.

여기서 잠깐, 클래스 상수를 #define으로 하려는 건 아니겠죠? 사실 그 순간 클래스 상수라는 건 말이 안됩니다. 매크로는 유효범위라는 것이 따로 없습니다. 컴파일이 끝날 때까지 유효합니다. 따라서 클래스 상수를 선언할 때 #define를 사용할 순 없습니다. 그렇게 하면 캡슐화도 할 수가 없습니다. private 같은 걸 사용할 수 없단 얘기죠. 그치만 상수 멤버는 그게 가능합니다.

여기서 끝난 게 아닙니다. 간혹 위의 문법을 허용하지 않는 컴파일러가 있습니다. 선언 시점에 초기화를 진행하는 것 말이죠. 표준에 어긋나는 것이지만 옛날 컴파일러는 그럴 수 있습니다. 또 여건에 따라 그런 걸 사용해야하는 상황일 수도 있고요. 그럴 땐 어쩔 수 없이 정의할 때 초기값을 제공해야 합니다.

// team.hpp
class Team {
private:
    static const int numOfMembers;
    std::string names[numOfMembers];
}

// team.cpp
const int Team::numOfMembers = 4;

이렇게 하면 앞서 말한 문제는 해결할 수 있습니다. 근데, 다른 컴파일 에러가 우릴 기다리고 있습니다. names를 선언할 때 초기화되지 않은 numOfMembers를 쓰고 있기 때문이죠. 답이 없습니다. 어떻게 해야할까요? enum으로 장난을 조금 치면 됩니다.

class Team {
private:
    enum { numOfMembers = 5 };
    std::string names[numOfMembers];
}

간지나게 'enum hack'이라고 부르는 방법입니다. 간단하지만 C++의 특징을 살리고 우리의 목적도 달성할 수 있는 섹시한 방법입니다. const로 선언된 상수는 나중에 포인터를 얻을 수 있지만 enum은 그것도 안됩니다. 그래서 나중에 다른 사용자나 개발자가 포인터를 얻지 못하게 하거나 참조자를 쓰지 못하게 할 때에도 좋은 열쇠가 될 수 있습니다. 하나 더 남았습니다. enum은 쓸데없이 메모리 낭비도 하지 않습니다.

이제 마지막 #define의 오용 사례를 보겠습니다. 아래는 두개의 인자 중 큰 것을 골라 함수를 호출하는 매크로 함수입니다.

#define CALL_WITH_MAX(a, b) foo((a) > (b) ? (a) : (b))

문제없는 코드처럼 보이죠? 과연 그럴까요? 아래 코드를 봅시다.

int a = 5;
int b = 0;

CALL_WITH_MAX(++a, b); // a가 2 증가
CALL_WITH_MAX(++a, b + 10); // a가 1 증가

이제 문제가 있는 게 확실해졌습니다. 끔찍합니다. 코드 작성시에 모르고 이렇게 썼다가 런타임시에 뭔가 문제가 있다는 것을 알아챈다면 더 끔찍해집니다. 매크로 함수가 함수 호출을 막아준다는 장점은 분명 가지고 있습니다. 하지만 이런 경우엔 그게 다가 아니죠. 그렇다면 어떻게 하면 이런 상황을 섹시하게 해결할 수 있을까요? 바로 템플릿과 인라인 함수를 쓰면 됩니다.

template<typename T>
inline void callWithMax(const T& a, const T& b) {
    foo(a > b ? a : b);
}

매크로 함수의 효율성도 유지하고 정규 함수의 동작 방식과 타입 안정성까지 취할 수 있는 방법입니다. 매크로 함수를 썼을 때 나타내는 문제가 발생할 여지도 없습니다. 더군다나 callWithMax는 진짜 함수기 때문에 유효 범위와 접근 규칙도 그대로 따라갑니다. 클래스안에서만 쓸 수 있는 인라인 함수도 있을 수 있다는 거죠. 매크로 함수는 그런 개념 자체가 없습니다.

const, enum, inline을 적절히 활용하면 얻을 수 있는 장점들이 이렇게나 많습니다. 무턱대고 #define를 쓰기 전에 한번 생각해보세요.

You've successfully subscribed to devkoriel
Great! Next, complete checkout to get full access to all premium content.
Error! Could not sign up. invalid link.
Welcome back! You've successfully signed in.
Error! Could not sign in. Please try again.
Success! Your account is fully activated, you now have access to all content.
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.