일반적으로 중괄호 쌍 안에서 선언된 이름은 해당 중괄호의 범위 안에서만 유효하지만 C++98 스타일의 enum은 예외적으로 규칙이 적용되지 않는다.

 

enum Color { black, white, red };

auto white = false; // error

위와같은 열거자들은 범위를 새어 나가기 때문에 범위 없는(unscoped) enum라고도 한다.

 

enum class Color { black, white, red };

auto white = false; // ok
Color c = white; // error
Color c = Color::white; // ok

C++11부터는 열거자들이 범위를 새어나가게 하지 않기 위해 범위 있는(scoped) enum을 추가하였다. enum class라고도 한다.

범위 있는 enum은 열거자들이 범위 밖으로 새어나가지 않기 때문에 이름 공간을 더럽히지 않는것과 더불어서 암시적 타입 변환에 의한 문제도 방지해준다.

범위 없는 enum은 암시적으로 정수 타입으로 변환되는 반면 범위 있는 enum은 암시적 타입 변환이 이루어지지 않기 때문이다.

 

enum Color { black, white, red };

Color c = red;
if (c < 14.5) { ... } // Color->int->double 암시적 변환


enum class Color { black, white, red };

Color c = Color::red;
if (c < 14.5) { ... } // 컴파일 에러

만약 범위 있는 enum의 타입 변환이 필요하다면 직접 캐스팅을 통해 타입 변환을 해주면 된다.

 

또한 범위 있는 enum은 전방 선언도 가능하다.

그런데 전방 선언이 왜 장점일까? 그것은 컴파일 의존 관계와 관련이 있다.

 

enum Status {
    good = 0,
    failed = 1,
    incomplete = 100,
    corrupt = 200,
    indeterminate = 0xFFFFFFFF
};

만약 이런 enum이 존재하고 시스템 전반적으로 사용된다고 가정해보자. 여기서 새 열거자가 추가되는 등의 수정 작업이 이루어지면 시스템 전체가 다시 컴파일되는 극심한 낭비가 발생하게 된다.

 

enum class Status;
...
void continueProcessing(Status s);

하지만 전방 선언을 사용하면 의존 관계가 크게 줄어들기 때문에 컴파일 타임이 크게 줄어들 수 있다.

 

그런데 전방 선언은 해당 타입의 크기를 알아야만 사용할 수 있는 문법이다. 컴파일러는 enum의 타입을 어떻게 알 수 있을까?

범위 있는 enum은 기본 타입이 int이기 때문에 별도의 타입을 지정하지 않아도 컴파일러가 타입을 알 수 있다. 하지만 범위 없는 enum은 기본 타입이 없기 때문에 전방 선언이 안된다. 그렇지만 타입을 임의로 지정해주면 범위 없는 enum도 전방 선언이 가능해진다.

 

enum Status; // 기본 타입 int
enum class Status; // 기본 타입 없음. 전방 선언 X

enum Status : std::uint32_t; // 사용자 정의 타입 uint32_t
enum class Status : std::uint32_t; // 사용자 정의 타입 uint32_t

 

 

범위 있는 enum이 범위 없는 enum보다 더 좋아보이기 때문에 범위 없는 enum은 사용할 필요가 아예 없을것처럼 보인다. 하지만 사용할만한 곳이 적어도 하나는 존재한다.

 

using UserInfo = std::tuple<std::string, std::string, std::size_t>;
UserInfo uInfo;
...
auto val = std::get<1>(uInfo); // 1의 필드는 대체 무슨 뜻?

바로 튜플을 사용할 때이다. 튜플은 각 필드를 std::get을 통해 접근하게 된다. 하지만 리터럴로 접근하면 코드를 읽는 다른 사람이 무슨 뜻인지 바로 알아볼수가 없다.

 

enum UserInfoFields { uiName, uiEmail, uiReputation };
UserInfo uInfo;
...
auto val = std::get<uiEmail>(uInfo); // email 필드의 값이구나!

