본문 바로가기
프로그래밍/C & C++

[C++] 값으로 전달과 참조로 전달 (std::ref(), std::cref())

by 별준 2023. 7. 11.

References

  • Ch7, C++ Templates The Complete 2nd

Contents

  • Passing By Value
  • Passing By Reference
  • Using std::ref() and std::cref()
  • Dealing with String Literals and Raw Arrays
  • Dealing with Return Values

C++이 처음 나왔을 때부터 call-by-value(값으로 전달)와 call-by-reference(참조로 전달)을 모두 제공한다. 일반적이지 않은 객체를 전달할 때는 참조로 전달하는게 더 나을 때가 많지만 값으로 전달보다 더 복잡하다. C++11부터는 이동 의미론 개념이 생기면서 참조로 전달하는 방법이 아래와 같이 더욱 다양해졌다.

  1. X const& (constant lvalue reference): 전달된 객체를 가리키는 파라미터이며 수정할 수 없음
  2. X& (nonconstant lvalue reference): 전달된 객체를 가리키는 파라미터이며 수정할 수 있음
  3. X&& (rvalue reference): 전달된 객체를 가리키는 파라미터이며, 이동 의미론을 사용한다. 즉, 값을 수정하거나 훔칠(steal) 수 있다.

알고 있는 타입인 경우에도 위의 세 가지 방법 중 어떤 것을 사용할 것인지 결정하기가 꽤 어렵다. 템플릿을 사용하는 경우에는 사용할 타입이 정해져 있지 않기 때문에 어떤 전달 방식이 적절한 지 결정하기 더 어렵다.

 

웬만하면 값으로 전달하는 것을 추천하지만, 아래의 예외 사항이 있을 수 있다.

  • 복사가 불가능할 때
  • 파라미터를 통해 값을 반환할 때
  • 인자의 원래 속성을 유지하면서 다른 곳으로 파라미터를 전달하기만 하려고 할 때
  • 성능상 효과가 두드러질 때

7장에서는 템플릿에서 파라미터를 선언하는 다양한 방식에 대해서 살펴보고 값으로 전달할 수 없는 이유가 있지 않는 한 값으로 전달하는 편이 더 낫다는 것을 보여준다.

 

만약 값 카테고리(value catecory) - lvalue, rvalue, prvalue, xvalue 등 - 에 익숙하지 않다면 아래 포스팅을 참조 바람.
 

[C++] Value Categories (값 카테고리)

References Appendix B, C++ Templates The Complete Guide https://en.cppreference.com/w/cpp/language/value_category https://www.scs.stanford.edu/~dm/blog/decltype.html Contents lvalues(좌측값) and rvalues(우측값) Value Categories Since C++11 Check Valu

junstar92.tistory.com

 


Passing by Value

인자를 값으로 전달하면 원칙적으로 모든 인자는 복사된다. 따라서, 각 파라미터는 전달된 인자의 복사본이다. 클래스의 경우에는 복사를 통해 생성된 객체는 대개 복사 생성자를 통해 초기화된다.

 

복사 생성자의 호출은 비용이 크다. 하지만 파라미터를 값으로 전달하더라도 여러 가지 방법을 통해 비용이 큰 복사 연산을 피할 수 있다. 컴파일러가 객체를 복사할 때 복사 연산을 최적화할 수도 있으며, 복잡한 객체의 경우에는 이동 의미론을 사용하여 적은 비용으로 복사할 수도 있다.

 

아래 예제를 통해서 인자가 값으로 전달되는 간단한 함수 템플릿 구현을 살펴본다.

template<typename T>
void printV(T arg) {
    ...
}

정수형에 대해서 위 함수 템플릿을 호출하면 인스턴화되는 코드는 다음과 같다.

void printV(int arg) {
    ...
}

arg는 전달된 인자가 객체이든 리터럴이든 함수가 반환한 값이든 간에 전달된 값의 복사본이다. 만약 std::string을 정의하고 이에 대해 함수 템플릿을 호출하면

