enum class

 

인간은 문자와 기호 그리고 숫자를 이용하고 컴퓨터는 모든 것을 숫자로 받아들인다. 그렇기 때문에 인간이 알아보기 쉽게 문자열로 만들면 프로그램이 느려지고 컴퓨터가 알아보기 쉽게 숫자로 만들면 인간이 알아보기가 어려워진다. 이것을 해결하기 위한 방법의 하나가 바로 enum(열거형)이다.

 

이 enum은 일반적으로 이름을 가지는 정수형의 상수를 정의해서 알아보기 쉽게 해주는데, 이 기존의 enum은 몇가지의 문제점이 존재한다.

 

 

1. 열거형에 존재하는 이름과 같은 이름의 클래스를 선언할 수 없다.

일반적인 enum 내에 선언된 열거자의 이름은 전역적이다. 그렇기 때문에 enum 내부에 선언된 이름과 같은 이름을 가진 클래스를 선언하고 사용하려고 하면 컴파일 에러가 발생한다. 다음의 코드는 이것에 대한 예시이다.

 

#include <iostream>
using namespace std;

enum EColor
{
    Red,
    Green,
    Blue,
};

class Red { };

int main()
{
    Red r;    // 컴파일 에러가 발생한다.
    EColor color = Red;

    return 0;
}

 

위와 같은 일이 발생하는 이유는 MSDN에 다음과 같이 설명되어 있다.

 

enum EColor
{
    Red,
    Green,
    Blue,
};

 

위와 같이 정의된 enum의 의미 체계는 다음과 같다고 한다.

 

static const int Red = 0;
static const int Green = 1;
static const int Blue = 2;

 

다시 말하자면 이미 전역 변수에 같은 이름의 변수가 선언되어 있기 때문에 같은 이름의 클래스를 선언할 수 없는 것이다. 하지만 enum class로 정의된 열거형의 경우에는 위와 같은 문제가 발생하지 않는다.

 

#include <iostream>
using namespace std;

enum class EColor
{
    Red,
    Green,
    Blue,
};

class Red { };

int main()
{
    Red r;    // 컴파일 에러가 발생하지 않는다.
    EColor color = EColor::Red;    // 대신 접근지정자를 이용해 스코프를 지정해주어야 한다.

    return 0;
}

 

열거형을 표준 열거형이 아닌 enum class로 선언하면 열거형 내부에 존재하는 이름으로 클래스를 선언해도 문제가 발생하지 않는다. 하지만 표준 열거형에서는 Red라는 이름만으로 열거형 변수에 값을 넣어줄 수 있었지만 enum class 열거형에서는 열거형 변수에 값을 넣어주기 위해서는 반드시 접근지정자를 이용해서 스코프를 지정해주어야만 컴파일 에러 없이 값을 대입할 수 있게 된다. 이러한 형태의 이유 역시 MSDN에 설명되어 있는데 그것을 보면 왜 이런 형태를 가지는지 충분히 이해할 수 있게 된다.

 

enum class EColor
{
    Red,
    Green,
    Blue,
};

 

enum class로 선언된 EColor의 경우에는 다음과 같은 의미 체계를 가지게 된다.

 

class EColor
{
public:
    static const int Red = 0;
    static const int Green = 1;
    static const int Blue = 2;
};

 

위와 같은 형태를 가지게 된다는 것을 생각해보면 왜 enum class 열거형 변수에 스코프를 지정해 주어야하는지 명확하게 이해할 수 있다.

 

 

 

 

 

2. 표준 열거형은 타입 세이프(Type safe)하지 않다.

일반적으로 프로그래머들은 언어의 동작에서 프로그래머가 무의식적으로 실수를 유도하게 되는 동작을 좋아하지 않는다. 만약 프로그래머가 명시적으로 한 행동에서 문제가 발생하면 명백한 프로그래머의 실수이고 그 문제점을 찾아내기도 쉽지만, 프로그래머가 명시하지 않았는데 언어가 묵시적으로 처리해서 발생하는 문제는 찾아내기 어렵기 때문이다.

 

이러한 측면에서 표준 열거형이 발생시키는 문제가 몇 가지 있다.

 