범위 없는 enum의 열거자들은 정수 타입으로 암시적 변환되며 각 필드를 접근하게 된다. 코드를 읽는 사람도 무슨 필드인지 명확하게 알 수 있기 때문에 가독성이 좋아진다.

 

하지만 범위 있는 enum을 사용한다면 반드시 캐스팅을 통한 타입 변환이 이루어져야 하기 때문에 템플릿 인수의 길이가 매우 길어질 수 있다.

범위 없는 enum에 의한 이름 공간 오염이 걱정된다면 열거자 하나를 받아서 std::size_t값을 돌려주는 함수를 직접 작성하면 된다. 그렇지만 꽤 까다로울 것이다.

 

std::get은 함수 템플릿이라서 컴파일 타임에 평가되기 때문에 함수의 반환값 역시 컴파일 타임에 산출되어야만 한다.

그러려면 해당 함수는 반드시 constexpr이어야 하고 예외를 던지지 않을 것을 알기 때문에 noexcept로 선언되어야 한다. 게다가 모든 종류의 enum에 대해서도 작동해야 하기 때문에 함수 템플릿으로 작성되어야 한다. 함수 템플릿을 고려한다면 반환 타입도 일반화 되어야 하기 때문에 이제는 std::size_t가 아니라 매개변수로 받은 열거자의 기본 타입을 반환시켜주어야 하므로 타입 특성을 이용해야 한다.

즉 1) constexpr, 2) noexcept, 3) 함수 템플릿, 4) type_traits 네가지를 고려해서 작성해야 한다.

 

template<typename E> // C++11
constexpr typename std::underlying_type<E>::type toUType(E enumerator) noexcept
{
    return static_cast<typename std::underlying_type<E>::type>(enumerator);
}


template<typename E> // C++14
constexpr auto toUType(E enumerator) noexcept
{
    return static_cast<std::underlying_type_t<E>>(enumerator);
}

enum의 기본 타입을 알아내는 것은 underlying_type을 사용하면 된다.

 

enum class UserInfoFields { uiName, uiEmail, uiReputation };
UserInfo uInfo;
...
auto val = std::get<toUType(UserInfoFields::uiEmail)>(uInfo);

해당 함수 템플릿을 사용하면 조금 더 번거롭고 코드가 살짝 길어졌지만 범위 없는 enum을 사용했을 때처럼 리터럴 값을 사용하지 않아도 된다. 게다가 범위 있는 enum이기 때문에 이름 공간의 오염과 의도하지 않은 암시적 타입 변환을 걱정할 필요가 없다.

 

◾ C++98 스타일의 enum을 이제는 범위 없는 enum이라고 부른다.

◾ 범위 있는 enum의 열거자들은 그 안에서만 보인다. 이 열거자들은 오직 캐스팅을 통해서만 다른 타입으로 변환된다.

◾ 범위 있는 enum과 범위 없는 enum 모두 기본 타입 지정을 지원한다. 범위 있는 enum의 기본 타입은 int이다. 범위 없는 enum에는 기본 타입이 없다.

◾ 범위 있는 enum은 항상 전방 선언이 가능하다. 범위 없는 enum은 해당 선언에 기본 타입을 지정하는 경우에만 전방 선언이 가능하다.

std::unique_ptr<std::unordered_map<std::string, std::string>>

위와 같은 타입이 꽤 많이 선언되고 사용된다고 가정해보자. 저걸 일일이 타이핑 하는것은 시간 낭비일 뿐더러 코드 가독성도 박살나게 된다.

 

typedef std::unique_ptr<std::unordered_map<std::string, std::string>> UPtrMapSS;

typedef를 사용하면 해결되지만 typedef는 너무 오래된 유물이다.

C++11부터는 별칭 선언이라는게 새로 추가되었다.

 

using UPtrMapSS = std::unique_ptr<std::unordered_map<std::string, std::string>>;

