본문 바로가기
💻 programming/c++

[c++] perfect forwarding (완벽한 전달)

by 연구원-A 2020. 12. 15.
반응형

Perfect forwarding

Perfect Forwarding

Definition

완벽한 전달 (Perfect forwarding)은 lvalue 또는 rvalue의 특성을 바꾸지 않고 함수 템플릿에 인자로 전달하는 것을 의미합니다.

Lvalues and rvalues

여기서는 굳이 glvalues, xvalues, prvlaue와 같은 자세한 내용에 대해서는 다루지 않습니다. 우리는 rvlaue와 lvalue에 대해서만 이해하고 있으면 됩니다.

Rvalue에 해당하는 변수는 다음과 같습니다.

  • 임시 객체
  • 이름 없는 객체
  • 주소가 없는 객체

만약 이중에서 하나라도 해당한다면 해당 변수는 rvalue 입니다. 반대로 lvalue는 이름과 주소를 가지고 있습니다. 아래 코드는 rvalue에 해당하는 예시입니다.

int five = 5;
std::string a = std::string("Rvalue");
std::string b = std::tring("R") + std::string("value");
std::string c = a + b;
std::string d = std::move(b);

여기서 rvlaue는 오른 쪽에 있는 것들입니다. 5나 std::string("Rvalue")는 주소를 가지지도 않고 이름을 붙일 수도 없습니다. std::move(b)는 조금 특별한 케이스에 해당하는 데 c++11부터 추가된 함수로 lvalue b를 rvalue reference로 변환하는 함수입니다.

Perfect factory method

완벽한 전달에 대한 직관적인 이해를 돕기 위해 perfect factory method를 이용해 설명하고자 합니다. 우선 perfect factory method는 공식적인 용어는 아닙니다.

일반적으로 perfect factory method는 다음의 특성을 가진 팩토리 함수를 의미합니다.

  • 가변 길이 인자를 받을 수 있어야 합니다.
  • lvaue와 rvalue 모드 인자로 받을 수 있어야 합니다.
  • 생성자에 똑같은 인자를 전달할 수 있어야 합니다.

First iteration

우선 우리는 참조 (reference)를 인자로 받는 함수 템플릿을 생각해볼 수 있습니다. 정확히 말하면 상수가 아닌 lvalue 참조를 의미합니다.

#include <iostream>

template <typename T, typename Arg>
T create(Arg& a) {
    return T(a);
}

int main() {
    // Lvalue
    int five = 5;
    int myFive = create<int>(five):
    std::cout << "myFive: " << myFive << std::endl;

    // Rvalues
    int myFive2 = create<int>(5);
    std::cout << "myFive2: " << myFive2 << std::endl;
}

위 코드를 컴파일하면 다음과 같은 에러가 발생합니다.

invalid initializetion of non-const reference of type 'int&' from an rvalue of type 'int'

상수 5를 인자로 전달했을 때 non-constant lvalue reference로 전달받을 수 없기 때문입니다. 이 문제를 해결하는 방법은 두 가지가 있습니다.

  1. create 함수의 인자를 constant lvalue reference로 바꾸는 방법:
    이 방법은 썩 좋은 방법은 아닙니다. 전달받은 인자가 항상 상수이므로 바꿀 수 없기 때문입니다.
  2. create 템플릿 함수를 오버로딩 하는 방법:
    이 방법이 조금은 더 쉬운 방법입니다. Second Iteration에서 이 방법을 사용해보겠습니다.

Second iteration

#include <iostream>

template <typename T, typename Arg>
T create(Arg& a) {
    return T(a);
}

template <typename T, typename Arg>
T create(const Arg& a) {
    return T(a);
}

int main(){

  std::cout << std::endl;

  // Lvalues
  int five=5;
  int myFive= create<int>(five);
  std::cout << "myFive: "  << myFive << std::endl;

  // Rvalues
  int myFive2= create<int>(5);
  std::cout << "myFive2: " << myFive2 << std::endl;

  std::cout << std::endl;

}

이제 프로그램은 정상적으로 실행됩니다. 그렇지만 아직도 구조상의 문제가 여전히 남아있습니다.

  1. 만약 n개의 인자가 존재한다면 2^n + 1개의 조합에 맞는 create 함수를 오버로딩해야 합니다.
  2. 함수의 인자로 받은 변수가 이름이 있는 lvalue로 정의되어 있다는 것이 문제입니다. T에 할당된 클래스의 이동생성자가 정의되어 있더라도 lvalue로 인자를 전달하면 더 이상 이동 생성자가 불리지 않고 복사가 일어나기 떄문입니다.

이제 c++에서 제공하는 std::forward 함수를 이용할 때가 왔습니다.

Third iteration

우선 std::forward를 이용하면 문제가 없어보입니다.

#include <iostream>

template <typename T, typename Arg>
T create (Arg&& a) {
    return T(std::foward<Arg>(a));
}

int main(){

  std::cout << std::endl;

  // Lvalues
  int five=5;
  int myFive= create<int>(five);
  std::cout << "myFive: "  << myFive << std::endl;

  // Rvalues
  int myFive2= create<int>(5);
  std::cout << "myFive2: " << myFive2 << std::endl;

  std::cout << std::endl;

}

여기서 유니버셜 레퍼런스 (universal reference)가 사용됐습니다. Arg&& a는 lvalue와 rvalue를 bind할 수 있는 강력한 도구입니다.

  • rvalue가 인자로 넘어오면 rvlaue reference가 되고
  • lvalue가 인자로 넘어오면 lvalue reference가 됩니다.

여기서 참조 (reference)는 각 변수의 특징은 그대로 유지하고 이름만 새로 할당했다고 생각하면 쉽게 이해할 수 있습니다.

완벽한 전달을 위해서는 std::forward를 사용해야 합니다. std::forward(a)는 유니버셜 레퍼런스로 타입 캐스팅하므로 rvalue는 rvlaue 그대로 남아있을 수 있습니다.

그럼 이제 create 함수는 완벽한 전달을 할 수 있을까요? 그렇지는 않습니다. 마지막 단계는 가변 길이 템플릿을 적용하는 것입니다.

Fourth iteration

마지막 단계는 가변 길이 템플릿을 이용하는 것입니다.

#include <iostream>
#include <string>
#include <utility>

template <typename T, typename... Args>
T create(Args&&... args) {
    return T(std::forward<Args>(args)...);
}

struct MyStruct{
  MyStruct(int i,double d,std::string s){}
};

int main(){

    std::cout << std::endl;

  // Lvalues
  int five=5;
  int myFive= create<int>(five);
  std::cout << "myFive: "  << myFive << std::endl;

  std::string str{"Lvalue"};
  std::string str2= create<std::string>(str);
  std::cout << "str2: " << str2 << std::endl;

  // Rvalues
  int myFive2= create<int>(5);
  std::cout << "myFive2: " << myFive2 << std::endl;

  std::string str3= create<std::string>(std::string("Rvalue"));
  std::cout << "str3: " << str3 << std::endl;

  std::string str4= create<std::string>(std::move(str3));
  std::cout << "str4: " << str4 << std::endl;

  // Arbitrary number of arguments
  double doub= create<double>();
  std::cout << "doub: " << doub << std::endl;

  MyStruct myStr= create<MyStruct>(2011,3.14,str4);

  std::cout << std::endl;

}
반응형

'💻 programming > c++' 카테고리의 다른 글

[c++] optional  (0) 2020.12.15
[c++] move  (0) 2020.12.15
[c++] async  (0) 2020.12.15
[c++] packaged task  (0) 2020.12.15
[c++] future  (0) 2020.12.15

댓글