dev.log2009. 2. 7. 18:33
일상적으로 통용되는 단위가 두가지 이상이라면 매우 헷갈린다. 미국은 미터법(metric system)과 영국단위(imperial system)의 차이 때문에 우주선[각주:1]을 하나 날려먹은 적도 있다. 그만큼은 심각한 것은 아니지만 회사에서 K군이 카메라 FOV를 조정할 때 라디안(radian)과 도(degree)의 차이 때문에 무척 헷갈려 하는 걸 본 적도 있다.

라디안은 우아하기 때문에 수학자나 공학자, 물리학자들이 좋아하고, 도는 역사가 오래기 때문에 세상 다른 모든 사람들이 선호하는 각도의 단위이다. 하지만 세상 모든 사람들이 우아하지 못한 단위를 사용한다고 해서 우리네 프로그래머까지 그런 단위를 써야만 한다는 것은 어불성설이라는 굳은 믿음을 갖고, 심심하던 차에 각도 클래스를 만들어 봤다.

기본 요구사항은 2가지.
  1. 어떤 각도를 나타내는 값이 도인지 라디안 인지가 항상 명확해야 한다.
  2. 도와 라디안의 변환은 인간이 신경을 쓰지 않도록 자동적으로 이루어져야 한다.
1번 요구사항은 그냥 angle = 3.5f; 라고 했을 때, 이 3.5가 도인지 라디안인지는 코드만 보고는 알 수 없기 때문에 혼동의 여지를 준다는 뜻이다. 이를테면, angle = radian(3.5f); 같은 식으로 이루어지면 항상 명확할 수 있다.
2번 요구사항은, 라디안을 받아들이는 함수에 실수로 도 단위로 90.f라는 값을 넣더라도, 1번에 의해 어떤 단위가 사용되고 있는지 항상 명시되므로, 자동으로 변환돼야 인간의 실수가 오류를 불러일으키지 않을 수 있다는 것.

const float pi = 3.14159265358979323846f;                           // 물론, float 타입은 이렇게 큰 정밀도를 저장하지 못한다.
const float pi_half = 3.14159265358979323846f*0.5f;

class radian;
class degree;

class radian
{
public:
    radian() {}
    explicit radian( float v ) : _value( v ) {}
    radian( const degree& r );

    operator float() const { return _value; }
    radian& operator=( float f ) { _value = f; return *this; }
    radian& operator=( const degree& d );

private:
    float _value;
};

class degree
{
public:
    degree() {}
    explicit degree( float v ) : _value( v ) {}
    degree( const radian& r );

    operator float() const { return _value; }
    degree& operator=( float f ) { _value = f; return *this; }
    degree& operator=( const radian& r );

private:
    float _value;
};


radian::radian( const degree& d )
{
    _value = static_cast<float>(d) * pi / 180.f;
}

radian& radian::operator=( const degree& d )
{
    _value = static_cast<float>(d) * pi / 180.f;
    return *this;
}

degree::degree( const radian& r )
{
    _value = static_cast<float>(r) * 180.f / pi;
}

degree& degree::operator=( const radian& r )
{
    _value = static_cast<float>(r) * 180.f / pi;
    return *this;
}

각 클래스의 float을 받아들이는 생성자가 explicit인 이유는, 함수 인자로 그냥 float 값을 넣을 때 어떤 단위를 의도하는지를 명시할 것을 강제하기 위해서이다. explicit이 아니라면 다음과 같은 일이 일어난다.

class radian
{
public:
    radian() {}
    radian( float v ) : _value( v ) {}
};

void rotate( radian r ); // radian을 받아들이는 함수
....
rotate( 90.f );              // 인간이 90도를 의도하며 함수를 호출하면 원치 않는 동작

하지만 float 생성자를 explicit으로 선언함으로써, 위와 같은 의도가 명확치 않은 코드는 아예 컴파일 단계에서 제거해 버릴 수가 있다. 컴파일 에러를 통해서;

class radian
{
public:
    radian() {}
    explicit radian( float v ) : _value( v ) {}
};

void rotate( radian r ); // radian을 받아들이는 함수
....
rotate( 90.f );              // ERROR!!!
rotate( radian(90.f) );   // 이렇게 하면 동작하지만 코드를 씀과 동시에 이상함을 발견하게 된다.
rotate( degree(90.f) );  // 이것이 원하는 동작.

그 밖의 경우에는 그냥 자연스럽게 동작한다.

radian a( pi/3.f );  // pi/3 을 지정
degree b = a;       // 도로 변환하면 60도
b = 90.f;               // 90도를 지정
a = b;                  // 라디안으로 변환하면 pi/2

이런 방법은 약간만 잘 이용하면 m/ft, ms/s, kg/pound 등등 다른 단위가 이용될 수 있는 모든 부분에서 인간의 헷갈림에 의한 실수를 원천봉쇄할 수 있지 않을까 싶다;

  1. Mars Climate Orbiter는 단위계의 차이로 파괴된 우주선이다. 추력을 설정할 때 지상의 운영 프로그램은 파운드 단위를, 기체의 제어 프로그램은 kg 단위를 기준으로 제작되어 발생한 사고. [본문으로]
Posted by uhm