본문 바로가기

3.구현/C or C++

[c++] override, const_iterator, noexcept, constexpr 사용하기

들어가기

이번에는 C++11에서 override, const_iterator, noexcept, constexpr에 대해서 살펴보자.

작성자: ospace114@empal.com, http://ospace.tistory.com/

override 선언

C++11에서는 파생 클래스(derivered class)에서 기반 클래스(base class)의 가상 함수를 재정의(override)할 때에 함수에 override을 붙인다. 기존에는 잘못으로 인한 잠재적 오류 가능성이 있다. override을 사용해서 명시적으로 지정하여 조건에 맞지 않으면 에러가 발행하도록 한다. 실제 사용 예를 보자.

class Base {
public:
    virtual void mf1() const;
    virtual void mf2();
};

class Drived : public Base {
public:
    virtual void mf1() const override;
    void mf2() override; // virtual 생략 가능
};

const_iterator

반복자를 사용하며 각 항목 변경이 필요없는 경우 const_iterator을 사용할 수 있다. C++98에도 있지만 사용하기 쉽지 않았다. 기존 begin(), end()은 const가 아니므로 const_iterator을 캐스팅해서 사용해야 한다. 이를 C++11에서는 cbegin(), cend()로 쉽게 접근할 수 있고 auto와 같이 활용함으로써 더 간단하게 사용할 수 있다.

std::vector<int> values;
auto found = std::find(values.cbegin(), values.cend(), 100);
values.insert(found, 100);

새로운 컨테이너인 경우 const_iterator가 없을 수 있다. 이때에 std::cbegin과 std::cend을 사용해서 일반화된 함수를 정의할 수 있다.

template<typename C, typename V>
void findAndInsert(C& cont, const V& targetVal, const V& insertVal) {
    using std::cbegin;
    using std::cend;
    auto found = std::find(cbegin(cont), cend(cont), targetVal);
    cont.insert(found, insertVal);
}

cbegin() 정의를 살펴보자.

template<typename C>
auto cbegin(const C& container) -> decltype(std::begin(container)) }
    return std::begin(container);
}

cbegin() 멤버함수를 호출하지 않고 const 컨테이너를 호출한다. const 컨테이너인 경우 const_iterator가 반환되고 이를 리턴한다. 그렇기에 cbegin() 멤버함수가 없는 컨테이너에서도 동작한다.

noexcept 사용

noexcept 예외가 발생하지 않을 함수에 선언하는 키워드이다. C++98에서도 예외 표기하는 방법은 있지만 권장하지 않는다. C++11에서는 함수가 예외가 없을 경우 noexcept로 명시적으로 선언할 수 있다. noexcept 하면 최적화에도 도움이 된다.

int f(int x) throw(); // C++98
int f(int x) noexcept; // C++11

C++98에서 예외 발생시 f() 호출시점까지 풀리면서 처리후 종료된다. C++11에서는 예외 발생시 호출이 풀릴 수도 있고 아닐 수도 있다. noexcept로 인해 예외가 함수 밖으로 전파되도 스팩 풀기 가능상태를 유지할 필요없다. 최적화기(optimizer)가 noexcept 함수에서 예외가 발생하지 않는다면 실행시점 스택을 풀기 가능상태로 유지할 필요가 없기에 최적화에 유리하다. throw()에는 이런 최적화가 없다.

noexcept에 유용한 함수로는 다음과 같다.

  • 이동연산
  • swap 함수
  • 메모리 해제 함수
  • 소멸자

값을 처리하는 중에 메모리 부족으로 재할당하고 기존 값을 이동해야 한다. 이때에 복사되는 경우는 예외 안전성을 보장할 수 있지만, 이동은 예외 발생시 이동했던 작업을 복원하지 못하기에 예외 안전성을 보장하지 못한다. C++11에서는 이동 연산이 예외가 발생하지 않는게 확실하다면 복사를 이동으로 대체할 수 있다. 그래서 표준 라이브러리에서는 “가능하면 이동하고 필요시 복사한다.” 전략을 취한다. noexcept로 예외 발생 하지 않다고 보장한다. std::move_if_noexcept가 이동 생성자 noexcept 여부에 따라 오른값으로 조건 캐스팅을 수행한다.

사용자 정의 swap()에서도 noexcept여부에 크게 의존한다.

template<class T, size_t N>
void swap(T (&a)[N], T(&b)[N]) noexcept(noexcept(swap(*a, *b)));