std::string s = "hi";
printV(s);

템플릿 파라미터 T는 std::string으로 인스턴스화된다. 이 경우에도 arg는 s의 복사본이 되며, 문자열 클래스의 복사 생성자로 복사본을 생성하므로 이 연산의 비용은 클 가능성이 높다. 원칙적으로는 이 복사 연산에서는 값을 저장할 내부 메모리를 할당받고 원래 값을 `deep copy`해야 하기 때문이다.

 

하지만 복사 생성자만 호출되는 것은 아니다. 아래 코드를 살펴보자.

std::string returnString();
std::string s = "hi";
printV(s);                 // copy constructor
printV(std::string("hi")); // copying usually optimized away (if not, move constructor)
printV(returnString());    // copying usually optimized away (if not, move constructor)
printV(std::move(s));      // move constructor

첫 번째 호출은 lvalue를 전달했으며 복사 생성자가 사용된다. 하지만 두 번째와 세 번째 호출에서 prvalue (temporarly objects or returned by another function)를 위한 함수 템플릿을 직접 호출하면 컴파일러는 복사 생성자가 호출되지 않도록 인자의 전달을 최적화한다. C++17부터는 이 최적화는 필수이다. C++17 이전에는 복사는 최적화하여 없애지는 않지만 적어도 이동 의미론을 사용하도록 하여 비싼 복사 연산을 피해야 한다. 마지막 호출에서 xvalue를 사용하여 전달하면 s의 값을 더 이상 쓰지 않을 것이라는 뜻이므로 이동 생성자를 호출하도록 한다.

 

따라서, printV()를 구현할 때 값으로 전달되는 파라미터로 선언하더라도 lvalue를 전달할 때만 비싼 복사 연산이 되지만, 이런 경우가 상당히 많다 (다른 함수에 전달하기 위해 객체를 미리 만들어두는 경우가 많으므로).

 

형 소실 (Value Decays)

값으로 전달할 때 가장 유의해야할 특징 중 하나는 인자를 값으로 전달하면 그 타입은 형 소실된다는 것이다. 원시 배열(raw array)는 포인터(pointer)로 변환되고 const와 volatile 같은 한정자(qualifier)는 제거된다 (auto로 선언된 객체를 값으로 초기화할 때와 동일하다).

 

template<typename T>
void printV(T arg) {
    ...
}

std::string const c = "hi";
printV(c); //  c decays so that arg has type std::string
printV("hi"); // decays to pointer so that arg has type char const*

int arr[4];
printV(arr); // decays to pointer so that arg has int*

따라서, 위와 같이 문자열 리터럴 "hi"를 전달하면 원래 타입인 char const[3]이 형 소실되어 char const*로 바뀐다.

 

이러한 동작은 C로부터 물려받은 것이며, 좋은 점도 있고 나쁜 점도 있다. 전달받은 문자열 리터럴을 다루기는 쉬워지지만 printV() 내에서 한 요소에 대한 포인터를 전달받은 것인지 배열을 받은 것인지 구별할 수가 없다. 문자열 리터럴과 원시 배열을 다루는 방법은 아래에서 따로 다루고 있다.


Passing by Reference

참조로 전달하는 경우 어떤 경우에도 복사는 일어나지 않는다. 또한 전달받은 인자에 대해서 형 소실도 발생하지 않는다. 하지만 때로는 전달할 수 없을 때도 있고 전달은 하더라도 파라미터의 결과 타입이 문제를 일으키는 경우도 있다.

 

Passing by Constant Reference

Nontemporary 객체를 전달할 때, 어떠한 불필요한 복사를 피하려면 상수 참조자(constant reference)를  사용하면 된다.

template<typename T>
void printR(T const& arg) {
    ...
}

위와 같이 선언하면 전달받은 객체를 절대로 복사하지 않는다.

