Modern C++ 강의 정리 – C++ 기능들

Enums

Enum Type은 enum 키워드로 정의할 수 있으며 정의하고 싶은 값에 대해 심볼을 연결시킬 수 있다.
Enum은 내부적으로는 integral types으로 표시되어 Enum은 integer로 암시적 변환이 가능하며 그 반대는 성립하지 않는다.
Enum의 디폴트 값은 0부터 시작하지만, 사용자가 임의로 값을 할당할 수 있다.
Enum은 매크로 상수가 아니며, 자신이 정의된 Scope 내에서만 Visible하다.

enum OS{LINUX, WINDOWS, ANDROID};
void printOSName(OS os) {
    if (os == LINUX) {
        std::cout << "LINUX" << std::endl;
    }
    else if (os == WINDOWS) {
        std::cout << "WINDOWS" << std::endl;
    }
    else if (os == ANDROID) {
        std::cout << "ANDROID" << std::endl;
    }
}
int main()
{
    OS os = LINUX;
    printOSName(os); // LINUX
    printOSName(WINDOWS); // WINDOWS
    printOSName(static_cast<OS>(2)); // ANDROID
    return 0;
}

위 코드에서 Main 함수는 각 줄에서 각 Enum 값을 함수에 전달한다. 세번째 호출에서 2는 OS로 암시적 변환이 불가능하기 때문에 정적 캐스팅을 사용했다.

Enum에서 각 열거자(enumerator)들은 서로 다르게 정의된 열거형에서 중복될 수 있다. 예를 들어 enum Color의 RED와 enum Team의 RED가 중복되는 경우가 있다. 이 때 enum class를 사용해서 스코프를 사용하면 이런 충돌을 막을 수 있다.

#include <iostream>

enum class OS{LINUX, WINDOWS, ANDROID};
void printOSName(OS os) {
    if (os == OS::LINUX) {
        std::cout << "LINUX" << std::endl;
    }
    else if (os == OS::WINDOWS) {
        std::cout << "WINDOWS" << std::endl;
    }
    else if (os == OS::ANDROID) {
        std::cout << "ANDROID" << std::endl;
    }
}

enum class ProjectName{SEARCH, ANDROID};

int main()
{
    OS os = OS::LINUX;
    printOSName(os);
    printOSName(OS::WINDOWS);
    printOSName(static_cast<OS>(2));
    return 0;
}

다음과 같이 enum class를 사용해 Enum을 정의한 후 Name을 Scope로 사용해 심볼이 같은 Enumerator를 구별할 수 있다.


Strings

C++에서는 Raw한 문자열로 Char *를 지원한다. Char *를 사용할 때는 할당된 배열의 길이에 매우 주의해야한다. 예를 들어 Length가 10인 Char 배열은 최대 9개의 문자만을 저장할 수 있다. 문자의 끝을 Null로 구분하기 때문이다. 이 때문에 두 문자를 합친다든지 복사 한다는지의 연산에서 사용자는 항상 그 길이에 대해 신경써야하고 에러를 유발하기 쉽다.

String 클래스는 그 모든 복잡한 과정을 알아서 다 처리해준다. 초기화 및 대입에 있어서 Raw String도 사용할 수 있으며 String 객체 끼리 합치는 연산도 단순히 ‘+’ 연산자만 사용하면 된다. 문자열 길이 또한 알아서 관리하기 때문에 별도의 변수를 유저가 따로 정의할 필요도 없다.

#include <iostream>
#include <string>
using namespace std;

int main()
{
    // Initialize & assign
    string s = "hello";
    s = "hello world";
    
    // Access
    s[0] = 'a';
    cout << s[0] << endl;
    
    // Size
    cout << s.length() << endl;

    // Insert & concatenate
    string s1{"Hello "}, s2{"World!"};
    s = s1 + s2;
    cout << s << endl;
    
    // Comparison
    cout << (s1 == s2) << endl;
    cout << s1.compare(s2) << endl;
    
    // Removal
    s.erase(0, 5);
    cout << s << endl;
    
    // Search
    auto pos = s.find("World", 0);
    if (pos != std::string::npos) {
        cout << "Found" << endl;
    }
    
    return 0;
}

