-
람다의 참조 캡처는 종종 불안하다개발/C·C++ 2024. 11. 22. 21:02
앞서, 콜백 함수의 인자는 C++17의 apply와 tuple을 이용해 처리할 수 있지만 예제를 최대한 간소화하기 위해 콜백 함수의 인자는 하나로 설정했다. 같은 이유로 요청을 처리할 함수는 멤버 함수가 아니라 전역 함수를 사용했다. 커맨드 패턴을 활용해 요청을 Job 객체로 만들어 큐에 넣고 공통 api-보통 커맨드 패턴에서는 Execute()를 애용하므로- Excute()를 호출해 Job(이하 일감)을 처리한다.
일감을 실행하기 위한 callback 함수를 요청 객체에서 가지고 있다가 Execute()를 할 때 callback() 함수를 호출하는 구조를 단순홰해서 볼 건데 callback은 람다로 구현할 것이다. 이 때 람다 본문에서 필요한 변수들은 참조 객체를 해도 될까? 값 복사는 비용이 들고 참조는 저렴한 대신 주의가 필요하다.
먼저 main을 보자.
int main() { queue<Job> job_queue; // 다른 스코프에서 job_queue에 객체를 넣는다 { Args a(10); Job job(job_func, move(a)); job_queue.push(move(job)); } auto& job = job_queue.front(); job.Execute(); job_queue.pop(); }
핵심이 Job의 생성자가 된다.
class Job { public: template<typename Return, typename Arg> Job(Return(*callback)(Arg), Arg&& arg) { _callback = [callback, &arg]() { callback(forward<Arg>(arg)); }; } Job(const Job&) = delete; Job(Job&& rhs) noexcept { _callback = rhs._callback; rhs._callback = nullptr; } void Execute() { _callback(); } private: std::function<void()> _callback; };
우리 예제에서 함수 인자와 Job에 전달하는 인수와의 타입 일치를 위해 move()를 이용해 인수를 전달하는데, 이렇게 되면 Job 생성자의 두 번째 인수의 타입이 오른값 참조가 된다. 이 오른값 참조를 람다에서 참조 캡처를 하고 callback() 함수에 전달할 때는 forward<>를 이용해 완벽 전달이 되도록 한다. 방금의 어느 부분에서도 복사는 일어나지 않았다. callback() 함수의 인자 타입의 이동 생성자를 통해 인자를 적절하게 초기화해주자. 참조 캡처를 사용할 때 걱정해야 하는 부분이 원본이 사라지는 것인데, 오른값 참조로 넘어온 데이터를 이동 생성자를 통해 초기화해주면 괜찮아진다.
만약 main() 함수에서 job 객체를 생성할 때 move()를 사용하지 않으면 어떻게 될까? 다음이 일감 객체에서 호출될 함수, 인자에 해당하는 클래스다.
class FuncArg { public: FuncArg() = default; FuncArg(const FuncArg&) { cout << "const Args&" << endl; } FuncArg(FuncArg&& rhs) noexcept { cout << "FuncArg&&" << endl; _a = rhs.get(); } FuncArg(int a) : _a(a) {} int get() const { return _a; } private: int _a = 0; }; void job_func(FuncArg arg) { cout << "job_func -> " << arg.get() << endl; }
move()를 사용하지 않고 job 객체를 만들면, Job 생성자에 전달하는 인수의 타입이 왼값 참조가 되기 때문에(FuncArg&), job_func의 인자 타입(FuncArg)과 불일치하게 된다. move()를 이용해서 전달하면 생성자의 인자가 오른값 참조가 되면서 (Arg&& -> FuncArg&&), 전체 타입은 오른값 참조이지만 Arg 타입은 FuncArg가 되기 때문에 job_func의 인자 타입과 일치하게 된다.
이런 현상이 생기는 이유는 템플릿 함수의 파라미터 추론을 왼쪽부터 하기 때문이다. job 객체를 생성할 때 왼쪽에 job_func를 먼저 넣어주기 때문에 Job 생성자의 Arg 타입이 FuncArg로 먼저 추론되므로, 여기에 맞춰서 두 번째 인수를 move()를 이용해 전달해야 한다.
다음 이미지가 말하는 것은 콜백 함수에 완벽전달로 인수를 전달했으므로 이동 생성자가 호출됐다는 의미다. 콜백 함수는 Job 클래스의 Execute() 함수를 호출하면 된다. 큐에서 일감을 꺼낼 때는 왼값 참조로 받아 복사를 방지한다.
'개발 > C·C++' 카테고리의 다른 글
[Protobuf 따라하기] 수신 데이터를 복사 없이 가공하기 (1) 2024.11.08 VirtualAlloc()의 메모리 할당에 대해 (0) 2024.10.12 64비트 시스템에서 메모리 정렬 경계에 대해 (0) 2024.10.11 포인터 붕괴와 붕괴가 아닌 것 (0) 2024.09.28 덧셈 연산 어셈블리 코드 간단 분석 (1) 2024.09.07