std::string returnString();
std::string s = "hi";
printR(s);                 // no copy
printR(std::string("hi")); // no copy
printR(returnString());    // no copy
printR(std::move(s));      // no copy

int형을 참조로 전달하면 비용 측면에서 약간 손해는 있지만 문제가 될 정도는 아니다. 따라서, 아래 코드에서

int i = 42;
printR(i); // passes reference instead of just copying i

printR()은 아래처럼 인스턴스화된다.

void printR(int const& arg) {
    ...
}

참조로 전달의 실제 구현은 사실 인자의 주소를 활용하게 된다. 주소는 압축되어 인코딩되므로 caller에서 callee로 주소를 전달한다는 것은 상당히 효율적이다. 하지만 주소로 전달하면 callee가 해당 주소를 가지고 무엇을 할지 모른다는 문제가 있다. 이론적으로 호출받은 쪽은 해당 주소를 통해 '도달 가능한' 모든 값을 변경할 수 있다. 즉, 컴파일러는 호출된 이후, (일반적으로  machine register에 )캐싱될 수 있는 모든 값이 유효하지 않다고 가정해야 한다. 이러한 값들을 다시 로딩하는 것은 상당한 비용이 든다.

 

상수 참조자(const reference)를 사용하여 전달하면 컴파일러가 변경된 것이 없다는 것을 알 수 있지 않을까 라고 생각할 수 있지만, 불행히도 callee는 자신의 non-const reference를 통해 참조된 객체를 변경할 수 있기 때문에 그렇게 가정할 수는 없다(e.g., const_cast).

 

inline을 사용하여 이 문재를 완화시킬 수는 있다. 해당 호출을 인라인으로 확장시킬 수 있다면 caller와 callee가 같은 곳에 있다는 것을 알게 되며, 값으로 전달할 때에만 주소가 사용된다. 함수 템플릿은 대부분 짧기 때문에 인라인으로 확장하기 좋다. 하지만, 복잡한 알고리즘을 가지고 있다면 인라인화하기 어려울 수도 있다.

 

Passing by Reference Does Not Decay

인자를 참조자로 전달하면 형 소실되지 않는다. 즉, 원시 배열(raw array)을 전달하더라도 포인터로 변환되지 않고, const나 volatile 같은 한정자도 없어지지 않는다. 하지만 파라미터를 T const&로 선언하기 때문에 템플릿 파라미터 T 자체는 const로 연역되지 않는다.

template<typename T>
void printR(T const& arg) {
    ...
}

std::string const c = "hi";
printR(c);    // T deduced as std::string, arg is std::string const&

printR("hi"); // T deduced as char[3], arg is char const(&)[3]
int arr[4];
printR(arr);  // T deduced as int[4], arg is int const(&)[4]

따라서, printR()에서 T라는 타입으로 선언한 local object는 const가 아니다.

 

Passing by Nonconstant Reference

전달된 인자에 값을 저장하여 반환하고 싶다면 nonconstant reference를 전달하면 된다. 이 경우에서도 인자를 전달할 때 복사가 발생하지 않으며, 호출된 함수 템플릿의 파라미터는 전달된 인자에 직접 액세스한다.

template<typename T>
void outR(T& arg) {
    ...
}

 

임시값(prvalue) 또는 이미 존재하는 객체를 std::move()(xvalue)로 전달하여 outR()을 호출할 수 없다.

std::string returnString();
std::string s = "hi";
outR(s);                 // OK: T deduced as std::string, arg is std::string&
outR(std::string("hi")); // ERROR: not allowed to pass a temporary (prvalue)
outR(returnString());    // ERROR: not allowed to pass a temporary (prvalue)
outR(std::move(s));      // ERROR: not allowed to pass an xvalue

상수가 아닌 타입의 원시 배열은 전달할 수 있으며, 형 소실은 발생하지 않는다.