첫 번째 문제로는 정수형의 변수에 곧바로 대입이 가능하다는 것이다(하지만 정수형에서 열거형으로의 대입은 명시적인 형 변환이 필요하다). 이것으로 인해서 발생하는 문제점에 대해서 경험이 많지 않아서 언급하지는 못하겠지만, 이런 묵시적인 동작으로 인해서 프로그래머가 실수를 할 가능성이 커지는 것은 부인할 수 없는 문제이다.

 

#include <iostream>
using namespace std;

enum EColor
{
    Red,
    Green,
    Blue,
};

int main()
{
    EColor color = Red;
    int i = color;    // 바로 대입이 가능하다.

    return 0;
}

 

하지만 열거형을 enum class로 선언할 경우, 다음과 같이 더 이상 명시적으로 형 변환을 거치지 않는 이상 열거형 변수나 값은 정수형에 대입할 수 없게 된다.

 

#include <iostream>
using namespace std;

enum class EColor
{
    Red,
    Green,
    Blue,
};

int main()
{
    EColor color = EColor::Red;
    int i = static_cast<int>(color);    // 명시적인 형 변환을 거쳐야만 대입이 가능하다.

    return 0;
}

 

두 번째 문제는 명시적인 형 변환 없이도 다른 열거형 간의 비교 연산이 가능하다는 것이다.

 

#include <iostream>
using namespace std;

enum EColor
{
    Red,
    Green,
    Blue,
};

enum EDirection
{
    Forward,
    Backward,
    Right,
    Left,
    Up,
    Down,
};

int main()
{
    EColor color = Red;
    EDirection dir = Up;
   
    if (color == dir)    // 문제없이 비교되고 컴파일 된다.
    {
        cout << "같다" << endl;
    }

    return 0;
}

 

이것이 무슨 문제가 되는지 이해하지 못할 수도 있겠지만, 위의 코드를 살펴보면 EColor 열거형과 EDirection 열거형의 종류와 내용, 그 의미는 명백하게 다르다. 이러한 상황을 프로그래머가 의도한 것이라면 모르겠지만, 이런 상황을 의도하지 않았고 실수로 전혀 다른 열거형을 비교하게 된 것이라면 문제가 될 것이고, 이러한 문제는 컴파일러가 프로그래머에게 알려주지 않기 때문에 찾아내기도 쉽지 않을 것이다.

 

하지만 열거형을 아래와 같이 enum class로 선언하면 명시적인 형 변환을 거치지 않는 이상 다른 열거형에 대한 비교 연산은 불가능해진다.

 

#include <iostream>
using namespace std;

enum class EColor
{
    Red,
    Green,
    Blue,
};

enum class EDirection
{
    Forward,
    Backward,
    Right,
    Left,
    Up,
    Down,
};

int main()
{
    EColor color = EColor::Red;
    EDirection dir = EDirection::Forward;
   
    if (color == (EColor)dir)    // 명시적으로 형 변환을 거친 후에야 비교 연산이 가능해진다.
    {
        cout << "같다" << endl;
    }

    return 0;
}

 

이러한 동작은 프로그래머의 무의식적인 실수로 인한 버그를 막아주는 것이고, 문제를 발생시킬 수 있는 동작의 경우에는 프로그래머가 의도적으로 코드를 작성해야 하고 그로인해 발생할 문제에 대한 대처와 책임을 충분히 해야한다는 것을 의미한다.

 

[투네이션 후원]

https://toon.at/donate/637735212761460238

 

[유니티 어필리에이트 프로그램]

아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.

- 유니티 에셋스토어 : https://assetstore.unity.com/?aid=1100lkbzf

- 유니티 플러스 : https://prf.hn/click/camref:1100lkbzf/creativeref:1011l61476

- 유니티 프로 : https://prf.hn/click/camref:1100lkbzf/creativeref:1101l61542

 

[디스코드 채널]

https://discord.gg/tqmRTy4pgk

반응형

'C++' 카테고리의 다른 글

[C++11] enum class  (1) 2017.07.17
[C++ 11] static_assert  (0) 2017.05.23
[C++ 11] Auto Vectorization  (1) 2016.11.01
[C++ 11] Range-Based For  (0) 2016.11.01
[C++ 11] Scoped Lock  (0) 2016.11.01
  1. 2022.01.14 14:57

    좋은 글 정말 감사합니다 :)

요즘 칩에는 벡터 레지스터라는 개념으로 메모리를 배치하는데, 이 벡터 레지스터 하나에 정수형이나 실수형 4개를 한꺼번에 담아둘 수 있다고 한다.