typedef를 이용한 타입 재정의와 완벽하게 동일한 일을 수행한다.

그럼 왜 굳이 using을 사용해야 하는가?

 

typedef void (*FP)(int, const std::string&);
using FP = void(*)(int, const std::string&);

둘다 동일하지만 using쪽이 좀더 이해하기 쉽다. 하지만 이것만으로는 using을 사용해야 하는 이유가 되기에는 모자라다.

 

template<typename T>
using MyAllocList = std::list<T, MyAlloc<T>>; // MyAllockList == std::list<T, MyAlloc<T>>

template<typename T>
struct MyAllocList{
    typedef std::list<T, MyAlloc<T>> type; // MyAllocList::type == std::list<T, MyAlloc<T>>
};

템플릿이 추가되면서 상황이 바뀐다. typedef는 템플릿화 할 수 없지만 using은 가능하기 때문이다.

좀 더 번거로워진것 뿐만 아니라 위의 타입들이 또다른 템플릿 안에서 사용되는 경우 typedef로 재정의된 타입은 typename 지정자를 반드시 붙여주어야 하는 번거로운 작업을 또 해야한다.

 

template<typename T>
class Widget {
private:
    typename MyAllocList<T>::type list;
    // 만약 T가 int이고 특수화 템플릿 Widget<int>의 멤버 변수 이름이 type이라면?
};

typedef로 재정의된 타입인 MyAllocList는 T에 의존적인 타입인데 C++에서는 이 경우에 앞에 반드시 typename 지정자를 붙여야 하는 규칙이 존재한다. 예를 들어 템플릿 특수화에 의해서 MyAllocList<int>::type이 타입이 아니라 어떤 데이터일수도 있기 때문이다. 그래서 컴파일러에게 타입이라고 명시해주는 것이다.

 

하지만 using으로 별칭을 선언한 경우는 해당되지 않는다. 똑같이 T에 의존하는 것처럼 보이지만 컴파일러가 MyAllocList<T>에 도달했을 때, 컴파일러는 그게 타입이라는 것을 확신한다. 그래서 typename 지정자가 필요 없고 붙여서도 안된다.

 

 

별칭 선언과 관련된 또다른 내용중에 타입 특성(type traits)이라는 것이 있다. 

어떤 타입 T에 const 한정자나 참조 한정자를 제거하거나 붙이고 왼값과 오른값을 서로 변환해야 하는 경우에 사용되는 것이다. TMP를 하다보면 마주치게 된다.

 

타입 특성(type traits)

타입에 대한 상수를 가진 구조체

 

std::remove_const<T>::type // const T -> T
std::remove_reference<T>::type // T& or T&& -> T
std::add_lvalue_reference<T>::type // T -> T&
...

대략적으로 이런 것이다. type이라는 하나의 인터페이스를 제공해준다. 위에서 봤던 예시와 같지 않은가?

맞다. 구조체 내부에 typedef로 재정의 되어있기 때문에 템플릿에서 사용하게 되면 typename 지정자를 붙여주어야 한다.

 

std::remove_const<T>::type // C++11
std::remove_const_t // C++14

std::remove_reference<T>::type // C++11
std::remove_reference_t // C++14

std::add_lvalue_reference<T>::type // C++11
std::add_lvalue_reference_t<T> // C++14

여담으로 C++14부터는 using으로 만든 별칭 템플릿도 추가되어있다.

 

template<typename T>
using remove_const_t = typename remove_const<T>::type;

template<typename T>
using remove_reference_t = typename remove_reference<T>::type;

이런 식으로 되어있다.

 

◾ typedef는 템플릿화를 지원하지 않지만, 별칭 선언은 지원한다.

◾ 별칭 템플릿에서는 "::type" 접미어를 붙일 필요가 없다. 템플릿 안에서 typedef를 지칭할 때에는 "typename" 접두사를 붙여야 하는 경우가 많다.