위와 같이 초기화 / 대입 / 인덱스를 통한 접근 / 합성 / 비교 / 삭제 / 검색 / 길이 등을 매우 쉽게 사용할 수 있다.


stringstream / istringstream / ostringstream

stringstream은 String 오브젝트를 stream화 시킬 수 있다.
<< 연산자와 >> 연산자 모두를 사용할 수 있어 stream에 string을 추가할 수도 있고 읽을 수도 있다.
>> 연산자로 stringstream에서 읽을 때는 띄워쓰기에 따라 stream 내부의 문자열이 구분된다.

#include <iostream>
#include <string>
#include <sstream>
using namespace std;

int main()
{
    stringstream ss("abc def gae");
    string word;
    int count = 0;
    while (ss >> word) {
        ++count;
    }
    cout << count;
    return 0;
}

stringstream의 멤버 함수인 str()은 stringstream이 저장하고 있는 string 객체의 복사본을 리턴한다. istringstream과 ostringstream은 각각 읽기와 쓰기만 할 수 있다고 생각하면 된다.


User defined Literals

C++에서는 integer, float, string 등의 여러 타입의 리터럴을 제공한다. 14.0f나 322L 또는 L”hello”와 같은 것들이 그 예다. C++11에서 부터는 사용자도 리터럴을 정의할 수 있는데, 이를 통해 코드를 간결히 만들고 타입 안정성을 늘릴 수 있다.

<return type> operator”” _<literal>(<arguments>

사용자 정의 리터럴은 위와 같은 문법에 의해 정의될 수 있다.

#include <iostream>
using namespace std;
class Dist {
public:
    Dist(long double km): m_km(km) {
    }
    long double m_km;
};
long double operator"" _m(long double val) {
    return val / 1000;
}
int main()
{
    Dist d1(31000.0_m);
    cout << d1.m_km;
    return 0;
}

위 코드에서 생성자의 인자로 넘겨진 31000.0_m은 사용자 정의 리터럴에 따라 1000으로 나누어지고, 그 값에 따라 Dist가 초기화되는 것을 확인할 수 있다.


constexpr

constexpr 키워드는 해당 표현식이 상수라는 것을 의미한다. 따라서 컴파일 타임에 그 값이 결정될 수 있다는 말이다. 이 표현식은 변수 뿐만이 아닌 함수에도 적용될 수 있다. constexpr을 사용하는 이유라고 하면 컴파일 타임에 연산이 끝나기 때문에 런타임 연산 속도가 빨라지기 때문이라고 할 것이다.

const와의 차이라고 한다면 const 키워드가 붙은 변수는 그 초기화가 상수로 주어지는게 아닌 이상은 런타임에 초기화 될 값이 정해지기 때문에 컴파일 타임에 그 값을 모를 수 있다.

함수에도 constexpr을 붙일 수 있다. 이 때 함수의 return type은 반드시 리터럴이어야 한다. 함수가 호출될 때 그 인자가 무조건 상수일 필요는 없다. 만약 상수일 경우 컴파일 타임에 그 값이 결정되며, 상수가 아닐 경우에는 런타임에 함수가 실행된다. 물론 그 값이 상수일 때만 그 결과를 constexpr로 취급할 수 있다.


초기화자 리스트 (Initializer_list<T>)

초기화자 리스트는 동일한 타입 T에 대해서 오브젝트의 배열을 나타낼 수 있다. 실제 컨테이너는 아니지만 컨테이너같은 행위를 할 수 있다고 한다.

#include <iostream>
using namespace std;
class Bag {
public:
    int item[10];
    int size{0};
    Bag(initializer_list<int> list) {
        for(auto item : list) {
            this->item[this->size++] = item;
        }
    }
};
int main()
{
    Bag bag = {1, 2, 3, 4};
    cout << bag.size;
    return 0;
}