loop 작업시 float이나 int 형식의 4바이트 변수의 배열에 대해 하나씩 훑으며 작업할 때 굳이 하나씩 작업하지 않고 4개를 묶어서 16바이트를 벡터 형식으로 한 번에 처리할 수 있고 이것은 물론 스칼라 형식으로 하나씩 작업하는 것보다 빠르다(이론 상 최대 4배까지 빠를 수 있다).


이런 것을 Vectorization이라고 한다. 일반적으로 최신 컴파일러는 이런 Vectorization을 자동으로 수행해준다.


어떤 조건하에선 수행하지 못하는 경우가 있다. 수백만줄의 코드를 모두 체크하지는 못하기 때문에 이 체크에 실패하면 Auto Vectorization에 실패한다.


Auto Vectorization에 실패했을때 알아내는 방법은 프로그래머는 어셈블리어 코드를 확인하는 방법 밖에 없는데 이것은 어려운 일이다

그리고 Debug 빌드에서는 최종 코드를 내는 것이 아니기 때문에 최적화 기능의 상당부분이 꺼져있어서 Vectorization 또한 수행되지 않는다.

 

10000개 짜리 float 배열 두 개에 대한 loop 작업을 10000회를 수행했을 때의 결과물이다.

 

Release : x64

역으로 성능이 감소하는 케이스가 발생하기는 하지만 대부분 1%이하의 확률이다.

평균적으로 40~50%의 성능 향상을 기대할 수 있다.

 

Release : x86

x64 버전과 거의 비슷한 수준의 결과물을 얻을 수 있다.

반복적으로 실험한 결과 평균 성능 향상 비율을 2~10% 가량 떨어진다.

 

Debug

Debug 빌드에서는 최적화 기능이 꺼져있기 때문에 성능 향상이 거의 없음을 알 수 있다.



Vectorization 성능 비교 코드

#include <iostream>
#include <string>
#include <Windows.h>
using namespace std;
#define ARRSIZE 100000
class Vectorization
{
private:
    int mWorkCount;
    __int64 mTotalVectorTime;
    __int64 mTotalNoVectorTime;
    __int64 mNowVectorTime;
    __int64 mNowNoVectorTime;
    float mMinImproveTime;
    float mMaxImproveTime;
    int mPerformDecreseCase;
    int mPerformIncreseCase;
    int mTotalWorkCount;
public:
    Vectorization() : mWorkCount(0), mTotalVectorTime(0), mTotalNoVectorTime(0),
        mNowVectorTime(0), mNowNoVectorTime(0), mMinImproveTime(0.f), mMaxImproveTime(0.f),
        mPerformDecreseCase(0), mPerformIncreseCase(0), mTotalWorkCount(0)
    {}
    void CalculateWorkTime(float* a, float* b, int arrsize, bool isVector)
    {
        __int64 t1;
        __int64 t2;
        for (int i = 0; i < arrsize; i++)
        {
            a[i] = b[i] = i;
        }
        QueryPerformanceCounter((LARGE_INTEGER*)&t1);
        if (isVector)
        {
            #pragma loop(vector)
            for (int i = 0; i < arrsize; i++)
            {
                a[i] += b[i];
            }
        }
        else
        {
            #pragma loop(no_vector)
            for (int i = 0; i < arrsize; i++)
            {
                a[i] += b[i];
            }
        }
        QueryPerformanceCounter((LARGE_INTEGER*)&t2);
        if (isVector)
        {
            mTotalVectorTime += mNowVectorTime = t2 - t1;
        }
        else
        {
            mTotalNoVectorTime += mNowNoVectorTime = t2 - t1;
        }
    }
    void CalcNowWorkImprovePerform()
    {
        float ImproveRate = 100.0f - (float)mNowVectorTime / (float)mNowNoVectorTime * 100.0f;
        if (ImproveRate < mMinImproveTime)
        {
            mMinImproveTime = ImproveRate;
        }
        else if (ImproveRate > mMaxImproveTime)
        {
            mMaxImproveTime = ImproveRate;
        }
        if (ImproveRate < 0)
        {
            mPerformDecreseCase++;
        }
        else
        {
            mPerformIncreseCase++;
        }
        cout << "현재 벡터화 작업시간 :: " << mNowVectorTime << endl;
        cout << "현재 비벡터화 작업시간 :: " << mNowNoVectorTime << endl;
        printf("현재 성능 향상 비율 :: %.2f %%\n\n", ImproveRate);
        mTotalWorkCount++;
    }
    void CalcTotalWorkImprovePerform()
    {
        float improveRate = 100.0f - (float)mTotalVectorTime / (float)mTotalNoVectorTime * 100.0f;
        cout << "총 벡터화 작업시간 :: " << mTotalVectorTime << endl;
        cout << "총 비벡터화 작업시간 :: " << mTotalNoVectorTime << endl << endl;;
        printf("최저 성능 향상 비율 :: %.2f %%\n", mMinImproveTime);
        printf("최고 성능 향상 비율 :: %.2f %%\n", mMaxImproveTime);
        printf("평균 성능 향상 비율 :: %.2f %%\n\n", improveRate);
        cout << "성능 감소 케이스 :: " << mPerformDecreseCase << "회" << endl;
        printf("성능 감소 케이스 비율 :: %.2f %%\n\n", (float)mPerformDecreseCase / (float)mTotalWorkCount * 100.f);
        cout << "성능 향상 케이스 :: " << mPerformIncreseCase << "회" << endl;
        printf("성능 향상 케이스 비율 :: %.2f %%\n\n", (float)mPerformIncreseCase / (float)mTotalWorkCount * 100.f);
        cout << "총 성능 비교 횟수 :: " << mTotalWorkCount << "회" << endl;
    }
};
 