int arr[4];
outR(arr); // OK: T dedcued as int[4], arg is int(&)[4]

 

따라서, 요소를 수정할 수도 있으며 배열의 크기를 다를 수도 있다.

template<typename T>
void outR(T& arg) {
    if (std::is_array<T>::value) {
        std::cout << "got array of " << std::extent<T>::value << " elems\n";
    }
    ...
}

 

하지만, 이렇게 구현하면 템플릿이 조금 이상하게 동작할 수 있다. const 인자를 전달하면 arg는 const reference에 대한 선언으로 연역될 수 있는데, 이렇게 되면 갑자기 lvalue가 들어올 거라고 기대하는 곳에 rvalue를 전달할 수 있게 된다.

std::string const c = "hi";
outR(c);                   // OK: T deduced as std::string const
outR(returnConstString()); // OK: same if returnConstString returns const std::string
outR(std::move(c));        // OK: T deduced as std::string const
outR("hi");                // OK: T deduced as char const[3]

물론 이 경우, 전달된 인자를 함수 템플릿 내에서 수정하려고 하면 에러가 발생한다. 즉, 호출할 때는 const 객체를 전달할 수 있지만 함수가 인스턴스화되었을 때 값을 수정하면 에러가 발생한다.

 

const reference를 non-const reference로 전달하지 못하도록 하고 싶다면 아래의 두 가지 방법 중 하나를 사용할 수 있다.

1. static assertion을 사용하여 컴파일 에러를 발생시킨다.

template<typename T>
void outR(T& arg) {
    static_assert(!std::is_const<T>::value,
        "out parameter of outR<T>(T&) is const");
    ...
}

2. std::enable_if<> 또는 C++20의 concepts를 사용

template<typename T,
    typename = std::enable_if_t<!std::is_const<T>::value>
void outR(T& arg) {
    ...
}

or

// in c++20
template<typename T>
requires !std::is_const_v<T>
void outR(T& arg) {
    ...
}

 

Passing by Forwarding Reference

Perfect forwarding을 위해 참조로 전달을 사용하기도 한다. 하지만 템플릿 파람리터에 대한 rvalue 참조자를 선언하여 forwarding reference(전달 참조자)로 사용한다면 특별한 법칙이 사용된다는 점을 기억해야 한다.

template<typename T>
void passR(T&& arg) { // arg declared as forwarding reference
    ...
}

전달 참조자(forwarding reference)로 어떤 값이든 전달할 수 있으며, 이 경우에도 복사는 일어나지 않는다.

std::string s = "hi";
passR(s);                 // OK: T deduced as std::string& (also the type of arg)
passR(std::string("hi")); // OK: T deduced as std::string, arg is std::string&&
passR(returnString());    // OK: T deduced as std::string, arg is std::string&&
passR(std::move(s));      // OK: T deduced as std::string, arg is std::string&&
int arr[4];
passR(arr);               // OK: T duduced as int(&)[4] (also the type of arg)

각 호출마다 passR() 내부의 파라미터 arg는 전달된 값이 rvalue인지 const/non-const lvalue인지 '알고 있는' 타입을 갖는다. 이 방법은 세 가지 경우의 타입을 구분하도록 인자를 전달하는 유일한 방법이다.

 

파라미터를 전달 참조자로 선언하는 것이 완벽해보일 수 있지만, 공짜는 아니다.

예를 들어, 템플릿 파라미터는 암묵적으로 참조자형이 되며, local 객체를 T로 선언하면서 초기화하지 않으면 에러가 발생한다.

template<typename T>
void passR(T&& arg) { // arg is a forwarding reference
    T x; // for passed lvalues, x is a reference, which requires an initializer
    ...
}

foo(42); // OK: T deduced as int
int i;
foo(i);  // ERROR: T deduced as int&, which makes the declaration of x in passR() invalid