◾ C++14는 C++11의 모든 타입 특성 변환에 대한 별칭 템플릿들을 제공한다.

리터럴 0은 int 타입이지 포인터 타입이 아니다. NULL 역시 마찬가지로 포인터 타입이 아니다. 그저 암시적으로 널포인터로 해석할 뿐이다.

 

void f(int);
void f(bool);
void f(void*);

f(0); // f(int)
f(NULL); // f(int)

둘 다 매개변수가 int인 함수를 호출하게 된다. 만약 NULL이 0L(long)로 정의되어 있다면 long->int, long->bool, 0L->void* 변환의 우선순위가 모두 동일하기 때문에 중의적 호출이 되어버린다. 어쨌든 코드의 의도와 실행이 모순된다.

 

nullptr은 사실 엄밀히 따지면 포인터 타입도 아니다. 실제 타입은 std::nullptr_t인데 다시 nullptr로 정의될 뿐이다. 그리고 nullptr은 모든 원시 포인터 타입으로 암묵적인 변환이 이루어지기 때문에 마치 모든 타입의 포인터처럼 행동할 수 있다.

 

f(nullptr); // f(void*)

nullptr을 인수로 넘겨줌으로써 의도와 실행이 일치하게 된다.

이뿐만 아니라 auto변수와 같이 사용될 때 코드의 가독성도 좋아지는 역할도 하게된다.

 

여담으로 위의 코드와 같이 함수의 매개변수로 정수 타입과 포인터 타입으로 오버로딩 되어있는 경우에는 중의적인 호출이 이뤄질수 있기 때문에 C++98에서는 정수 타입과 포인터 타입에 대한 함수 오버로딩을 피하라는 지침을 따랐고 nullptr이 등장한 지금도 여전히 유효한 지침이다. 왜냐하면 아직까지도 0과 NULL을 사용하는 개발자들이 여전히 존재할 것이기 때문이다.

 

auto result = findRecord(/* params */);

if (result == 0) { // result는 무슨 타입?
...
}

코드를 보자마자 result의 타입이 정수인지 포인터인지 알 방법이 없다. 함수의 정의를 직접 찾아봐야 알 수 있을것이다.

 

if (result == nullptr) {
...
}

하지만 이제는 명확히 알 수 있다. 어떤 타입의 포인터인지는 정의를 봐야겠지만 어쨌든 포인터 타입이라는것은 명백해졌다.

 

nullptr은 템플릿과 함께할 때 더욱 빛난다. 예시를 보면서 얘기해보자.

 

int f1(std::shared_ptr<Widget> spw);
double f2(std::unique_ptr<Widget> upw);
bool f3(Widget* pw);

std::mutex f1m, f2m, f3m;
using MuxGuard = std::lock_guard<std::mutex>;
...
{
    MuxGuard g(f1m);
    auto result = f1(0); // 0->nullptr 암시적 변환
}
...
{
    MuxGuard g(f2m);
    auto result = f2(NULL); // NULL->nullptr 암시적 변환
}
...
{
    MuxGuard g(f2m);
    auto result = f3(nullptr);
}

암시적 변환이 일어난건 둘째치고 코드의 중복이 심각하다.

 

template<typename FuncType, typename MuxType, typename PtrType>
decltype(auto) lockAndCall(FuncType func, MuxType& mutex, PtrType ptr) // C++14
{
    using MuxGuard = std::lock_guard<MuxType>;
    
    MuxGuard g(mutex);
    return func(ptr);
}
...
auto result1 = lockAndCall(f1, f1m, 0); // error
auto result2 = lockAndCall(f2, f2m, NULL); // error
auto result3 = lockAndCall(f3, f3m, nullptr); // ok

템플릿 타입 추론에 의해서 0, NULL은 널포인터로 암시적 변환이 이루어지지 않는다.

 

auto result1 = lockAndCall(f1, f1m, 0);

