Exceptional C++ Style : 15. 접근 권한의 사용과 오용

Access Modifiers



public
protected
private
class 자신
접근 가능
접근 가능
접근 가능
파생 class
접근 가능
접근 가능
접근 불가
friend
접근 가능
접근 가능
접근 가능
외부
접근 가능
접근 불가
접근 불가

위의 표를 보면 private나 혹은 protected는 외부에서 접근이 불가능하다고 하지만 방법이 없는 것은 아니다. 표준을 만족하지 않고 이식성(Portability)을 만족하지 않는 많은 편법이 존재하고 있으며 그 중 악명높은 방법들을 3가지로 압축하고 있다.
  1. Forger(위조범)
  2. Pocketpicker(소매치기)
  3. The Cheat(사기꾼)
그리고 표준을 완전히 만족하면서 이식성도 보장하는 1가지 방법을 소개한다.

다음 코드를 예로 설명하면

// X.h

#ifndef X_H_
#define X_H_
#include <stdio.h>
class X {
private:
   int private_;
public:
   X() : private_(1) {}
   virtual ~X();

   template<class T>
   void f(const T& t) {
   }
   int Value() {
       return private_;
   }
};

#endif /* X_H_ */

위의 코드는 private_라는 private member를 가지고 있고 1로 초기화 되어 있으며 해당 값을 변경할 수 있는 member function을 제공하지 않는다. 이 값을 앞서 말한 3가지 편법과 1가지 적법을 이용하여 변경해보면,

Forger(위조범)


X.h의 X class의 정의를 복사하고 friend를 추가한다.
이 방법이 위법인 이유는 ODR(One Definition Rule) 을 위반하기 때문이다. 단, 하나의 형식이 여러번 정의되는 경우에 대한 예외가 있으며 이 경우 그 정의들은 반드시 동일해야 한다.
위조범 방법의 경우 이 예외를 악용한 것으로, friend를 추가해도 객체의 내부적인 자료 배치는 변경되지 않기 때문에 대부분의 컴파일러에서 허용한다.
  1. forgerX.h
// forgerX.h
#ifndef FORGER_X_H_
#define FORGER_X_H_
#include <stdio.h>
class X {
private:
int private_;
public:
X();
virtual ~X();

template<class T>
void f(const T& t) {}
int Value() { return private_; }
friend void ::Hijack(X&);
};

void Hijack(X& x) {
   printf("forger!!\n");
   x.private_ = 2;
}

#endif /* FORGER_X_H_ */
  1. main.cpp
//#include "X.h"
#include "forgerX.h"
//#include "pickpocketX.h"
//#include "tricksterX.h"
#include <stdio.h>

int main() {
   X x;
   printf("before : %d\n", x.Value());

   Hijack(x);

   printf("after %d\n", x.Value());

   return 0;
}
  1. 실행결과
before : 1
forger!!
after 2

Pocketpicker(소매치기)


class의 정의의 의미를 바꾸는 살짝 바꾸는 방법으로 아래와 같이 #define을 이용하여 private를 public으로 치환한다.
이 방법이 위법인 이유는 2가지 이다.
- 예약어를 #define으로 변경하는 것은 위법이다.
- ODR(One Definition Rule)를 위반하였다. X를 private를 public으로 치환하여 재정의 하였기 때문이다.
  1. pocketpickerX.h
#define private public
#include "X.h"

void Hijack(X& x) {
   printf("pickpocket!!\n");
   x.private_ = 2;
}
  1. main.cpp
//#include "X.h"
//#include "forgerX.h"
#include "pickpocketX.h"
//#include "tricksterX.h"
#include <stdio.h>

int main() {
   X x;
   printf("before : %d\n", x.Value());

   Hijack(x);

   printf("after %d\n", x.Value());

   return 0;
}
  1. 실행결과
before : 1
forger!!
after 2

The Cheat(사기꾼)

사기꾼은 user가 기대하는 것 대신에 다른 것으로 바꿔치기 한다.
방법은 class X의 private를 접근 하기 위해 private_와 같은 메모리 위치에 public member가 위치한 class를 새로 정의 한 후 X를 새로 정의 한 class로 강제 형변환 하여 값을 변경한다.
아래 tricksterX.h를 보면
#ifndef TRICKSTERX_H_
#define TRICKSTERX_H_

#include "X.h"

class BaitAndSwitch {
public:
   int dummy;
   int dummy2;
   int notSoPrivate;
};

void Hijack(X& x) {
   printf("trickster!!\n");

   BaitAndSwitch& temp = reinterpret_cast<BaitAndSwitch&>(x);
   temp.notSoPrivate = 2;
}

#endif /* TRICKSTERX_H_ */
위와 같이 class BaitAndSwitch의 public member인 notSoPrivate가 x.private_와 같은 메모리 위치에 배치되도록 class를 구성한 다음 강제 형변환을 통하여 해당 변수에 접근 하는 것이다.
위 코드를 실제로 ubuntu 14.04 64bit, g++ 4.8.2 에서 실행시 아래와 같이 두 변수가 같은 메모리 위치를 가리킴을 확인 할 수 있었다.

이 방법은 컴파일러나 OS에 따라 배치가 바뀔 수 있으므로 이식성이 낮다.
  • Exceptional C++ Style 책에서는 예제에 baitAndSwitch의 public member로 notSoPrivate 하나만 정의 하였으나  ubuntu 14.04 64bit, g++ 4.8.2에서 실행하였을때는 다른 위치를 가리키고 있었다. 위의 int dummy는 메모리 위치를 맞추기 위해 추가한 member이다.
그리고 reinterpret_cast는 C의 typecase와 유사하다. 프로그래머가 책임을 질테니 되든 안되는 형 변환을 하라는 의미이므로 위의 편법이 의도한대로 사용할 수 있도록 허용한다.

The Language Lawyer (표준을 완전히 만족하면서 이식성도 보장)


#ifndef THELANGUAGELAWYERX_H_
#define THELANGUAGELAWYERX_H_

#include "X.h"

namespace {
struct Y {};
}

template<>
void X::f(const Y&) {
   private_ = 2;
}

void Hijack(X& x) {
   printf("TheLanguageLawyerX\n");
   x.f(Y());
}

#endif /* THELANGUAGELAWYERX_H_ */
위의 방식은 적법한 방법이다. class X에 member function f가 templete임을 이용한 것으로, 멤버 템플릿을 임의의 형식으로 특수화한 function f()를 통하여 private_의 값에 접근 한다.
여기서 namespace { struct Y {}; }를 사용한 이유는 ODR(One Definition Rule)을 위배하지 않도록 하기 위함이다.
  • 익명의 이름공간에 있는 struct Y를 사용함으로서 다른 누군가가 같은 형식으로 다른 동작을 하도록 다시 특수화 할수 없다.

Summary (나쁜맘 먹지 말자)

위의 마지막 방법이 templete의 특수화라는 적법한 방법을 통하여 class access
control mechanism을 bypass할수 있는 이식성 있는 방법을 제공한다는 점에서 암묵적으로 encapsulation을 깨뜨린다고 볼 수 있다. 하지만 실제 문제가 되지 않는다. 이유는 C++의 Access Modifier의 목적은 의도적인 악용을 막는 것이 아니라 우발적인 오용을 막는 것이다.
결론은 하지 말자. 이다. ㅋ

댓글

이 블로그의 인기 게시물

Raspberry pi 한글 설정 및 chromium 설치

Google Test를 이용한 Class의 Private/Protected Member Test