이와 같은 상황에서 x가 참조자가 되지 않게 하려면 std::remove_reference라는 type trait를 사용하면 된다.

template<typename T>
void f(T&&)
{
    std::remove_reference_t<T> x; // x is not a reference
    ...
}

 


Using std::ref() and std::cref()

C++11에서부터 호출하는 측에서 함수 템플릿 인자를 전달할 때 값으로 전달할지 참조로 전달할지 결정할 수 있다. 템플릿을 선언할 때 인자를 값으로 받도록 구현했다면 호출하는 측에서는 <functional> 헤더에 선언된 std::ref()와 std::cref()를 사용하여 인자를 참조자로 전달할 지 결정할 수 있다.

template<typename T>
void printT(T arg) {
    ...
}

std::string s = "hello";
printT(arg);          // pass s by value
printT(std::cref(s)); // pass s "as if by reference"

std::cref()가 템플릿 내에서 파라미터를 취급하는 방법을 바꾸지는 않는다. 대신 전달된 인자 s를 참조자처럼 행동하도록 하는 객체로 둘러싸는 트릭을 사용한다. 실제로 원래 인자를 참조하는 std::reference_wrapper<> 타입의 객체를 생성하고 이 객체를 값으로 전달한다. 이 wrapper는 원래 타입으로 되돌리는 implicit type conversion 연산 한 가지만을 지원하며, 이 연산을 통해 원래 객체를 얻을 수 있다. 따라서, 전달된 객체에 대해 유효한 연산자가 있으면 reference wrapper에도 사용할 수 있다. 아래 예시를 살펴보자.

#include <functional>
#include <string>
#include <iostream>

void printString(std::string const& s)
{
    std::cout << s << "\n";
}

template<typename T>
void printT(T arg)
{
    printString(arg); // might convert arg back to std::string
}

int main(int argc, char** argv)
{
    std::string s = "hello";
    printT(s);            // print s passed by value
    printT(std::cref(s)); // print s passed "as if by reference"
}

마지막 printT() 호출은 std::reference_wrapper<string const> 타입의 객체를 파라미터 arg로 전달하는 데, 값으로 전달하게 된다. 그러면 다시 원래 타입인 std::string으로 변환한다.

 

원래 타입으로의 암묵적 변환이 필요하다는 것을 컴파일러가 알고 있어야 한다. 그래서 std::ref()와 std::cref()는 보통 일반 코드를 통해 객체를 전달할 때만 잘 동작한다. 예를 들어, 전달받은 타입 T의 객체를 직접 출력하려고 시도하면, std::reference_wrapper<>에 대한 출력 연산자가 없기 때문에 실패하게 된다.

template<typename T>
void printV(T arg) {
    std::cout << arg << "\n";
}
...
std::string s = "hello";
printV(s);            // OK
printV(std::cref(s)); // ERROR: no operator << for reference wrapper defined

 

또한, 아래 코드도 컴파일 에러가 발생한다.

template<typename T1, typename T2>
bool isless(T1 arg1, T2 arg2)
{
    return arg1 < arg2;
}
...
std::string s = "hello";
if (isless(std::cref(s), "world")) ...              // ERROR
if (isless(std::cref(s), std::string("world"))) ... // ERROR

이는 reference wrapper를 char const*나 std::string과 비교할 수 없기 때문이다.

 

또한, arg1과 arg2의 타입이 같은 T 이어도 비교할 수 없다. arg1과 arg2에 대한 T를 추론하는 동안 타입들이 서로 충돌하기 때문이다.

template<typename T>
bool isless(T arg1, T arg2)
{
    return arg1 < arg2;
}

 

따라서, std::reference_wrapper<> 클래스의 효과는 일급 객체(first-class object)처럼 참조자를 사용하도록 하는 것이며, 복사할 수 있고 따라서 함수 템플릿에 값으로 전달할 수 있다. 클래스 내에서도 사용할 수 있는데, 예를 들어, 컨테이너 내에서 객체에 대한 참조자를 저장할 때도 사용할 수 있다. 하지만 마지막에는 항상 실제 타입으로 변환해야 한다.

 