template<class T1, class T2>
struct pair {
    void swap(pair& b) noexcept(noexcept(swap(first, p.first))&
        noexcept(swap(second, p.second)));
};

조건부 noexcept 구문으로 noexcept 절 안에 표현식이 noexcept 여부에 의존한다. 사용자 정의 swap은 프로그래머에 의해 noexcept을 결정한다.

모든 메모리 해제 함수(operator delete, operator delete[])와 소멸자는 암묵적으로 noexcept로 별도 선언이 필요 없다. 만약 소멸자에서 예외가 발생한다면 예측할 수 없는 행동을 한다.

그외 대부분의 함수가 예외 중립적(exception-neutral)이고 noexcept을 사용할 수 없다. 예외 중립 함수는 예외를 통과시킨다. noexcept 함수로 했다가 해제하는 경우가 더 많은 문제가 발생하므로 예외 발생하지 않는게 확실한 경우가 아니면 noexcept을 사용하지않는게 좋다.

추가로 noexcept 함수에서 noexcept가 없는 함수인 예외 중립 함수를 호출할 수도 있다.

constexpr 사용

constexpr은 const와 비슷하다. 단지 차이점은 constexpr은 컴파일타임에 정해지고, const은 컴파일타임 또는 런타임에 정해진다. 이렇게 정의된 값은 정수 상수 표현식(integral constant expression)에서 사용할 수 있다.

int sz = 10;
constexpr auto arraySize = sz; // 에러: 상수 표현식으로 초기화 해야함
std::array<int, sz> data;      // 에러: 템플릿 인자가 상수 표현식이 아님

arraySize는 정해진 값을 저장해야 한다. sz는 런타임에 할당되기에 arraySize에는 할당할 수 없다. 또한 std::array에 크기도 정수 상수 표현식을 사용해야되는데 sz라는 변수를 사용하고 있다.

constexpr auto arraySize = 10;
std::array<int, arraySize> data;

만약 constexpr를 함수에 사용할 경우를 보자. 호출 구문에 대해 컴파일 타임에 상수인지 여부를 산출한다. 상수 표현식을 인자로 호출하는 경우에 contexpr로 취급되고, 그렇지 않으면 일반함수로 취급된다.

constexpr
int pow(int base, int exp) noexcept {
    int res = 1;
    for(int i=0; i<exp; ++i) res *= base;
    return res;
}

constexpr auto num = 5;
std::array<int, pow(num)> data;

constexpr 함수는 리터럴 형식(literal type)을 입력 받고 리턴해야 한다. C++11에서는 void을 제외한 나머지 내장 형식이 리터럴 형식이다.

클래스 생성자에도 constexpr을 지정할 수 있고 객체를 constexpr 객체로 생성할 수 있다.

class Box {
public:
    constexpr Box(int value) noexcept : value(value) {}
    constexpr int valueOf() const noexcept { return value; }
    void setValue(int value) noexcept { this->value = value; }
private:
    int value;    
};

constexpr
Box doubleOf(const Box& b) noexcept {
    return { b.valueOf() + b.valueOf() };
}

constexpr 생성자와 constexpr getter도 사용할 수 있다. constexpr 함수를 암묵적으로 const가 선언되므로 생략할 수 있다. 또한 constexpr 객체를 다음처럼 생성할 수 있다.

constexpr Box box(10);
constexpr Box box2 = doubleOf(box);

setValue()은 값을 변경하므로 constexpr을 선언할 수 없고 constexpr 객체는 고정된 값을 가지기에 setValue()을 호출할 수 없다.

constexpr이 많아지는 경우 컴파일 타임때 결정해야할 작업이 많아지므로 컴파일 시간이 길어질 수 있다. 그렇지만 런타임에 실행시간이 줄어든다.

마무리

override을 사용해서 좀더 안전한 재정의를 할 수 있다. 이전에는 const_iterator가 있어도 사용하기 불편해서 실제 사용할 때에 iterator를 조심해서 사용했었는데 cbegin()과 cend()으로 인해 const_iterator 사용이 쉬워졌다. noexcept로 좀더 단순하고 최적화가 더 좋아졌다. constexpr로 선택이 폭이 더 넓어졌다.
부족한 글이지만 여러분에게 도움이 되었으면 합니다. 즐프하세요. ospace.

참고

[1] 스콧 마이어스 저, 류광 역, Effective Modern C++, 인사이트

반응형