다시 전편의 소스를 살펴보시면, 두개의 해더파일에 모두 Q_OBJECT 라는 키워드가 들어가있는 것이 보일 것입니다.
이 키워드야말로 큐티의 메타오브젝트시스템의 핵심이라고 할수 있습니다.
메타 오브젝트 시스템의 기본은 QObject클래스와 Q_OBJECT 키워드, 그리고 moc(메타 오브젝트 컴파일러)라는 큐티전용 전처리기입니다.
메타 오브젝트 시스템이란, 이름대로만 설명하자면 객체스스로 객체에 대한 정보를 담고 있는 것을 말합니다.
일반적인 C++클래스는 그 클래스 자체에 대한 설명은 담고 있지 않습니다.
예를 들어 A라는 클래스의 인스턴스 a를 생성했을때, 따로 클래스의 선언이나 정의를 한 소스를 보거나, 클래스를 설명한 문서를 보는 등의 일을 하지 않으면, 이 인스턴스 a로부터 a의 클래스 '이름'은 무엇인가, 또 다른 클래스 B와의 상속관계는 어떠한가와 같은 정보는 알수 없습니다.
이에반해 큐티의 메타오브젝트 시스템이 적용된 클래스는, 인스턴스만 가지고도 그 인스턴스의 형에 대한 정보를 알 수 있습니다.
사실 이것과 메타 오브젝트 시스템 전반에 관련되어서 어떤 연결점이 있는진 저도 잘 모르므로, 너무 구체적인 설명은 피하도록하고, 메타 오브젝트 시스템덕분에 어떤게 가능하고, 어떻게 이용하지는에 대해서 적도록 하겠습니다.
일단 전편에서 설명한 시그널/슬롯 또한 이 메타 오브젝트 시스템덕분에 가능합니다.
또, qobject_cast라고 하는 형변환 연산자가 제공됩니다.
이 형변환 연산자는 QObject를 상속받은 클래스간에서 형변환을 해주는데 쓸수 있습니다.
원래 C++에는 dynamic_cast라는 상속관계에 있는 클래스간에 부모클래스에서 자식클래스로, 혹은 자식클래에서 부모클래스로 형변환해주는 캐스팅연산자가 있습니다.
하지만, 이 dynamic_cast는 RTTI에 의해서 캐스팅되기 때문에 이식성이 없는 코드가 되기 쉽고, 무엇보다 성능면에서 심각하게 느립니다.
그래서 보통 이건 100% 상속관계에 있다고 확신할땐 그냥 static_cast를 씁니다.
하지만, 형변환을 해야겠는데, 상속관계에 있을수도 있고 아닐수도 있다거나, 서로다른 동적 라이브러리간에서 형변환을 해야하는 경우에 유용한 것이 qobject_cast입니다.
이 연산자(내장된게 아니므로 정확히는 연산자가 아니라 템플릿함수입니다만 앞으로 그냥 편의상 형변환 연산자와 똑같이 취급하겠습니다)는 QObject를 상속한 두 클래스간에 형변환을 해주고, 만약 형변환이 불가능한 경우는 널 포인터를 반환해줍니다.
예를 들어 QLabel은 QWidget을 상속했으므로
QLabel *label = new QLabel;
QWidget *widget = qobject_cast<QWidget*>(label);
과 같은 형변환에서는 제대로 형변환이 이루어질테고, QLabel과 QPushButton은 상속관게에 없으므로
QPushButton *button = qobject_cast<QPushButton*>(label);
과 같은 변환에서는 button은 널 포인터가 됩니다.
그럼 qobject_cast<QPushButton*>(widget)의 결과는 어떨까요?
얼핏 QPushButton은 QWidget을 상속받으니까 widget을 QPushButton*으로 형변환 가능할 듯하지만, 이게 기똥차게도 널을 반환해줍니다.
실제로 widget은 QLabel의 인스턴스니까 여기서 만약 널이 반환되지 않으면 라벨을 토글하는 등의 어처구니 없는 일이 발생하겠죠?
특히 유용하게 쓰이는 경우는 예를 들어 어떤 함수의 인자로 QWidget*을 받아오는데, QPushButton*인 경우만 특별한 처리를 하고 싶거나 하는 경우에, 간단하게 qobject_cast로 캐스팅해보고 널인지 아닌지 검사해서 QPushButton의 포인터만 골라내는 것이 가능합니다.
또한, tr/trUtf8을 이용한 번역기능도 역시 메타 오브젝트 시스템의 덕분입니다(사실 이부분은 저도 실제로 번역해본 적이 없기 때문에 자세한 설명은 못하겠습니다. 그냥 습관적으로 일단 외부로 보여지는 문자열은 전부 tr로 싸고 있습니다.)
Q_PROPERTY키워드를 이용한 프로퍼티도 지원됩니다만, 프로퍼티는 제가 써본적이 없네요-_-;
위에서 설명한 기능들을 이용하기위해서는 메타오브젝트 시스템이 필요하고, 여기서 Q_OBJECT의 역할이 나옵니다.
Q_OBJECT는 매크로인데, 메타오브젝트를 위한 각종 함수들을 자동으로 선언해줍니다.
그리고 moc는 컴파일하기전에 Q_OBJECT가 포함된 클래스 선언문을 찾아서 적절한 moc파일을 생성해줍니다.
moc는 qmake로 생성한 Makefile에서 알아서 돌려주므로, 우리가 신경써야 할것은 첫째, QObject를 상속받을 것(당연히 직접 상속받지 않고 QObject를 상속받은 클래스를 상속받아도 상관없습니다), 그리고 클래스 선언내에 private영역에 Q_OBJECT라는 키워드를 적어둘 것, 이 두가지입니다.
connect함수의 원형을 보면 알겠지만, const QObject *로 넘기도록 되있으므로 일단 QObject를 상속받지 않으면 connect함수 자체를 이용 할 수 없습니다.
하지만 저도 종종 Q_OBJECT선언하는 것을 까먹곤 합니다.
이런 경우 컴파일은 잘되도 실행했을때 해당하는 슬롯이나 시그널이 없다는 문구가 콘솔창에 나타나므로, 분명히 제대로 했는데도 그럴땐 혹시 Q_OBJECT를 빼먹은 것 아닌가 확인하시구요, Q_OBJECT를 추가했을땐 꼭 그파일도 moc가 돌아가도록 qmake를 한번 더 실행해줘야합니다.
또 한가지 주의할점이 있는데요, Q_OBJECT는 반드시 해더파일에 있어야 합니다.
확실친 않지만 아마도 moc가 해더파일만 도는건지, cpp파일에 Q_OBJECT를 포함한 채로 클래스를 선언하면 링크단계에서 어떤 vtable이 존재하지 않는다는 에러가 나옵니다.
이상 메타오브젝트 시스템에 대한 설명이었습니다.
마지막으로, 지난 예제 clicks의 MainWidget의 생성자에 대해서 좀 설명해볼려고 합니다.
생성자부분만 다시 가지고 오면,
[code]
MainWidget::MainWidget(QWidget *parent)
: QWidget(parent) {
button = new DoubleButton(this);
label = new QLabel(trUtf8("클릭 안됨"), this);
connect(button, SIGNAL(doubleClicked()), this, SLOT(setLabelDoubleClicked()));
label->move(100, 0);
resize(200, 50);
}
[/code]
여기서 보통 C++을 쓰는 사람이라면 new연산자를 이용하는 부분이 잘 이해가 안될수 있습니다.
딱히 new를 이용해서 동적할당할 필요성도 못느끼는데다가, 아무리 눈씻고 찾아봐도 여기서 할당한 메모리를 delete하는 부분이 없으니까요.
물론 '간단한 프로그램이고 한번 할당한담에 종료할때 알아서 운영체제가 해제할테니 별로 신경안써도 되'라고 생각 할수도 있지만, 사실 프로그램이 종료될때 button과 label은 잘 delete된후에 종료됩니다.
그 비밀은 바로 QObject가 가지고 있습니다(그래서 딱히 메타 오브젝트 시스템을 이용한게 아님에도 밀접한 연관이 있는 클래스이므로 이 글에 같이 적습니다)
전에 위젯 클래스를 소개할때 생성자로 넘기는 '부모 위젯'에 대해 이야기한적이 있습니다.
사실 그때 설명하지 않은 것이 있는데, 부모는 위젯에만 있는게 아닙니다.
QObject를 상속받는 모든 클래스는 '부모'를 지정할 수 있습니다.
QObject의 생성자를 보아도 QObject(QObject *parent = 0) 처럼 QWidget과 동일한 모습을 가지고 있습니다.
이렇게 어떤 인스턴스가 다른 인스턴스의 '자식'이 되면, 부모 인스턴스가 소멸될때 자식으로 등록된 인스턴스를 알아서 지워줍니다.
다시 생성자를 보면, label도, button도 모두 생성자의 마지막에 this(MainWidget 인스턴스의 포인터)를 넘기고 있는 것을 알 수 있습니다.
MainWidget은 main함수의 지역변수로 선언되어있으므로, 프로그램이 종료될때 저절로 소멸될 것입니다.
그리고 이때, MainWidget의 자식으로 등록되어있는 label과 button도 알아서 delete 시켜주는 것이죠.
물론 동적 생성을 하지 않고 처음부터 그냥 인스턴스로 생성해도 상관없습니다.
이경우는 delete하지 않아도 자동으로 소멸될것이고, 또한 MainWidget의 소멸자에서 미리 delete하도록 해도 문제없습니다.
어떤 원리인지는 저도 잘 모르지만 알아서 delete가 필요하면 지우고 필요없으면 안하는 구조로 되어있으니까요.
이덕분에 Qt로 프로그래밍할때는 자바의 가비지 컬렉션처럼 메모리 관리가 자동으로 되면서도 가비지 컬렉션처럼 느리지도 않다는 잇점이 있습니다.
다만 주의할 점은 이것은 QObject를 상속받은 경우에만 해당하는 것입니다(일부 QObject를 상속받지 않고 구현된 큐티의 클래스들도 있는데, 이 클래스들에는 해당사항없습니다).
그래도 아직 따질게 있으신 분이 계실지도 모르겠습니다.
동적할당하는 메모리가 자동으로 관리된다고 해도, 이경우는 동적할당행위 그자체가 필요 없는데 굳이 동적할당을 하는 이유가 뭐냐? 고 물으실수 있는데요, 그 이유는 컴파일 의존성을 줄이기위해서입니다.
컴파일 의존성이 뭔데?
포인터로 선언하고 해더파일을 인클루드하는 대신에 전방선언을 해서 컴파일 의존성을 줄일수 있는 것이지요.
그래서 대부분의 QObject를 상속받는 클래스의 인스턴스는 동적할당으로 생성하게 됩니다.
이제 생성자에서 느껴진 위화감은 해소되었을꺼 같은데, 마지막 두줄이 신경쓰입니다.
label->move(100, 0);
resize(200, 50);
이 두줄인데요, 이 두줄을 적은 이유는, 그냥 생성만 해놓으면 button과 label이 겹쳐서 보이지 않기 때문에 적당히 label을 오른쪽으로 움직여준것이고, 또 움직인 영역이 위젯의 크기보다 크면 짤리거나 안보이기 때문에 위젯자체도 적당한 크기로 리사이즈 해준 것입니다.
그런데, 이게참 번거롭기 짝이없을 뿐만 아니라 모양새도 참 구리구리합니다.
그럼 매번 모든 위젯의 위치를 계산하고 움직여준다음에 크기를 변경해줘야하는 수고가 필요한걸까요?
절~대 아닙니다. 우리의 큐티는 이미 이 문제를 쉽게 해결할수 있는 레이아웃 시스템을 가지고 있습니다.
다음회에는 레이아웃에 대해서 소개해보겠습니다.
Posted by xylosper


