본문 바로가기

3.구현/C or C++

[c++] decltype, auto, 중괄호 초기화 사용하기

들어가기

C++11에 추가된 기능 중에 decltype과 auto가 있다. decltype은 대상의 형식이나 값 범주를 수정 없이 그대로 타입으로 생성하며, auto는 모든 종류의 데이터를 저장할 수 있는 타입이다. 하나씩 살펴보자.

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

decltype 키워드

delctype은 객체에 선언된 원래 형식을 가감없이 그대로 나타낸다. 참조 정보까지 포함한다.

#include <iostream>

struct Box {
    int value = 0;
};

int main() {
  const int val = 0;
  std::cout << typeid(decltype(val)).name << "\n";

  Box box;
  std::cout << typeid(decltype(box)).name << "\n";
  std::cout << typeid(decltype(Box::value)).name << "\n";
}

decltype을 사용하는 방법은 해당 객체의 타입을 사용해서 새로운 객체를 정의할 수 있다.

#include <iostream>

struct Box {};

int main() {
  const Box& box1 = box;
  decltype(auto) box2 = box1;
  std::cout << typeid(decltype(box2)).name << "\n";
}

decltype을 활용할 다른 방법이 함수 리턴형식을 표현할 때에 사용된다. 어쩌면 이게 핵심일 수 있다. 함수에서 후행 리턴 형식(tailing return type)에서 사용할 수 있다. 후행 리턴 형식은 함수 선언 뒤에 -> 다음에 오는 리턴 형식 선언 부분이다.

auto valueOf(Foo& foo) -> decltype(foo.value) {
  return foo.value;
}

decltype에 의해서 후행 리턴형식을 정의할 경우 함수의 리턴형은 auto로 해야한다. 그러면 foo.value 타입이 리턴 타입이 된다.

C++14에서 decltype(auto)로 인해서 후행 리턴 형식을 선언할 필요가 없이 쉽게 표현할 수 있다.

decltype(auto) valueOf(Foo& foo) {
  return foo.value;
}

그냥 auto만 사용해도 되는데 왜 decltype을 사용할까? 일반적인 경우는 auto로 사용하면 문제가 없다. decltype을 사용하는 경우 리턴형이 참조인 경우에 유용하다. 아래 Box에서 valueOf()은 참조 형태로 리턴한다.

struct Box {
    int value = 0;
    int& valueOf() { return value; }
};

valueOf()에 의해서 참조로 리턴되지만 accessValue()에서 다시 리턴되면서 리턴 형식인 auto 연역으로 인해 해당 참조는 사라지고 값으로 리턴된다. 이때에 decltype을 사용하면 이런 참조 정보도 그대로 유지되서 리턴 형식에 반영된다.

decltype을 사용할 때에 주의할 부분이 있다. 괄호로 한번더 감싸는 경우이다.

decltype(auto) f1() {
    int ret = 0;
    return ret;
}

decltype(auto) f2() {
    int ret = 0;
    return (ret);
}

f1()에서 리턴은 int 형으로 되며 해당 값이 반환된다. f2()에서는 decltype((ret))와 동일한 형태로 이는 int& 형이 된다. 그렇기에 f2()은 지역 변수가 참조로 리턴되는 위험한 상황이 된다.

auto 변수

auto 변수는 초기화 시점에 형식이 결정된다. 이때 형식이 결정되는 과정에서 형식 연역이 발생한다. auto로 인해 복잡한 템플릿 클래스 표현을 사용한 자료형을 기술할 필요가 없다. vector에서 interator을 활용하기 위해서 vector<int>::iterator 타입을 기술해서 사용해야 했다. 이제는 auto로 사용하면 된다.

vector<int> vals;

vector<int>::iterator it1 = vals.begin();
auto it2 = vals.begin();

특히 콜백 핸들러에서 인자 타입을 일일히 확인할 필요 없이 auto로 사용할 수 있다.

auto handlerLess = [](const auto& l, const auto& r) {
    return l < r;
}

그리고 추후 자료형이 변경이 필요한데 일일히 찾아가서 변경할 필요 없다. 결국 auto 사용해서 신경쓸 부분은 초기화하는 부분이다.

함수에 의해서 생성된 객체가 auto 변수에 저장된 경우 복잡한 표현식에 의해서 원하는 값이 불명확한 경우 이때에 static_cast을 사용해서 명시적으로 지정할 수 있다.

auto ep = static_cast<float>(calcSomeValue()[1]);

이 방법을 형식 명시 초기치 관용구(explicity typed initializer idiom)이라는 방법이다. 이는 단순히 형식을 명확하게 표현뿐만 아니라 값의 범위를 변경할 수도 있다. 다시 초기화 구분이 길어졌네. ㅡ.ㅡ;;;;

중괄호 초기화

C++11에서 균일 초기화(uniform initialization)을 도입하고 이를 중괄호로 감싼 초기화(braced initialization)라고 한다. 값들을 가지는 vector 초기화도 단순해졌다. 기존에는 가변인자를 가지는 초기화함수를 만들었는지만 중괄호로 쉽게 초기화된다.

std::vector<int> vals {1, 3, 5};
auto val1{1};
auto val2(2);

중괄호를 이용해서 단일 값도 초기화할 수 있다. 물론 단일 값 초기화는 기존 괄호를 이용한 초기화도 가능하다. 중괄호 초기화는 암묵적 형변화이 안된다. 괄호는 암묵적 형변환이 발생하기 때문에 의도하지 않는 형변환이 가능하다. 이럴 경우 괄호에 의한 초기화는 문제가 발생할 수 있다.

double x, y;
int sum1 { x + y }; // 에러!
int sum2 ( x + y ); // int로 값이 변환됨

만약 새로운 클래스에서 중괄호 초기화를 사용하고 싶다면 std::initializer_list<>을 가진 생성자를 정의하면 된다.

class Box {
public:
    Box(int val1, double val2);
    Box(std::initializer_list<double> vals);
};

Box box( 1, 2.0 ); // 첫번째 생성자 호출
Box box{ 1, 2.0 }; // 두번째 생성자 호출

빈 중괄호는 인수없음으로 기본생성자 호출할 수 있고 std::initializer_list로 초기화할 수 있다. 표준에서는 기본 생성자를 호출한다. 빈 std::initializer_list을 호출하려면 괄호나 중괄호로 한번 더 감싼다.

Box box1({});
Box box2{{}};

마무리

decltype에 대해서 간단하게 살펴보았다. decltype 자체로는 크게 어렵지는 않다. 이 부분이 템플릿과 std::move와 std::forward가 같이 사용되면서 복잡해질 뿐이다. 또한 auto는 처음 변수 선언할 때 타입을 적는 불편함이 사라졌고, 매번 API문서에 타입을 찾아는 하는 작업을 줄여준다. 또한 컨테이너를 초기화할 때 중괄호 초기화로 더 간편하게 사용할 수 있었다.
불필요한 부분을 정리하기 위해서 생각하다보니 시간이 좀 걸리네요. 부족한 글이지만 도움이되었으면 합니다. 즐프하세요. ospce.

참고

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

반응형