이동 의미 체계가 C++11부터 도입되었는데 여러 참조로 전달 방법도 다양해졌습니다
- X const& : 상수 lvalue 참조자
전달된 객체를 가리키는 파라미터로, 수정할 수 없습니다. - X & : 상수가 아닌 lvalue 참조자
전달된 객체를 가리키는 파라미터로, 수정할 수 있습니다. - X && : rvalue 참조자
전달된 객체를 가리키는 파라미터로, 이동 의미 체계를 사용합니다. 즉, 값을 수정하거나 훔칠 수 있습니다.
7.1 값으로 전달
인자를 값으로 전달하면 원칙적으로 모든 인자는 복사 생성자를 통해 복사됩니다. 복사 생성자 호출은 비싸지만, 여러 가지 방법으로 이를 최적화할 수 있습니다.
만약 임시 객체(prvalue)를 인자로 전달하면 C++17에서는 복사 생성자가 호출되지 않게 최적화합니다. 그 이전은 이동을 사용하고자 해야합니다. 값이 있는 인자를 std::move를 통해 전달(xvalue)하는 경우에는 이동 생성자를 활용하는 것이다.
또한 앞서 얘기하였지만, 인자를 값으로 전달할 때에는 형 소실이 되어, 원시 배열은 포인터로 변환되며 const와 volatile 같은 한정사는 제거됩니다.
7.2 참조로 전달
복사 및 형 소실은 일어나지 않지만, 전달할 수 없거나 파라미터의 결과형이 문제를 일으키는 경우가 존재합니다. 함수의 인수가 왼쪽 참조 인경우에는 임시 객체(prvalue)나 이미 있는 객체를 std::move()를 통해 전달할 수 없습니다. 하지만 const 인자를 전달하면 상수 참조자에 대한 선언으로 연역될 수 있어 lvalue가 들어올 곳에 rvalue를 전달하는 것이 가능해집니다. 이를 막고 싶다면 static_asset, std::enable_if<> 혹은 concept를 사용해서 막아야 합니다.
전달 참조자(&&)를 통해서 전달한다면, rvalue 인지 상수/비상수 lvalue인지 알 수 있게 되어, 인자를 제대로 전달할 수 있게 됩니다.
7.3 std::ref()와 std::cref() 사용
C++11에서부터 호출자가 함수 템플릿 인자를 전달할 때 값으로 전달할 지 참조로 전달할 지 결정할 수 있는데, 템플릿에서 값으로 전달받게 설정했다면, std::ref()나 std::cref()를 사용하여 인자를 참조로 전달할 지를 결정할 수 있습니다. 이는 원래 인자를 가리키는 std::reference_wrapper<>형의 객체를 생성하여 값으로 전달하는 방식입니다. 따라서 형식 T의 연역이 std::reference_wrapper<>의 형태를 띄며, 이는 operator<<나 형식과의 비교 연산이 지원되지 않습니다.
7.4 문자열 리터럴과 원시 배열
값으로 호출하면 형 소실돼 요소형에 대한 포인터로 바뀌며, 참조인 경우는 배열 그대로 진행이 되며, 이 둘은 서로 상이한 장단점을 지닙니다. 만약 배열로 전달을 받고 싶다면, 다음과 같은 특별한 템플릿 파라미터로 선언되어야 합니다.
// 배열 형식 사용
template<typename T, std::size_t L1, std::size_t L2>
void foo(T (&arg1)[L1], T(&arg2)[L2])
{
T* pa = arg1; // arg1 형 소실
T* pb = arg2; // arg1 형 소실
...
};
// 형식 특질 사용
template<typename T, typename = std::enable_if<std::is_array_v<T>>>
void foo(T&& arg1, T&& arg2)
{
...
};
7.5 반환 값 다루기
값을 반환해야 할 때에도 역시 값으로 반환할 지 참조자로 반환할 지 결정해야 합니다. 하지만 참조자를 반환한다면 문제가 될 소지가 다분한데 다음의 경우에 상수 참조자로 반환하는 것이 보통 적법합니다
- 컨테이너나 문자열의 요소 반환
- 클래스 멤버로 쓰기 접근을 허용할 경우
- 연결된 호출하에서 객체 반환
또한 보편 참조를 사용하는 템플릿 함수인 경우에 T를 사용하여 이것이 참조로 반환되는 경우에도 충분히 주의를 기울여야 합니다. 이떄는 std::remove_reference<> 혹은 std::decay<>를 사용하여 참조자를 제거하거나,반환형을 auto로 선언하면 항상 형 소실이 일어나게 합니다.
7.6 템플릿 파라미터 선언 추천 방식
인자를 값으로 전달하는 경우에는, std::ref를 통해 참조로 전달할 지의 여부를 결정할 수 있지만, 최적의 성능을 보이기에는 어렵습니다. 참조의 경우는 성능은 좋을 수 있으나, 형 소실이 일어나지 않아 원치 않은 형 변환이 일어나 원하는 대로 인스턴스화 되지 않을 수 있습니다. 선택할 때 고려할 사항은 다음과 같습니다
- 기본적으로 파라미터는 값으로 전달하고 복사가 비싼 객체의 lvalue를 전달할 경우에는 std::Ref나 std::cref를 사용합시다.
- 출력이나 입출력 파라미터가 필요하다면 참조자가 필요하며, 템플릿이 인자를 전달한다면 std::forward<>를 활용한 완벽한 전달을 사용합시다.
- 성능에 대해서 직관적으로 판단하지 말고 측정을 해야 합니다.
- 함수를 너무 일반적으로 만들지 않는 것이 좋습니다. 따라서 형식에 제약을 두는 것이 좋습니다.
이를 std::make_pair의 함수 선언 변화를 통해 어떻게 C++ 표준이 변하는 지를 확인할 수 있습니다.
// C++98 - 길이가 다양한 문자열 리터럴과 원시 배열의 쌍에서 문제가 발생
template<typename T1, typename T2>
pair<T1, T2> make_pair(T1 const& a, T2 const& b)
{
return pair<T1, T2>(a, b);
}
// C++03 - 참조에서 값으로 변경하여 효율성보다 안정성을 선택
template<typename T1, typename T2>
pair<T1, T2> make_pair(T1 a, T2 b)
{
return pair<T1, T2>(a, b);
}
// C++11 - 이동 의미 체계를 지원하기 위해서 전달 참조자로 인자를 받습니다.
template<typename T1, typename T2>
constexpr pair<typename decay<T1>::type, typename decay<T2>::type>
make_pair(T1&& a, T2&& b)
{
return pair<typename decay<T1>::type, typename decay<T2>::type>
(forward<T1>(a), forward<T2>(b));
}
7.7 요약
- 템플릿을 검사할 목록에 다양한 길이의 문자열 리터럴을 꼭 넣는다.
- 템플릿 파라미터를 값으로 전달하면 형 소실되지만 참조로 전달하면 형 소실되지 않는다.
- 형식 특질인 std::decay<>를 사용하면 템플릿에서 참조로 전달된 파라미터도 형 소실시킬 수 있다.
- 함수 템플릿이 값으로 전달로 선언돼 있을 때 인자를 참조로 전달하고 싶다면 std::cref()나 std::ref()를 사용한다.
- 템플릿 파라미터를 값으로 전달하면 간단하지만 최적의 성능을 내진 못할 수 있다.
- 참조로 전달해야 하는 좋은 이유가 있지 않는 한 함수 템플릿에는 파라미터를 값으로 전달한다.
- 반환 값은 항상 값으로 전달되게 주의를 기울인다.
- 성능이 중요하다면 언제나 직접 측정해야 한다. 직관에 의존하지 말자. 거의 대부분 틀린다.
'C++공부 > C++ Templates' 카테고리의 다른 글
9. 템플릿 실제 사용 (0) | 2022.06.11 |
---|---|
8. 컴파일 과정 프로그래밍 (0) | 2022.06.11 |
6. 이동 의미 체계와 enable_if<> (0) | 2022.06.09 |
5. 까다로운 기초 지식 (0) | 2022.06.09 |
4. 가변 인자 템플릿 (0) | 2022.06.09 |