함수가 호출될 때 0은 int로 추론되어서 f1의 인수로 넘겨지게 되는데, f1의 매개변수 타입은 std::shared_ptr<Widget>이기 때문에 에러가 발생하게 된다. 하지만 nullptr로 넘겨진다면 lockAndCall에서 std::nullptr_t로 추론되었다가 f3의 인수로 넘겨질 때 Widget* 타입으로 암시적 변환이 이루어지게 되므로 정상적으로 동작하게 된다.

 

대단한 내용은 아니지만 글이 길어졌다. 어쨌든 널 포인터를 지정할 거라면 0이나 NULL대신 nullptr을 사용하라는 얘기이다.

 

◾ 0과 NULL보다 nullptr을 선호하라.

◾ 정수 타입과 포인터 타입에 대한 오버로딩을 피하라.

int x(0);
int y = 0;
int z{0};

C++11부터 객체 생성 구문이 다양해졌다.

 

int z = {0};

이런 것도 있는데 이번 항목에서는 다루지 않는다. 대체로 그냥 중괄호만 쓴것과 동일하게 취급된다.

 

중괄호 초기화는 컨테이너, 비정적 멤버, 복사 불가능한 객체(예:atomic)에 모두 사용이 가능하다. 나머지 두 종류는 경우에 따라 사용가능 여부가 달라진다. 그래서 중괄호 초기화를 균일 초기화라고도 부른다.

 

double x, y, z;
...
int sum1{x + y + z};

중괄호 초기화의 기능 중 하나는 암묵적인 타입 변환을 방지한다는 것이다.

만약 중괄호 초기화가 아니라면, x + y + z는 계산 결과가 암시적 변환을 통해 int로 초기화 된다.

 

Widget w1(10); // 생성자 호출
Widget w2(); // 기본 생성자 호출.. 을 하려고 했지만 w2라는 함수를 선언하게 된다
Widget w3{}; // 기본 생성자 호출

또 하나의 기능은 성가신 구문 해석(most vexing parse)에서 자유로워진다.

w2라는 객체의 기본 생성자를 호출하려고 했지만, 선언으로 해석될 수 있기때문에 컴파일러는 선언으로 해석해버린다. 하지만 중괄호 초기화는 의도한대로 기본 생성자를 호출하게된다.

 

하지만 auto와 다르게 중괄호 초기화를 선호하는것은 안된다. 반드시 상황에 맞춰서 적절하게 사용해야 한다.

몇 가지 예시를 살펴보자.

 

class Widget {
public:
    Widget(int i, bool b); // 1
    Widget(int i, double b); // 2
};

Widget w1(10, true); // 1
Widget w2{10, true}; // 1
Widget w3(10, 5.0); // 2
Widget w4{10, 5.0}; // 2

초기화의 종류에 상관없이 생성자 두가지 중 한가지에 대응된다.

 

class Widget {
public:
    ...
    Widget(std::initializer_list<long double> il); // 3
};

Widget w1(10, true); // 1
Widget w2{10, true}; // 3. long double로 형변환이 강제로 일어남
Widget w3(10, 5.0); // 2
Widget w4{10, 5.0}; // 3. long double로 형변환이 강제로 일어남

그런데 생성자 중에서 하나 이상이 초기화 리스트 타입의 매개변수를 선언한다면 중괄호 초기화 구문은 초기화 리스트를 매개변수로 받는 생성자를 강하게 선호한다.

그냥 선호도 아니고 왜 강하게 선호하냐면, 초기화 리스트를 받는 생성자 호출로 해석될 여지가 아주 조금이라도 있으면 컴파일러는 그 해석을 반드시 선택하기 때문이다.

심지어 이미 존재하는 생성자들(1, 2)보다 더 부합되지 않는 매개변수 타입이더라도 말이다.

 

class Widget {
public:
    ...
    operator float() const; // float로 변환
};

