개요

Qt GUI 개발을 하면서 지속적으로 등장하게 될 시그널과 슬롯의 정의를 다시 한번 명확하게 짚고 넘어감으로써 앞으로 능숙하게 써먹을 준비 갖추기

시그널은 from, 슬롯은 to의 성격을 가지는 개념.

시그널

  • 특정 이벤트 발생시에 내보내어짐 (emit을 이용하여)

시그널 자체는 public으로 접근 가능한 함수이며 따라서 어디서든 접근이 가능하지만, 이 시그널을 정의한 클래스 및 서브클래스에서만 호출할 것을 권장함.

또한 시그널은 별도의 구현 절차가 없다. 무슨 말이냐면, emit sigNoname(param); 식으로 시그널을 발생시키기만 하면, 그 다음에 할 일은 슬롯으로 넘겨서 슬롯 내부에서 로직을 구현하면 되기 때문이다.

다음은 심플한 QObject 기반 클래스 예제다.

#include <QObject>

class Counter : public QObject
{
    Q_OBJECT

public:
    Counter() { m_value = 0; }

    int value() const { return m_value; }

public slots:
    void setValue(int value);

signals:
    void valueChanged(int newValue);

private:
    int m_value;
};

슬롯

특정 이벤트 발생 후 시그널이 emit 되면, 연결된 슬롯에서 이어서 사전에 정의된 작업을 수행한다.

하나의 시그널에는 다수의 슬롯이 연결될 수 있다.

위에서 언급했듯, 시그널과 달리 슬롯은 시그널과 함께 받은 인자를 가지고 무슨 작업을 해줄지 정의해줘야 한다. 위의 Counter 클래스 예제를 통해 정의된 setValue 슬롯을 내부적으로 다음과 같이 정의할 수 있다.

void Counter::setValue(int value)
{
    if (value != m_value) {
        m_value = value;
        emit valueChanged(value);
    }
}

위의 예제에서는 갱신된 값을 인자로 하여 슬롯 내부에서 다시 한번 시그널을 내보내도록 정의하고 있다.

시그널과 슬롯 연결해 주기

그렇다면 이 둘을 어떻게 연결을 해줘야 실질적으로 우리가 기대하는 동작을 할 수 있나?

QObject에서는 connect 함수를 이용해서 둘을 연결하도록 지원하고 있다. Qt 6버전 기준으로 connect 함수는 다음과 같은 6종류가 지원되고 있다. 보다 자세한 내용은 여기 참조.

  • QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection)
  • QMetaObject::Connection QObject::connect(const QObject *sender, const QMetaMethod &signal, const QObject *receiver, const QMetaMethod &method, Qt::ConnectionType type = Qt::AutoConnection)
  • QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal, const char *method, Qt::ConnectionType type = Qt::AutoConnection) const
  • template <typename PointerToMemberFunction> QMetaObject::Connection QObject::connect(const QObject *sender, PointerToMemberFunction signal, const QObject *receiver, PointerToMemberFunction method, Qt::ConnectionType type = Qt::AutoConnection)
  • template <typename PointerToMemberFunction, typename Functor> QMetaObject::Connection QObject::connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
  • template <typename PointerToMemberFunction, typename Functor> QMetaObject::Connection QObject::connect(const QObject *sender, PointerToMemberFunction signal, const QObject *context, Functor functor, Qt::ConnectionType type = Qt::AutoConnection)

현 단계에서는 이 중에서 맨 위의 함수만 확인하고 넘어간다. 왜냐면 현재 프로젝트에서 connect는 이 방식의 호출로 구현이 되고 있었기 때문이다.

파라미터 순서대로 들여다 보면

1. 어떤 객체에서
2. 어떤 시그널을 호출 했을 때
3. 어떤 객체의
4. 어떤 슬롯에서 이를 받는다
5. 그리고 커넥션 타입은 ㅇㅇㅇ와 같다

와 같이 정의되어 있다. 여기서 중요한 점은, 시그널 및 슬롯을 파라미터로 전달할 때 각각 SIGNALSLOG 매크로를 사용해서 던져야 한다는 점이다.

커넥션 타입에는 여섯 종류가 제공되고 있는데, 이는 시그널과 슬롯이 연결된 방식을 정의한다. 다시 말하면 시그널 발생 시 이를 슬롯에 즉시 던지는지, 혹은 기다렸다가 던지는지 등을 정리하고 있다. 현재 크립토 프로젝트 내에서 쓰이고 있는 Qt::QueuedConnection은 그중 하나인데, 이 타입에 대한 설명은 다음과 같다.

The slot is invoked when control returns to the event loop of the receiver's thread. The slot is executed in the receiver's thread.

정확한 내용 이해가 되지 않아서 내용 해석이 이상할 수 있음. 얼른 이해하고 가다듬기