Dealing with String Literals and Raw Arrays

문자열 리터럴과 원시 배열(raw array)를 템플릿 파라미터로 사용하면 다른 효과가 발생했다.

  • 값으로 호출하면 형 소실이 발생하여 pointer로 변환된다.
  • 어떤 방식으로든 참조자로 호출하면 형 소실되지 않기 때문에 인자는 배열을 가리키는 참조자가 된다.

둘 다 좋은점도 있고 나쁜점도 있는데, 배열이 포인터로 형 소실되면 요소에 대한 포인터와 전달된 배열을 구별할 수 없다. 반면, 문자열 리터럴을 전달받았는데 형 소실이 되지 않는다면 길이가 다른 문자열 리터럴은 타입이 다르다는 문제가 발생한다. 아래 예제 코드를 살펴보자.

template<typename T>
void foo(T const& arg1, T const& arg2)
{
    ...
}

foo("hi", "guy"); // ERROR

위 코드에서 foo("hi", "guy")는 컴파일이 되지 않는다. "hi"는 char const[3]이고 "guy"는 char const[4]인데, 템플릿에서는 둘의 타입이 T로 동일해야 하기 때문이다. 즉, 문자열 리터럴들의 길이가 같을 때에만 컴파일이 된다.

 

함수 템플릿 foo()에서 인자를 참조자가 아닌 값으로 전달하도록 선언하면 함수 호출에는 성공할 수 있다.

template<typename T>
void foo(T arg1, T arg2)
{
    ...
}

foo("hi", "guy"); // Compiles, but...

하지만 이걸로 모든 문제가 해결된 것은 아니며, 오히려 컴파일 에러가 런타임 에러로 바뀔 수 있다. 전달된 인자를 operator==으로 비교한다고 가정해보자.

template<typename T>
void foo(T const& arg1, T const& arg2)
{
    if (arg1 == arg2) {
        // compares addresses of passed arrays
        ...
    }
}

foo("hi", "guy");

전달된 문자 포인터는 문자열로 해석해야 한다. 즉, 템플릿은 이미 형 소실된 후 들어오는 문자열 리터럴인 인자를 다룰 수 있어야 한다.

 

하지만 형 소실되는 것이 도움이 되는 경우도 많다. 특히 두 객체가 같은 타입이거나 같은 타입으로 변환될 수 있는지 검사할 때 좋다. 전형적인 예시로 perfect forwarding이 있다. 이런 경우에는 type traits인 std::decay<>()를 사용하여 인자를 명시적으로 형 소실시킬 수 있다.

 

때로는 일부 type traits에서 암묵적으로 형 소실시키기도 하는데, 전달된 인자의 타입 중 common type을 찾는 std::common_type<>이 대표적이다.

 

Special Implementations for String Literals and Raw Arrays

전달받은 인자가 포인터인지 아니면 배열인지에 따라 구현이 달라져야하는 경우가 있다. 이런 경우에는 전달받은 배열이 형 소실되어선 안된다. 이 두 가지 경우를 구분하기 위해서는 배열이 전달되었는지 검사해야 한다. 기본적으로 사용할 수 있는 방법은 두 가지가 있다.

 

1. 배열에서만 유효한 템플릿 파라미터를 선언한다.

template<typename T, std::size_t L1, std::size_t L2>
void foo(T(&arg1)[L1], T(&arg2)[L2])
{
    T* pa = arg1; // decay arg1
    T* pb = arg2; // decay arg2
    if (compareArrays(pa, L1, pb, L2)) { ... }
}

위의 경우 arg1과 arg2는 같은 요소 타입 T의 원시 배열이지만 크기는 L1과 L2로 다르다. 하지만 다양한 원시 배열을 지원하려면 구현이 많이 필요하다.

 

