-
std::transform()에 대한 간단한 고찰개발/C·C++ 2024. 9. 4. 22:21
C++ 문법에 대해 살펴보다가 재미있는 코드가 있어서 간단하게 고찰해보려고 한다.
공식 문서에 구현 예제까지 나와있다.
헤더는 <algorithm>
template<class InputIt, class OutputIt, class UnaryOp> constexpr //< since C++20 OutputIt transform(InputIt first1, InputIt last1, OutputIt d_first, UnaryOp unary_op) { for (; first1 != last1; ++d_first, ++first1) *d_first = unary_op(*first1); return d_first; }
template<class InputIt1, class InputIt2, class OutputIt, class BinaryOp> constexpr //< since C++20 OutputIt transform(InputIt1 first1, InputIt1 last1, InputIt2 first2, OutputIt d_first, BinaryOp binary_op) { for (; first1 != last1; ++d_first, ++first1, ++first2) *d_first = binary_op(*first1, *first2); return d_first; }
대략적인 흐름은 원본 데이터에 대한 시작 반복자, 끝 반복자로 범위를 지정해주고
결과물을 저장할 자료구조(우리 예제에서는 vector)의 시작 반복자,
그리고 데이터를 가공할 방법을 안내하는 콜백 함수를 넣어주는 형태다.
앞서 잠시 언급했듯이 vector를 사용해 볼 건데,
transform()에 넣을 함수 포인터는 멤버 함수에 대한 함수 포인터다.
멤버 함수 포인터를 만드는 방법은 일반 함수와 조금 다르다.
멤버 함수를 호출할 때 보통 "인스턴스." 혹은 "포인터 변수->"의 방식으로
멤버 함수를 호출하고 필요한 인수를 넣는데 컴파일러가 추가하는 코드가 있다.
바로 멤버 함수의 첫 번째 인자로 해당 인스턴스의 주소값을 넘기는 점이다.
클래스 입장에서는 모든 멤버 함수 내부에 this가 생기는 거다.
이런 일반 함수와의 차이 때문에 멤버 함수의 포인터를 생성할 때 이에 대해 명시를 해줘야 한다.
다음을 보면 멤버 함수의 첫 번째 인수가 this임을 확실히 알 수 있다.
class TestClass { public: int some_func(int a) { cout << "some_func -> " << a << endl; return 1; } }; int main() { TestClass tc; // int(TestClass*, int)로 지정해서 f(&tc, 5)도 가능하다 function<int(TestClass&, int)> f = &TestClass::some_func; f(tc, 5); }
다시 transform으로 돌아와보자. 만들어 볼 것은 다음과 같다.
1. 크기를 지정해서 만든 vector<int> 인스턴스를 5개 생성한다
2. 이들 인스턴스 5개를 멤버로 가지고 있는 wrapper_vec을 만든다
3. 새로운 vector<int> 인스턴스를 만들어 wrapper_vec의 각 멤버들의 사이즈를 값으로 저장한다
1. 크기를 지정해서 vector<int> 5개 만들어보자.
vector<int> a(1); vector<int> b(2); vector<int> c(3); vector<int> d(4); vector<int> e(5)
2. 이 각각의 인스턴스를 멤버로 가지고 있는 wrapper_vec을 만든다.
vector<vector<int>> vec_wrapper; vec_wrapper.push_back(a); vec_wrapper.push_back(b); vec_wrapper.push_back(c); vec_wrapper.push_back(d); vec_wrapper.push_back(e);
3. vertor<int> size_vec을 만들어 wrapper_vec의 각 멤버의 사이즈를 값으로 저장한다
이 때 멤버함수 포인터를 넘기거나 람다를 사용하는 방법 둘 다 가능하다.
// 멤버함수 포인터 vector<int> size_vec(5); function<size_t(vector<int>&)> functor_vector = &vector<int>::size; transform(vec_wrapper.begin(), vec_wrapper.end(), size_vec.begin(), functor_vector); // 람다 transform(vec_wrapper.begin(), vec_wrapper.end(), size_vec.begin(), [](vector<int>& vec) { return vec.size(); });
mem_fn() 함수를 사용하면 별도로 함수 포인터 변수를 만들지 않아도 된다.
transform(vec_wrapper.begin(), vec_wrapper.end(), size_vec.begin(), mem_fn(&vector<int>::size));
그리고 멤버 함수 포인터를 넘길 때 다음과 같이 설정하면,
transform(vec_wrapper.begin(), vec_wrapper.end(), size_vec.begin(), &vector<int>::size);
Visual Studio 2022에서 출력되는 에러 메시지는 다음과 같다.
_Func는 최소 하나의 인수를 받아야 하는데 vector의 size() 함수는 인수를 받지 않는다.
멤버 함수 일 때, 코드 수준의 시그니처에는 인수를 받지 않아도
컴파일러가 해당 인스턴스의 주소를 첫 번째 인수로 추가한다.
이런 이유로 람다로 표현하면 조금 더 직관적으로 느껴진다.
functional 헤더를 추가하지 않아도 되는 장점이 있다.
개인적으로는 람다가 코드 가독성에 더 도움을 주는 것 같다.
다음은 예제 코드 전체.
#include <iostream> #include <functional> #include <vector> #include <algorithm> using namespace std; int main() { //// 벡터들의 각 크기로 벡터 만들기 vector<int> a(1); vector<int> b(2); vector<int> c(3); vector<int> d(4); vector<int> e(5); vector<vector<int>> vec_wrapper; vec_wrapper.push_back(a); vec_wrapper.push_back(b); vec_wrapper.push_back(c); vec_wrapper.push_back(d); vec_wrapper.push_back(e); vector<int> size_vec(5); function<size_t(vector<int>&)> functor_vector = &vector<int>::size; transform(vec_wrapper.begin(), vec_wrapper.end(), size_vec.begin(), &vector<int>::size); //transform(vec_wrapper.begin(), vec_wrapper.end(), size_vec.begin(), [](vector<int>& vec) { // return vec.size(); // }); // 함수 객체를 만들어서 반환해주는 mem_fn() 함수 // transform(vec_wrapper.begin(), vec_wrapper.end(), size_vec.begin(), mem_fn(&vector<int>::size)); for (auto it = size_vec.begin(); it != size_vec.end(); ++it) { cout << "vector size: " << *it << endl; } }
'개발 > C·C++' 카테고리의 다른 글
포인터 붕괴와 붕괴가 아닌 것 (0) 2024.09.28 덧셈 연산 어셈블리 코드 간단 분석 (1) 2024.09.07 포인터의 타입 추론 (0) 2024.08.31 [사소한 테스트] 곱셉/나머지 연산의 로직을 덧셈/뺄셈으로 변경한다면? (1) 2024.08.28 [윈도우 시스템 프로그래밍] 압축 파일 복사하기 (0) 2024.08.27