이 설명에 의하면,

  • 이 연결방식을 채택한 경우, 슬롯은 시그널 호출을 받는 스레드 (receiver thread)의 이벤트 루프로 컨트롤이 넘겨질 때 호출이 되며
  • 슬롯은 리시버 스레드 내에서 실행된다.

현재 크립토 코드 내에서 다음과 같이 호출하고 있음을 확인할 수 있다.

QObject::connect(this, SIGNAL(sigLog1(QString)), this, SLOT(slotLog1(QString)), Qt::QueuedConnection);

그러면 위에 있는 Counter의 시그널과 슬롯은 다음과 같이 연결해 주면 될 것이다. Counter 객체 내에서 이를 호출한다고 가정한다.

QObject::connect(this, SIGNAL(valueChanged(int)), this, SLOT(setValue(int)), Qt::QueuedConnection);
반응형

다음과 같은 함수가 선언되어 있다.

이 함수 내부의 로직을 보면, 문제점이 하나 보인다.

if (x + y > INT32_MAX) 부분을 보면, 이 부분이 과연 정상적으로 실행될 수 있는 부분인지 먼저 확인해볼 필요가 있다.

예를 들어서 x의 값을 2000000000, y의 값도 마찬가지로 2000000000이라고 해보자. 이 둘을 합하면 어떤 결과가 나올지 다음 코드를 통해 확인해 보자.

#include <iostream>
#include <cstdint>

using namespace std;

int main() {
    int32_t x = 2000000000;
    int32_t y = 2000000000;

    cout << x + y << endl;
    return 0;
}

실행 결과:
-294967296

20억과 20억을 더했는데 전혀 엉뚱한 결과가 나왔다. 왜일까?

int32_t 데이터 타입에서 제공하는 값의 범위는 -2,147,483,648 ~ 2,147,483,647 이다. 여기서 최대값 + 1을 하게 되면 int32_t 타입의 변수 입장에서는 담을 수 있는 범위의 최대치를 넘겨버리게 된다. 이를 정수 오버플로우 (integer overflow)라고 한다.

'끝과 끝은 맞닿아 있다'라는 말처럼, 데이터 타입의 최소값과 최대값은 서로 맞닿아 있다. 그렇기 때문에 오버플로우가 발생한 경우 컴파일러는 증가분에 대한 값을 INT32_MAX의 다음 값인 최소 값으로 넘긴다. 따라서 -2,147,483,648을 리턴하게 된다.

만일 최대값 + 2를 하게 되면 위의 논리에 따라 -2,147,483,647을 리턴하게 된다.

이러한 문제는 역으로 최소값에 1을 빼려고 시도하는 경우에도 마찬가지로 나타난다.

#include <iostream>
#include <cstdint>

using namespace std;

int main() {
    int32_t x = INT32_MIN;
    int32_t y = -1;

    cout << x + y << endl;
    return 0;
}

위의 코드에서 출력되는 x + y의 값은 2,147,483,647가 된다. 최소값인 -2,147,483,648에서 1을 더 빼서 맞닿아 있는 최대값을 리턴하기 때문이다.

주의

이번 문제에 대해 찾아보던 중, 용어를 헷갈려 해서 다음과 같이 정리해 보고자 한다. 난 최대값의 범위를 넘어가서 계산하는 것을 오버플로우, 최소값보다 작은 값을 계산하는 것을 언더플로우라고 생각하고 있었는데 그동안에 용어에 대한 개념을 완전 잘못 알고 있었던 것이다. 두 용어를 다시 한번 더 정리하자면 다음과 같다.

  • 오버플로우: 특정 데이터 타입의 값 범위 밖의 연산이 일어나는 경우 (그게 최소값이던 최대값이던간에 관계없음)
  • 언더플로우: 나무위키에서 정리한 내용이 있어서 다음과 같이 인용해 본다 (https://namu.wiki/w/%EC%96%B8%EB%8D%94%ED%94%8C%EB%A1%9C)
    부동소수점 언더플로(floating point underflow) 또는 산술 언더플로(arithmetic underflow)는 컴퓨터에서 부동소수점으로 표현된 수에서, 지수(E)가 최솟값보다 낮아지는 상황을 뜻한다. 예를 들어 IEEE 754의 단정밀도 실수(single-precision)에서 표현가능한 지수의 값은 −126 ~ +127 이다. 그런데, 2-100 * 2-100 을 계산하면 2-200 이 되어야 하는데, 단정밀도에서는 지수의 값을 -126까지만 처리할 수 있으므로 범위를 벗어나는 값이 된다.

즉 언더플로우는 부동소수점의 정밀도와 관련된 개념으로, 표현이 가능한 지수 범위를 초과한 경우를 일컫는 말이다.

그렇다면 위의 함수를 개선하여 올바른 코드를 짜는 방법은?

그건 다음 시간에 이어서 올리겠다.

반응형

+ Recent posts