2. 배열(또는 포인터)이 전달되었는지 검출하는 type traits를 사용한다.

template<typename T,
    typename = std::enable_if_t<std::is_array_v<T>>>
void foo(T&& arg1, T&& arg2)
{
    ...
}

 

위의 방법들처럼 특별한 처리가 필요하기 때문에 배열을 다르게 처리해야 하는 경우에는 그냥 다른 함수 이름을 사용하는 것이 좋다. 물론 템플릿의 호출자가 std::vector나 std::array를 쓰도록 하면 더 좋다. 하지만 문자열 리터럴이 원시 배열이므로 원시 배열을 고려하지 않을 수는 없다.


Dealing with Return Values

값을 반환할 때 역시 값으로 반환할 지, 참조자로 반환할 지 결정해야 한다. 하지만 참조자를 반환하면 제어를 벗어난 무언가를 참조한다는 뜻이므로 문제가 발생할 소지가 다분하다. 참조자를 반환하는 다음의 일반적인 프로그래밍 상황이 몇 가지 있긴 하다.

  • 컨테이너나 문자열의 요소 반환 (operator[]나 front()를 통해)
  • 클래스 멤버에 write access를 허용하는 경우
  • 연결된 호출 (스트림의 operator<<, operator>>, 클래스 객체의 operator=)에서의 객체 반환

또한 멤버에 대한 읽기 접근을 허용할 때는 const-reference로 반환하는 것이 일반적이다.

 

이러한 상황에서도 적절하게 사용하지 않으면 문제가 생길 수 있다. 아래의 극단적인 예시를 살펴보자.

std::string* s = new std::string("whatever");
auto& c = (*s)[0];
delete s;
std::cout << c; // runtime ERROR

위 코드에서는 문자열 요소에 대한 참조자를 얻었지만, 사용하는 시점에 해당 문자열은 이미 delete되고 없다 (dangling reference). 따라서 정의되지 않은 동작이 발생한다.

물론 위와 같은 경우는 쉽게 발견할 수 있지만, 아래와 같이 문제를 꼬아놓을 수도 있다.

auto s = stD::make_shared<std::string>("whatever");
auto& c = (*s)[0];
s.reset();
std::cout << c; // runtime ERROR

 

따라서 함수 템플릿이 결과를 값으로 반환하도록 주의해야 한다. 하지만 앞서 살펴본 것과 같이 템플릿 파라미터 T를 사용하는 것이 참조자를 쓰지 않는다는 것을 의미하지 않는다. T 자체가 참조자로 추론될 수 있기 때문이다.

template<typename T>
T retR(T&& p) // p is a forwarding reference
{
    return T{...}; // OOPS: returns by reference when called for lvalues
}

 

템플릿 파라미터에 명시적으로 참조자를 사용하면 값으로 호출된 상태로 T가 템플릿 파라미터로 추론될 때에도 참조자 타입이 될 수 있다.

template<typename T>
T retV(T p) // Note: T might become a reference
{
    return T{...}; // OOPS: returns a reference if T is a reference
}
int x;
retV<int&>(x); // retT() instantiated for T as int&

 

확실하게 하려면 다음의 두 가지 방법 중 하나를 사용하면 된다.

 

- 참조자가 아닌 타입으로 변환하는 type traits인 std::remove_reference<> 사용

template<typename T>
typename std::remove_reference<T>::type retV(T p)
{
    return T{...}; // always returns by value
}

std::decay<>와 같은 특질도 유용하며, 이는 암묵적으로 참조자를 제거한다.

 

- 반환 타입을 auto로 선언하여 컴파일러가 반환 타입을 결정하도록 한다(C++14부터 허용). auto는 항상 형 소실된다.

template<typename T>
auto retV(T p) // by-value return type deduced by compiler
{
    return T{...}; // always returns by value
}

댓글