Widget w5(w4); // 복사 생성자 호출
Widget w6{w4}; // 초기화 리스트 생성자 호출. float로 변환되고 그게 다시 long double로 변환됨

Widget w7(std::move(w4)); // 이동 생성자 호출
Widget w8{std::move(w4)}; // 초기화 리스트 생성자 호출. float로 변환되고 그게 다시 long double로 변환됨

강하게 선호하다보니 복사 생성이나 이동 생성이 일어나는 경우에도 초기화 리스트 생성자가 끼어들게 된다.

 

class Widget {
public:
    ...
    Widget(std::initializer_list<bool> il); // long double -> bool
};

Widget w{10, 5.0};

long double일때는 더 큰 타입이라서 암시적 변환이 이뤄지기때문에 허용됐지만 이번에는 더 좁은 타입(int, double -> bool)으로 변환되어야 하기 때문에 컴파일러가 거부를 한다.

이미 매개변수의 타입이 완벽하게 부합하는 생성자가 존재함에도 불구하고 중괄호 초기화를 썼기때문에 초기화 리스트 생성자를 호출하려고 시도하는 것이다.

 

초기화 리스트 생성자가 존재하고 중괄호 초기화를 했음에도 다른 버전의 생성자를 호출하는 경우가 있긴 있다.

bool<->string처럼 암시적 변환 자체가 불가능해서 아예 방법이 존재하지 않을때만 다른 버전의 생성자를 호출한다.

int, double은 bool로 타입 변환시 비트가 깎여서 본래의 값을 유지하지 못할 경우가 훨씬 많지만 어쨌든 '가능'하기 때문에 상황이 달랐던것이다.

 

class Widget {
public:
    Widget();
    Widget(std::initializer_list<int> il);
};

Widget w1{}; // 이런 경우는?
Widget w2{{}};
Widget w3({});

만약 생성자가 위와 같이 단 두개만 존재하고 인수가 없는 생성자를 호출한다고 하면 과연 어떤 생성자가 호출될까?

이 경우는 비어있는 초기화 리스트라고 판단하지 않고 인수 자체가 없다고 판단하여 기본 생성자를 호출하게 된다.

혹시라도 초기화 리스트 생성자를 호출하고 싶다면 w2, w3처럼 사용시 비어있는 초기화 리스트라고 판단해서 초기화 리스트 생성자를 호출한다.

 

중괄호 초기화와 생성자 오버로딩에 따라 결과가 아예 천차만별로 나오는 사례를 하나 보도록 하자.

 

std::vector<int> v1(10, 20); // size:10, 값:모두20
std::vector<int> v2{10, 20}; // size:2, 값:10, 20

사용되는 생성자가 서로 달라서 결과가 아예 다르기때문에 주의해야한다.

 

마지막으로 생성자 오버로딩과 초기화 리스트에 관련되어 알아야 할 사항 두가지를 언급한다.

 

첫번째, 클래스를 작성할 때 초기화 리스트를 매개변수로 받는 생성자가 하나 이상 존재하게 한다면, 중괄호 초기화 구문 사용시 해당 생성자만 적용될 수 있음을 반드시 알아야 한다. 초기화 리스트 생성자는 다른 생성자들과 경쟁하는 수준이 아니라 아예 제외될 정도로 가려버리기 때문이다.

 

두번째, 객체 생성시 괄호와 중괄호를 신중하게 선택해야 한다. 괄호나 중괄호만 할 수 있는 기능을 파악해두면 선택하는데에 있어서 어려움은 없을 것이다.

 

또 한가지, 템플릿 작성시에도 고려해야 한다.

 

◾ 중괄호 초기화는 가장 광범위하게 적용할 수 있는 초기화 구문이며, 좁히기 변환을 방지하며, C++의 가장 성가신 구문 해석에서 자유롭다.

◾ 생성자 오버로딩 해소 과정에서 중괄호 초기화는 가능한 한 std::initializer_list 매개변수가 있는 생성자와 부합한다(심지어 겉으로 보기에 그보다 인수들에 더 잘 부합하는 생성자들이 있어도).

