본문 바로가기

C++

C++ 면접 질문) "Perfect Forwarding이 뭔가요? 관련해서 사용한 경험 있으시면 말씀해주세요."

Perfect Forwarding 소개:

  • Perfect Forwarding은 템플릿에서 잘 사용되는 개념 입니다.
    • 예시:
void function_name(int& lvalue) {
  std::cout << "this fuction accepts lvalue: " << lvalue << std::endl;
}

void function_name(int&& rvalue) {
  std::cout << "this function accepts rvalue: " << rvalue << std::endl;
}

- 위와 같은 경우에 템플릿으로 임의의 Type 을 받았을 때, function_name 을 호출하고 싶은데, 받은 Type이 lvalue 인지 rvalue 인지에 따라 다른 함수를 호출하고 싶으면 어떻게 할까?

template <class Type>
void template_fuction(Type&& parameter) { // universal reference: 유니버셜 참조
  function_name(std::forward<Type>(parameter); // std::forward: C++ 표준 라이브러리 utility 함수
}

- 위 코드가 perfect forwarding 이다. universal reference를 통해 lvalue 와 rvalue 가 모두 인자로 올 수 있다는 것을 나타낸다. lvalue가 오면 lvalue를 function_name 인자로 넣어주고, rvalue가 오면 rvalue를 function_name 인자로 넣어주는 것이 perfect forwarding (속성이 유지됨) 이다. 이때, std::forward 를 사용해 넣는 parameter 인자가 rvalue면 rvalue를 쓰겠다고 알려줘야 한다. 아니면, lvalue로 가정해 rvalue가 lvalue로 변환되어 호출된다.

 

universal reference가 뭐고 std::forward 는 꼭 같이 써야 하는가:

  • universal reference는 템플릿 작성 시 매개 변수 옆에 &&을 붙이는 경우 동작하는 개념입니다.
    • 템플릿이 아닌 비템플릿 코드에서 매개 변수 옆에 &&을 붙이면 rvalue 매개 변수를 뜻합니다.
  • universal reference는 매개변수로 lvalue, rvalue 가 모두 올 수 있다는 것을 뜻합니다.
  • std::forward 는 설계 될 때 템플릿에서 universal reference와 같이 사용되도록 설계 되었습니다.
    • 단, template 을 안쓰고 명시적으로 type을 rvalue로 정의하는 경우 universal reference 없이 std::forward 를 사용 할 수 있습니다.
void template_function(int&& parameter) { // 직접 매개변수 정의
  function_name(std::forward<int&&>(parameter));
}

- 단, 이때는 std::move와 똑같이 동작하므로, std::forward 를 사용해야 할 의무는 없습니다.

 

C+11 에서 move semantics 도입한 배경:

  •  복사가 아닌 이동을 위해서:
    • 복사는 비싸다는 것을 보여주는 예시:
/* 테스트 준비 */

class LargeObject {
private:
  std::vector<int> data;
  
public:
  LargeObject(size_t size) : data(size, 0) {
    // size_t 는 플랫폼 종속적인 양의 정수를 나타낼 때 씀
  }
  
  LargeObject(const LargeObject& other) : data(other.data) {
    // copy constructor
  }
  
  LargeObject(LargeObject&& other) noexcept : data(std::move(other.data)) { // member 변수로 쓰는 std::vector는 move constructor가 noexcept 이므로 noexcept 가능
    // move constructor
  }
  
  LargeObject& =operator(const LargeObject& other) {
    // copy assignment operator
    
    if (&other != this) { // address를 비교. other != *this 는 value를 비교한다
      // avoid self-assignment
      data = other.data;
      // vector 는 별도 delete 과정이 필요없다 (내부적으로 처리)
    }    
    return *this;
  }
  
  LargeObject& =operator(LargeObject&& other) noexcept { // 내부적으로 쓰이는 member 변수의 vector의 move assignment operator가 noexcept 이므로 noexcept 가능
    // move assignment operator
    
    if (&other != this) { // other != *this 와는 다르다. other != *this 는 value 비교를 뜻한다. 물론, 이는 !=operator 의 정의에 의해 동작한다.
      // avoid self-assignment
      data = std::move(other.data); // std::move의 반환값은 xvalue (rvalue) 이다.
      // rvalue를 기존에 생성된 vector에 대입하고 있으므로 vector의 move assignment operator 가 호출된다
      // vector의 move assignment operator가 other.data를 정리하므로 other.data.clear() 는 필요 없다.
    }
  }
};
/* 시간 측정 테스트 */

template <class Func>
void measure_time_taken(const std::string& operation_name, Func&& func) {
  auto start = std::chrono::high_resolution_clock::now();
  func();
  auto finish = std::chrono::high_resolution_clock::now();

  auto duration = finish - start;
  auto duration_cast = std::chrono::duration_cast<std::chrono::milliseconds>(duration);

  std::cout << operation_name << " took " << duration_cast.count() << " ms" << std::endl;
}

size_t size = 10'000'000; // '는 C++14부터 도입된 자릿수 구분 기호

LargeObject object(size);

measure_time_taken("copy constructor", [&](){
  LargeObject copy_object(object);
});

measure_time_taken("move constructor", [&](){
  LargeObject move_object(std::move(object));
});

LargeObject another_object(size);

measure_time_taken("copy assignment operator", [&](){
  copy_object = another_object;
});

measure_time_taken("move assignment operator", [&](){
  move_object = std::move(another_object);
});

 

rvalue 소개:

  • rvalue 자체는 C++98/03 부터 존재했던 개념
  • rvalue 는 C++11 이후 두 종류로 나뉨
    • pure rvalue (prvalue): 임시 객체 또는 리터럴 값
      • 리터럴 값: 5, "Hello World" 등 바로 값이 assembly code에 써짐
      • 임시 객체: 임의의 메모리 주소에 값을 적고, 그 메모리 주소를 참조하는 객체
    • xvalue (expiring value): 이동 대상 객체
/* prvalue */

// 리터럴
// 5, "Hello world"

int x = 5;

/* 임시 객체 */ 
// Foo()

Foo x{Foo()};
// uniform initialization 사용 의도적으로 Foo() 라는 임시 객체를 생성함.
// C++17 이후에서는 무조건 copy elision 발생.

/* xvalue */
// std::move() 가 반환하는 값

std::string str = "Hello, world!";
std::string moved_str = std::move(str);

 

다음 글:

- C++ 면접 질문) "uniform initialization 사용을 권장하는 이유를 혹시 아시나요?"

- 자료구조 면접 질문) "array와 linked list 의 차이점을 말해주세요."