int main()
{
    Vectorization v;
    float fArr1[ARRSIZE];
    float fArr2[ARRSIZE];
    for (int i = 0; i < 10000; i++)
    {
        v.CalculateWorkTime(fArr1, fArr2, ARRSIZE, true);
        v.CalculateWorkTime(fArr1, fArr2, ARRSIZE, false);
        v.CalcNowWorkImprovePerform();
    }
    v.CalcTotalWorkImprovePerform();
}


반응형

'C++' 카테고리의 다른 글

[C++11] enum class  (1) 2017.07.17
[C++ 11] static_assert  (0) 2017.05.23
[C++ 11] Auto Vectorization  (1) 2016.11.01
[C++ 11] Range-Based For  (0) 2016.11.01
[C++ 11] Scoped Lock  (0) 2016.11.01
  1. 해커 2021.10.02 18:47

    msvc 동작 방법에 대해서는 어떻게 알아낸 건가요?

C++ 11부터 지원되는 Range-based for(범위 기반 for)는 기존의 C++ STL의 컨테이너, 배열 등에서 반복문을 좀 더 쉽게 사용하게 해주는 기능이다.

 

이 기능은 Microsoft의 Visual C++ 기능인 for each문과 그 기능과 사용법이 비슷해서 쉽게 사용할 수 있다.

 

기존의 for문과 iterator를 이용한 방법과 Visual C++ for each문을 이용한 방법 그리고 C++ 11 표준의 Range-based for문을 이용한 방법을 비교하자면 아래와 같다.


vector<Object*> vect;

// 기존의 for문과 auto 키워드를 이용한 iterator 반복 처리
for (auto obj = vect.begin(); obj != vect.end(); obj++)
{
    // Object에 처리
}

// Visual C++ 타입의 for each문을 이용한 반복 처리
for each(auto obj in vect)
{
    // Object에 처리
}
// C++ 11타입의 Range-based for문을 이용한 반복 처리
for (auto obj : vect)
{
    // Object에 처리
}


코드를 살펴보면 분명하게 미세하게나마 코드의 양이 줄어드는 것이 보인다. 같은 동작을 하는 데도 기존의 for문과 auto 형식의 복잡한 타이핑 없이 처리할 수 있다는 것은 프로그래머의 피로도를 상당히 줄여준다.(물론 auto 키워드가 나오기 전에는 더더욱 복잡했었다.)

반응형

'C++' 카테고리의 다른 글

[C++11] enum class  (1) 2017.07.17
[C++ 11] static_assert  (0) 2017.05.23
[C++ 11] Auto Vectorization  (1) 2016.11.01
[C++ 11] Range-Based For  (0) 2016.11.01
[C++ 11] Scoped Lock  (0) 2016.11.01

+ Recent posts