◾ 괄호와 중괄호의 선택이 의미 있는 차이를 만드는 예는 인수 두 개로 std::vector<타입>을 생성하는 것이다.

◾ 템플릿 안에서 객체를 생성할 때 괄호를 사용할 것인지 중괄호를 사용할 것인지 선택하기가 어려울 수 있다.

std::vector<bool> features(const Widget& w) { ... }
...
auto highPriority = features(w)[5]; // std::vector<bool>::operator[](5)
/* 둘은 같은 코드일까? */
bool highPriority = feature(w)[5];

정답부터 얘기하면 둘의 타입은 다르다.

vector<bool>은 유일하게 템플릿 특수화에 의해 만들어지는 컨테이너이다. 또한 컨테이너의 요소를 바이트가 아닌 비트로 관리하고 있다.

그런데 C++에서 비트에 대한 참조는 금지되어있다는 사실을 안다면 vector<bool>::operator[]는 bool& 타입을 반환하지 못한다는 사실 역시 알 수 있다.

대신 마치 bool&처럼 작동하는 객체를 돌려주는 우회법을 사용한다.

 

vector<bool>::operator[]는 bool&이 아닌 vector<bool>::reference 타입을 반환해준다. 그리고 해당 타입은 실제 bool 타입에 대한 암시적 변환이 가능하게 해준다.

 

auto highPriority1 = features(w)[5]; // highPriority1 = std::vector<bool>::reference
bool highPriority2 = feature(w)[5]; // std::vector<bool>::reference의 bool로 암시적 변환

auto highPriority의 타입은 std::vector<bool>::reference가 되고 bool highPriority의 타입은 std::vector<bool>::reference의 암시적 변환 허용에 의해 정상적인 값이 대입된다. 물론 반환받은 값은 실제 vector의 요소가 아닌 임시로 생성된 bool 객체이다.

 

std::vector<bool>::reference는 스마트 포인터와 같이 프록시 패턴이 적용된 프록시 클래스의 일종이다.

operator[]가 마치 비트에 대한 참조를 돌려주는 듯한 환상을 제공해주는 것이다. 그런데 스마트 포인터와는 조금 다르게 캡슐화가 더 잘 되어있을 뿐이다.

 

 

Matrix sum = m1 + m2 + m3 + m4;

프록시 클래스는 표현식 템플릿 기법을 사용하는 라이브러리에서도 흔히 사용된다.

Matrix::operator+의 결과로 Matrix를 반환하는게 아니라 Sum<Matrix, Matrix>같은 프록시 클래스의 객체를 돌려주도록 하면 계산이 조금 더 효율적이다.

그런데 이걸 auto로 선언한다면 Sum<Sum<Sum<Matrix, Matrix>, Matrix>, Matrix> 같은 타입이 되기 때문에 사용자에게 보여줄 필요가 없다.

그래서 대체로 위와 같은 캡슐화가 잘된(보이지 않는) 프록시 클래스는 auto와 그다지 잘 맞지 않는다.

 

그러면 대체 어떻게하면 보이지 않는 프록시 클래스를 사용하고 있는지 파악할 수 있을까?

답은 의외로 간단하다. 타입 추론을 우리가 의도하는 타입으로 강제하는 것이다.

 

auto highPriority = static_cast<bool>(features(w)[5]);

auto sum = static_cast<Matrix>(m1 + m2 + m3 + m4);

auto ep = static_cast<float>(calcEpsilon()); // 본래 반환값은 double

일부러 타입을 변환한다는 의도도 명확히 드러낼 수 있다.

 

◾ "보이지 않는" 프록시 타입 때문에 auto가 초기화 표현식의 타입을 "잘못" 추론 할 수 있다.

◾ 타입 명시 초기치 관용구는 auto가 원하는 타입을 추론하도록 강제한다.

+ Recent posts