멤버 데이터 포인터.
멤버 함수 포인터는 흔히들 쉽게 다룰 수 있을 만한 주제가 아닙니다. 표기법 및 사용법도 복잡하지만 내부적으로 동작하는 방식 역시 컴파일러 벤더마다 다른 것으로 알려져 있습니다. 멤버 데이터 포인터는 멤버 함수 포인터 보다도 더 알려지지 않은 C++의 고급 기능 중 하나 입니다. 인터넷을 검색해봐도 멤버 데이터 포인터에 관한 자료는 쉽게 접할 수가 없습니다. 잘 알려지지 않았 다는 것은 멤버 데이터 포인터가 상대적으로 그다지 효용성이 없는 기능이기 때문일 지도 모르지만 반대로 아직까지 이 기능을 최적으로 적용할 수 있을만한 예가 발견되지 않았기 때문일 지도 모릅니다. 멤버 데이터 포인터가 어떤 특징을 가지는지 한번 쯤 살펴보는 것도 나쁘지 않을거라 생각했습니다.
이번 기회에 멤버 데이터 포인터를 자세히 공부하면서 스스로 터득한 내용들을 나름대로 정리해봤습니다. 지극히 제 주관적인 해석에 의한 내용을 다루기 때문에 잘못된 내용이 있을 지도 모릅니다. 오류를 발견하시면 지적해 주시면 감사 하겠습니다.
1. 멤버 데이터 포인터 변수의 선언.
다음은 설명을 이어 나가면서 사용할 예제 구조체입니다.
struct student
{
int id;
int age;
std::string name;
};
student 구조체에 속하는 멤버 데이터 포인터 변수의 선언은 다음과 같이 합니다.
int student::*int_ptr_of_student = 0;
std::string student::*str_ptr_of_student = 0;
멤버 데이터 '포인터'도 포인터 이기 때문에 0 으로 초기화 하는 것이 가능합니다.
첫 번째 int_ptr_of_student 변수를 student 구조체 멤버 중에서 int 형인 멤버의 포인터로 선언합니다. 따라서 id 또는 age 가 잠정적으로 이 변수에 대입할 수 있는 후보 값이라고 할 수 있습니다. 두 번째 str_ptr_of_student 는 student 구조체 멤버 중에서 std::string 형인 멤버의 포인터로 선언합니다. 따라서 위 예제에서는 name 이 이 변수에 대입할 수 있는 유일한 값이 됩니다.
조금 복잡해 보여도 잘 들여다 보면 논리적으로 합이 들어맞는 것을 알 수 있습니다. student 구조체의 멤버라는 것을 지정하기 위해서 student:: 가 사용되고 바로 * 를 사용함으로써 포인터라는 것을 알립니다. 같은 맥락으로 전역으로 선언된 변수를 가리키는 포인터를 선언하기 위해서 다음과 같이 시도해 볼 수 있습니다.
// error C2645: no qualified name for pointer to member (found ':: *')
int ::*int_ptr_of_global = 0;
컴파일 오류 발생합니다. --; 그냥 int * int_ptr = 0; 으로 선언하면 됩니다.
2. 멤버 데이터 포인터 변수의 초기화.
당연한 이야기지만 멤버 데이터 포인터 변수는 멤버 데이터 포인터 값을 할당 함으로써 초기화할 수 있습니다. 그럼 멤버 데이터 변수 값은 어떻게 표기할 수 있는지를 알아야 겠습니다.
int student::*int_ptr_of_student1, student::*int_ptr_of_student2;
int_ptr_of_student1 = &student::id;
int_ptr_of_student2 = &student::age;
위에서 보는 바와 같이 멤버 데이터 포인터는 멤버 함수 포인터와 똑같은 방식으로 표기합니다.
3. 멤버 데이터 포인터의 사용. (pointer-to-member 연산자).
멤버 데이터 포인터나 멤버 함수 포인터나 양쪽 경우 모두, 멤버를 접근하기 위해서는 멤버 접근 포인터 (pointer-to-member) 연산자 .* 또는 ->* 를 사용합니다.
student s1;
s1.id = 1;
s1.age = 15;
s1.name = "Daniel";
student * ps1 = &s1;
int student::*int_ptr_of_student = &student::id;
std::string student::*str_ptr_of_student = &student::name;
assert( s1.*int_ptr_of_student == s1.id );
assert( !strcmp( (s1.*str_ptr_of_student).c_str(), s1.name.c_str() ) );
assert( ps1->*int_ptr_of_student == ps1->id );
assert( !strcmp( (ps1->*str_ptr_of_student).c_str(), ps1->name.c_str() ) );
이제 표현이 조금 복잡해 졌지만 아직 까지는 그럭저럭 참고 봐 줄만합니다. 멤버 데이터 포인터 변수를 사용하지 않고 멤버 데이터 포인터 값을 직접 사용하여 접근할 수도 있습니다.
assert( s1.*&student::id == s1.id );
assert( !strcmp( (s1.*&student::name).c_str(), s1.name.c_str() ) );
assert( ps1->*&student::id == ps1->id );
assert( !strcmp( (ps1->*&student::name).c_str(), ps1->name.c_str() ) );
변수 int_ptr_of_student 를 값 &student::id 로 치환하고 변수 str_ptr_of_student 는 값 &student::name 으로 치환하기만 하면 됩니다. 표현식이 더 복잡해 졌지만 ( .*& 또는 ->*& ) 그 의미를 정확히 알 수 있습니다.
멤버 접근 포인터 (pointer-to-member) 연산자의 생김새를 그 동작과 연과시켜서 생각해보면 역시 논리적으로 앞 뒤가 들어맞는 다는 것을 알 수 있습니다. 멤버를 접근 ( . 또는 -> 사용 ) 하는데, 포인터를 사용하여 접근하기 때문에 포인터를 역참조 ( * ) 하라는 의미에서 사용하는 .* 또는 ->* 연산자는 상황에 잘 부합한다고 생각되어집니다.
4. 연산자 오버로딩.
잠깐 삼천포로 빠집니다~
일반적으로 멤버 접근 (member access) 연산자 . 또는 -> 를 연산자 오버로딩해야 하는 상황은 일반적인 프로그래밍에서는 그 다지 접할 기회가 없습니다. 아마도 스마트 포인터를 작성하고자 하는 라이브러리 제작자에게나 필요한 기능 일 겁니다.
잘 아시다시피 C++ 에서 . 는 연산자 오버로딩이 불 가능 합니다. 반면에 -> 는 연산자 오버로딩이 가능합니다. 두 연산자 모두 단항 연산자입니다.
평소에 맘에 안들던 동료 프로그래머를 골탕 먹이기 위해서 다음과 같이 멤버 접근 연산자를 오버로딩 해서 동료에게 보냈습니다. 어떻게 어떻게 해서 student 구조체의 구현은 몰래 숨겼 두었습니다.
struct student
{
int id;
int age;
std::string name;
student * operator ->()
{
return 0;
}
};
void main()
{
student ps1 = new student;
ps1->id = 1;
ps1->age = 15;
ps1->name = "Daniel";
delete ps1;
}
예상 밖으로 한 시간 두 시간을 기다려도 동료에게서 불평하는 메세지는 오지 않습니다. 그럴 수 밖에 없는 것이 멤버 접근 연산자 오버로딩이 전혀 호출되지 않고 있기 때문입니다.
void main()
{
student s1;
s1->id = 1;
s1->age = 15;
s1->name = "Daniel";
}
위와 같이 호출해야지만 -> 연산자 오버로딩이 호출됩니다. 즉 연산자 오버로딩은 객체에 대해서 동작하지 포인터에 대해서는 동작하지 않습니다. 한 가지 더 기억할 점은 멤버 접근 연산자 -> 의 오버로딩은 구조체 또는 클래스 밖에서 정의할 수 없습니다. 항상 클래스 멤버 함수로써 정의 해야만 합니다.
따라서 다음 코드는 컴파일 오류를 발생시킵니다.
// error C2801: 'operator ->' must be a non-static member
student * operator ->(student & s)
{
return &s;
}
멤버 접근 연산자 . 가 연산자 오버로딩이 불가능 하듯이 멤버 접근 포인터 (pointer-to-member) 연산자 .* 역시 연산자 오버로딩이 불가능합니다. 반면에 ->* 는 연산자 오버로딩이 가능합니다. 멤버 접근 포인터 (pointer-to-member) 연산자 .* 와 ->* 는 모두 이항 연산자 입니다. 게다가 항상 클래스 또는 구조체 멤버 함수로써 연산자 오버로딩을 해야마 하는 -> 연산자 와는 달리 구조체나 클래스 외부 에서 ->* 연산자 오버로딩을 하는 것이 가능합니다.
따라서 다음과 같이 ->* 연산자를 오버로딩 해봅니다.
// error C2803: 'operator ->*' must have at least one formal parameter of class type
int operator ->*( student * s, int (student::*int_ptr_of_student) )
{
return s->*int_ptr_of_student;
}
위의 -> 연산자 오버로딩의 예제에서 언급 했듯이 연산자 오버로딩은 포인터가 아닌 객체에 대해서 동작 한다는 것을 위의 예제 코드에서 발생하는 컴파일 오류를 확인하면서 상기할 수 있습니다. 위의 코드가 설령 문제없이 컴파일이 된다고 해도 재귀 호출 무한 루프에 빠지게 됩니다.
따라서 ->* 연산자 오버로딩은 구조체 또는 클래스 내부에서 다음과 같이 정의하거나,
struct student
{
int id;
int age;
std::string name;
int operator ->*( int (student::*int_ptr_of_student) )
{
return this->*int_ptr_of_student;
}
};
또는 클래스 외부에서 라면 다음과 같이 정의할 수 있습니다.
int operator ->*( student & s, int (student::*int_ptr_of_student) )
{
return s.*int_ptr_of_student;
}
->* 연산자 오버로딩의 리턴 값은 우변 입력으로 주어지는 인자가 멤버 함수 포인터인 경우에는 함수 호출 연산자 operator ()()를 가지는 호출 가능한 개체이어야 하고 우변 입력으로 주어지는 인자가 멤버 데이터 포인터인 경우에는 죄변 값 (l-value)을 리턴 해야합니다.
중요한 점은 .* 또는 ->* 연산자가 이항 연산자 이며 멤버 함수 포인터 또는 멤버 데이터 포인터를 입력 인자로써 받는 다는 사실입니다.
이를 조금 다르게 해석하면 . 또는 -> 연산자는 컴파일 시점에서 정적으로 바인딩 되는데 반해서 .* 또는 ->* 연산자는 동적으로 바인딩할 수 있다는 의미가 됩니다. 이제부터 이게 무슨 의미인지 알아봅니다.
5. 멤버 데이터 포인터의 사용 2.
int_ptr_of_student = &student::id;
assert( 1 == s1.*int_ptr_of_student ); // (1)
assert( 1 == s1.id ); // (2)
assert( s1.id == s1.*int_ptr_of_student );
int_ptr_of_student = &student::age;
assert( 15 == s1.*int_ptr_of_student ); // (3)
assert( 15 == s1.age ); // (4)
assert( s1.age == s1.*int_ptr_of_student );
위의 예제의 (2) 에서 s1.id 를 사용하여 s1 객체의 id 멤버를 접근하는 것과 (1) 에서 멤버 데이터 포인터 변수 int_ptr_of_student 를 사용하여 s1 객체의 id 멤버를 접근하는 것은 결과는 같을지 언정 (3), 그 의미는 상당히 다릅니다.
멤버 접근 연산자 . 를 사용하는 전자의 경우인 s1.id 는 컴파일 시점에서 s1 객체와 id 멤버가 정적으로 바인딩됩니다. 멤버 데이터 포인터 변수를 사용하는 후자의 경우에는 컴파일 시점이 아닌 실행 시점에서 s1 객체와 s1의 멤버 중 int 형인 멤버가 동적으로 바인딩 됩니다.
위에서 s1.*int_ptr_of_student 라는 동일한 문장이 문맥에 따라서 서로 다른 값을 가질 수 있다는 것을 의미합니다. (2)의 s1.id 는 죽어다 깨어나도 s1의 id 멤버를 가리키지만 s1.*int_ptr_of_student 는 id 멤버가 될 수도 (1) 또는 age 멤버가 될 수도 (3) 있습니다.
과연 이런 특징을 어떻게 사용할 수 있을까요? 다음과 같은 예제 함수를 살펴봅니다.
struct student
{
int id;
int age;
std::string name;
};
void process_student(student const & s, int (student::*int_ptr_of_student) )
{
std::cout << "Processing " << s.name.c_str();
if(&student::id == int_ptr_of_student)
{
std::cout << "'s id(";
}
else if(&student::age == int_ptr_of_student)
{
std::cout << "'s age(";
}
std::cout << s.*int_ptr_of_student << ")..." << std::endl;
}
void main()
{
student s1;
s1.id = 1;
s1.age = 15;
s1.name = "Daniel";
process_student( s1, &student::id );
process_student( s1, &student::age );
}
멤버 데이터 포인터를 입력으로 받는 단일 함수에서 주어지는 멤버 데이터 포인터 값에 따라서 서로 다른 일을 수행할 수 있습니다. 위와 같은 기능의 함수를 . 또는 -> 연산자를 사용해서는 작성할 수 없습니다.
6. 중첩 구조체의 접근.
조금 더 복잡한 경우를 살펴봅니다.
struct student
{
int id;
int age;
std::string name;
};
struct school_class
{
std::vector<student> male_students;
std::vector<student> female_students;
};
새로운 구조체 school_class 는 다른 구조체 멤버를 포함하고 있습니다. 이제 부터 조금씩 복잡 해집니다.
school_class sc;
school_class * psc = ≻
sc.male_students.push_back( s1 );
std::vector<student> (school_class::*mates_ptr_of_school);
mates_ptr_of_school = &school_class::male_students;
// --------------------------------------------------------------------------------
// operator .*
// --------------------------------------------------------------------------------
assert( 1 == sc.male_students[0].id );
assert( 15 == sc.male_students[0].age );
assert( 1 == (sc.*mates_ptr_of_school)[0].id );
assert( 15 == (sc.*mates_ptr_of_school)[0].age );
assert( 1 == (sc.*&school_class::male_students)[0].id );
assert( 15 == (sc.*&school_class::male_students)[0].age );
assert( 1 == (sc.*&school_class::male_students)[0].*&student::id );
assert( 15 == (sc.*&school_class::male_students)[0].*&student::age );
assert( 1 == (*(sc.*&school_class::male_students).begin()).*&student::id );
assert( 15 == (*(sc.*&school_class::male_students).begin()).*&student::age );
// --------------------------------------------------------------------------------
// operator ->*
// --------------------------------------------------------------------------------
assert( 1 == psc->male_students[0].id );
assert( 15 == psc->male_students[0].age );
assert( 1 == (psc->*mates_ptr_of_school)[0].id );
assert( 15 == (psc->*mates_ptr_of_school)[0].age );
assert( 1 == (psc->*&school_class::male_students)[0].id );
assert( 15 == (psc->*&school_class::male_students)[0].age );
assert( 1 == (psc->*&school_class::male_students)[0].*&student::id );
assert( 15 == (psc->*&school_class::male_students)[0].*&student::age );
assert( 1 == &(*(psc->*&school_class::male_students).begin())->*&student::id );
assert( 15 == &(*(psc->*&school_class::male_students).begin())->*&student::age );
연산자 우선 순위에 주의하여 괄호만 잘 사용하면 중첩된 구조체의 멤버를 멤버 데이터 포인터를 사용하여 접근하는 것이 가능합니다. 물론 표현식이 극악의 복잡도를 가지게 되지만 멤버 데이터 포인터를 사용하는 경우 동적인 바인딩이 이루어진다는 특징을 이용할 수 있습니다.
student s2;
s2.id = 2;
s2.age = 14;
s2.name = "Goun";
student s3;
s3.id = 3;
s3.age = 17;
s3.name = "Jae";
student s4;
s4.id = 4;
s4.age = 16;
s4.name = "Mina";
sc.female_students.push_back( s2 );
sc.male_students.push_back( s3 );
sc.female_students.push_back( s4 );
mates_ptr_of_school = &school_class::male_students;
int_ptr_of_student = &student::id;
assert( 1 == (psc->*mates_ptr_of_school)[0].*int_ptr_of_student );
assert( 1 == psc->male_students[0].id );
mates_ptr_of_school = &school_class::female_students;
int_ptr_of_student1 = &student::age;
assert( 14 == (psc->*mates_ptr_of_school)[0].*int_ptr_of_student );
assert( 14 == psc->female_students[0].age );
psc->*mates_ptr_of_school)[0].*int_ptr_of_student1 라는 동일한 문장이 멤버 데이터 포인터 변수 mates_ptr_of_school 또는 int_ptr_of_student 의 값에 따라서 다른 결과를 리턴합니다.
다음과 같이 응용이 가능할 것 같습니다.
void process_class(school_class & sc, std::vector<student> (school_class::*mates_ptr_of_school), int (student::*int_ptr_of_student) )
{
std::cout << "Processing ";
if(&school_class::male_students == mates_ptr_of_school)
{
std::cout << "male students... " << std::endl;
}
else if(&school_class::female_students == mates_ptr_of_school)
{
std::cout << "female students... " << std::endl;
}
std::vector<student>::iterator it = (sc.*mates_ptr_of_school).begin(), it_e = (sc.*mates_ptr_of_school).end();
for( ; it != it_e; ++it )
{
student & s = *it;
process_student( s, int_ptr_of_student );
}
}
void main()
{
student s1;
s1.id = 1;
s1.age = 15;
s1.name = "Daniel";
student s2;
s2.id = 2;
s2.age = 14;
s2.name = "Goun";
student s3;
s3.id = 3;
s3.age = 17;
s3.name = "Jae";
student s4;
s4.id = 4;
s4.age = 16;
s4.name = "Mina";
school_class sc;
sc.male_students.push_back( s1 );
sc.female_students.push_back( s2 );
sc.male_students.push_back( s3 );
sc.female_students.push_back( s4 );
// process male student's id
process_class( sc, &school_class::male_students, &student::id );
// process female student's age
process_class( sc, &school_class::female_students, &student::age );
}
7. boost::lambda와 멤버 데이터 포인터
멤버 데이터 포인터 사용의 묘미는 boost::lambda 를 사용하여 STL 의 functional 이나 algorithm 에서 사용 할 무명 함수 객체를 작성 하고자 할 때 그 절정에 달한다고 생각합니다.
다음은 여기 데브피아에도 자주 올라오는 STL 질문입니다.
질문: 학생의 id 에 따라서 vector 를 정렬하고 싶습니다. 다음과 같이 했더니 컴파일 오류가 발생합니다. 어떻게 해야 하나요?
struct student
{
int id;
int age;
std::string name;
};
void sort_student(std::vector<student> & mates)
{
// compiler error!
std::sort( mates.begin(), mates.end() );
}
답변1: 연산자 오버로딩을 해주면 됩니다. student 구조체에 operator <() 를 오버로딩 하세요. 다음과 같이 수정하면 되겠네요.
struct student
{
int id;
int age;
std::string name;
bool operator <(student & rhs) const
{
return this->id < rhs.id;
}
};
void sort_student(std::vector<student> & mates)
{
std::sort( mates.begin(), mates.end() ); // OK!
}
답변 2. 앞선 분께서 잘 설명해주셨네요. 그런데 그 방법 말고 함수 객체를 만드는 방법도 있답니다. 다음과 같이 해보세요.
struct student
{
int id;
int age;
std::string name;
};
struct comp_student
{
bool operator ()(student & lhs, student & rhs) const
{
return lhs.id < rhs.id;
}
};
void sort_student(std::vector<student> & mates)
{
std::sort( mates.begin(), mates.end(), comp_student() ); // OK!
}
개인적으로 답변 2 보다 답변 1 을 아주 조금 더 선호합니다. 필요할 때마다 커스텀 함수 객체를 만드는 것이 번거로울 뿐만 아니라 소스가 분산되는 문제점이 발생할 수 있기 때문입니다.
답변 1의 경우도 POD (Plain Old Data) 데이터형이 연산자 오버로딩 때문에 Non-POD 데이터 형으로 바뀌는 단점아닌 단점이 있을 수도 있습니다. 물론 위의 student 구조체는 std::string 때문에 원래부터 POD가 아니었지만 Non-POD로 변경 되는 경우에는 initilizer 를 이용하여 초기화를 못하게 될 수도 있습니다.
boost::lambda 를 사용하면 훨씬 근사한 방법이 있습니다. lambda 를 사용하여 무명 함수 객체를 작성하는 방법입니다.
#include <boost/lambda/lambda.hpp>
namespace lambda = boost::lambda;
struct student
{
int id;
int age;
std::string name;
};
void sort_student(std::vector<student> & mates)
{
std::sort( mates.begin(), mates.end(), &lambda::_1->*&student::id < &lambda::_2->*&student::id ); // OK!
}
boost::lambda 의 복잡한 내용은 신경 쓰지 말고 그냥 보이는 그대로 해석하면 됩니다. lambda::_1 은 std::sort 알고리즘이 비교할 때 좌변 값으로 주어지는 입력 입니다. 따라서 lambda::_2 은 우변 값으로 주어지는 입력 이라는 것을 알 수 있습니다. std::vector<student> 컨테이너를 정렬하는 예제이기 때문에 입력으로 주어지는 값이 student 형 객체라는 것은 쉽게 알 수 있습니다. 즉, lambda::_1 이나 lambda::_2 는 student 형 객체라는 사실을 인지하고 멤버 데이터 포인터의 사용법에 유의하면 위의 코드를 이해할 수 있습니다.
위에서는 멤버 데이터 포인터를 사용하여 student 형 객체의 id 멤버를 비교하는 것이 가능합니다. 한 가지 주의할 점은 boost::lambda 의 구현 상 제약적인 이유도 있지만 .* 는 연산자 오버로딩이 불가능하기 때문에 &lambda::_1->* 와 같은 방식으로 멤버 데이터 포인터를 사용해야 합니다.
student 구조체를 수정할 필요도 없었고 커스텀 함수 객체를 작성하지도 않습니다. std::sort 를 수행하는 한 지점에서 행동 양식을 결정합니다. 만약 id 가 아닌 age 에 의해서 정렬 하고자 한다면 정렬 함수를 다음과 같이 변경하면 됩니다.
void sort_student(std::vector<student> & mates)
{
std::sort( mates.begin(), mates.end(), &lambda::_1->*&student::age < &lambda::_2->*&student::age ); // OK!
}
조금 더 복잡한 예제를 살펴 봅니다.
질문: 사각형의 id 를 키 값으로 사각형 좌표값을 대응시키는 맵 구조가 있습니다. 화면 상의 특정 좌표가 주어졌을 때 이 좌표를 포함하는 사각형의 id를 찾고 싶습니다.
#include <algorithm>
#include <boost/lambda/lambda.hpp>
namespace lambda = boost::lambda;
typedef std::map<int, RECT> RECT_MAP;
int find_id_of_rect_on(RECT_MAP const & rect_map, POINT pt)
{
typedef RECT_MAP::value_type value_pair;
RECT_MAP::const_iterator it_f = std::find_if( rect_map.begin(), rect_map.end(),
lambda::bind<BOOL>( &::PtInRect, &(&lambda::_1->*&value_pair::second), pt ) );
return it_f == rect_map.end() ? -1 : it_f->first;
}
여태까지 앞에 설명된 내용을 잘 이해 하셨다면 &(&lambda::_1->*&value_pair::second) 가 반복되어지는 map 구성 요소에 저장된 RECT 형 포인터를 의미한다는 것을 알수 있으실 겁니다.
boost::bind 랑 boost::lambda::bind 는 서로 잘 호환되어 동작하지 않습니다. lambda 를 쓸 때는 전용의 boost::lambda::bind 를 사용하는 게 좋습니다.
그런데 lambda 는 그 기능이 막강하기 때문인지 템플릿 지원이 미비한 VC6 같은 구닥다리 컴파일러를 전혀 지원하지 않습니다. 그래서 VC6 에서도 동작하는 기본적인 기능의 lambda 라이브러리를 직접 만들어 보고자 합니다.
... continued ...