2015. 11. 12. 23:45

Policies/Binary Compatibility Issues With C++ (C++ 에서 이진 호환성 문제들) - 2

Policies/Binary Compatibility Issues With C++ - 2


Trouble shooting (문제 해결)

Adding new data members to classes without d-pointer

d-포인터가 없는 클래스들에 새 데이터 맴버를 추가하기

If you don't have free bitflags, reserved variables and no d-pointer either, but you absolutely have to add a new private member variable, there are still some possibilities left. If your class inherits QObject, you can for example place the additional data in a special child and find it by traversing over the list of children. You can access the list of children with QObject::children(). However, a fancier and usually faster approach is to use a hashtable to store a mapping between your object and the extra data. For this purpose, Qt provides a pointer-based dictionary called QHash (or QPtrDict in Qt3).

만약에 남은 비트플래그, 예약 변수 그리고 d-포인터 값이 값이 없지만, 새로운 프라이빗 맴버 변수를 추가해야 한다면, 아직 가능성이 남아 있습니다. 만약에 사용하시는 클래스가 QObject 를 상속 받는다면, 시범 삼아서 특별한 자식 (클래스)에 값을 추가하고, 그 자식들의 목록들을 순회하며 그 값을 찾을 수 있습니다. QObject::children() 을 사용하여 자식들의 목록에 접근할 수 있습니다. 하지만, 더 괜찮고 빠른 접근 방법으로는 당신의 객체와 추가적인 자료 사이를 연결하고 저장하는 해쉬 테이블을 사용하는 것입니다. 예를 들어, Qt 는 포인터-기반 사전 자료구조로 QHash 를 제공하고 있습니다. (또는 Qt3 에서는 QPtrDict)

The basic trick in your class implementation of class Foo is:
이 Foo 클래스에서 보여주는 기본 기교로

  • Create a private data class FooPrivate.
    FooPrivate 라는 프라이빗 데이터 클래스 생성
  • Create a static QHash<Foo *, FooPrivate *>.
    QHash<Foo *, FooPrivate *> 이라는 정적 자료 구조 생성
  • Note that some compilers/linkers (almost all, unfortunately) do not manage to create static objects in shared libraries. They simply forget to call the constructor. Therefore you should use the Q_GLOBAL_STATIC macro to create and access the object:
    몇몇 컴파일러/링커들은(불행히도 거의 모든) 공유되는 라이브러리에서 생성되는 정적 객체들을 관리하지 않음을 참고하시기 바랍니다. 그들은 단순히 생성자를 호출하는 것을 잊어버립ㄴ디ㅏ. 그러므로 객체를 접근하고 만드는데 Q_GLOBAL_STATIC 를 사용하셔야 합니다.

// BCI: Add a real d-pointer
Q_GLOBAL_STATIC(QHash<Foo *,FooPrivate *>, d_func);
static FooPrivate* d( const Foo* foo )
{
    FooPrivate* ret = d_func()->value( foo, 0 );
    if ( ! ret ) {
        ret = new FooPrivate;
        d_func()->insert( foo, ret );
    }
    return ret;
}
static void delete_d( const Foo* foo )
{
    FooPrivate* ret = d_func()->value( foo, 0 );
    delete ret;
    d_func()->remove( foo );
}
  • Now you can use the d-pointer in your class almost as simple as in the code before, just with a function call to d(this). For example:
    위에 적은 데로 간편하게 d(this) 를 호출하는 d-포인터를 사용할 수 있습니다. 예를 들어

d(this)->m1 = 5;
  • Add a line to your destructor:
    파괴자에는 다음의 줄을 추가하세요.

delete_d(this);
  • Do not forget to add a BCI remark, so that the hack can be removed in the next version of the library.
    다음 버전의 라이브러리에서 해당 (해킹)기술을 제거하기 위해서, BCI 표기를 추가하는 것을 잊지 말아주세요
  • Do not forget to add a d-pointer to your next class.
    다음 클래스에는 d-포인터를 추가하는 것을 잊지 말아주세요

Adding a reimplemented virtual function

다시 구현한 가상 함수를 추가하기
As already explained, you can safely reimplement a virtual function defined in one of the base classes only if it is safe that the programs linked with the prior version call the implementation in the base class rather than the derived one. This is because the compiler sometimes calls virtual functions directly if it can determine which one to call. For example, if you have

이전에 설명 드린바와 같이, 이전 버전의 상속 받은 구현 물(함수)보다 부모 클래스의 구현 물을 링크(연결) 하는 것이 안전 할 때에만 부모 클래스에서 정의된 가상 함수를 안전하게 재구현 할수 있습니다. 이는 컴파일러가 가끔 어떤 가상 함수를 호출할 수 있을지 결정할 수 있다면, 직접 부르기 때문입니다. 예를 들어 아래 코드를 봅니다.

void C::foo()
{
    B::foo();
}

then B::foo() is called directly. If class B inherits from class A which implements foo() and B itself doesn't reimplement it, then C::foo() will in fact call A::foo(). If a newer version of the library adds B::foo(), C::foo() will call it only after a recompilation.

B::foo() 를 직접 호출이 됩니다. 만약에 클래스 B 는 foo() 를 구현한 클래스 A 에게서 상속 받았고, B 자체는 재구현하지 않았다면, 사실 C::foo() 는 (B::foo() 가 아닌) A::foo() 를 호출합니다. 만약에 새 버전의 라이브러리가 B::foo() 를 추가한다면, C::foo() 는 재 컴파일 이후에만 (제대로) 불려질 것입니다.

Another more common example is:

또다른 다른 공통된 예로

B b;		// B derives from A
b.foo();

then the call to foo() will not use the virtual table. That means that if B::foo() didn't exist in the library but now does, code that was compiled with the earlier version will still call A::foo().

foo() 는 가상 테이브를 사용하지 않을 것입니다. 이 말은 B::foo() 는 라이브러리에 존재하지 않지만, 코드에서는 동작하며, 그 코드는 A::foo() 를 호출하는 이전 버전의 것으로 컴파일 되고 있습니다.

If you can't guarantee things will continue to work without a recompilation, move functionality from A::foo() to a new protected function A::foo2() and use this code:

재 컴파일하지 않고, 모든 것들이 정상적으로 동작할 것이라고 보장하지 못한다고 판단이 되면, 당신은 아래 코드처럼 새 프로텍티드 함수인 A::foo2() 를 A::foo() 에 추가하여 동작성을 변경하시기 바랍니다.

void A::foo()
{
    if( B* b = dynamic_cast< B* >( this ))
        b->B::foo(); // B:: is important
    else
        foo2();
}
void B::foo()
{
    // added functionality
    A::foo2(); // call base function with real functionality
}

All calls to A::foo() for objects of type B (or inherited) will result in calling B::foo(). The only case that will not work as expected are calls to A::foo() that explicitly specify A::foo(), but B::foo() calls A::foo2() instead and there should not be other places doing so.

B 의 형식 (또는 상속받은 B) 의 자료형의 객체에 대한 모든 A::foo() 호출은 B::foo() 를 호출하게 됩니다. 예상대로 동작되지 않는 경우는 A::foo() 를 호출하도록 명시적으로 정의된 A::foo() 를 호출할 때입니다. 하지만, 그 대신에 B::foo() 는 A::foo2() 를 호출하게 되는데, 해당 동작을 하는 함수는 다른 곳에 두어서는 안됩니다.

Using a new class

새 클래스를 사용하기

A relatively simple method of "extending" a class can be writing a replacement class that will include also the new functionality (and that may inherit from the old class to reuse the code). This of course requires adapting and recompiling applications using the library, so it is not possible this way to fix or extend functionality of classes that are used by applications compiled against an older version of the library. However, especially with small and/or performance-critical classes it may be simpler to write them without making sure they'll be simple to extend in the future and if the need arises later write a new replacement class that will provide new features or better performance.

클래스를 "확장하는" 상대적으로 쉬운 방법으로 새로운 기능을 (예전의 코드를 재사용하는 오래된 클래스에서 상속 받을 수 있는) 포함하는 대체 클래스를 작성할 수 있습니다. 이 방법은 물론 이 라이브러리를 사용하는 응용 프로그램을 변경하고 재컴파일이 요구되는데, 이는 이전 버전의 라이브러리에서는 발견된 문제 해결이나 기능의 확장된 기능을 누릴수 없기 때문입니다. 하지만, 규모가 작거나, 성능이-중요한 클래스들은 신기능이나 더 좋은 성능을 제공하는 새로운 대체 클래스를 작성해야 한다던지, 향후 쉽게 이 클래스를 확장해야 하는 것과 관련된 고민들을 하지 않고도 작성할 수 있습니다.

Adding new virtual functions to leaf classes

단말 클래스들에 새 가상 함수들을 추가하기

This technique is one of cases of using a new class that can help if there's a need to add new virtual functions to a class that should stay binary compatible and there is no class inheriting from it that should also stay binary compatible (i.e. all classes inheriting from it are in applications). In such case it's possible to add a new class inheriting from the original one that will add them. Applications using the new functionality will of course have to be modified to use the new class.

이 기술은 새 가상함수를 추가할 필요가 있으면서 이진 호환성을 유지해야하며, 이진 호환성을 계속 지키기 위해 이를 상속하는 클래스 또한 존재하지 않는 (다시 말하면, 이 클래스를 상속하는 모든 클래스들은 응용프로그램들 내에 있습니다.) 새 클래스를 만들 때, 사용될 수 있는 방법들 중에 하나입니다. 이런 경우, 그들(가상 함수들을) 추가되는 원 클래스를 상속받는 새로운 클래스를 만들은 것은 가능합니다. 새 기능을 사용하는 응용프로그램들은 물론, 새 클래스를 이용하기 위해 변경될 필요가 있습니다.

class A {
public:
    virtual void foo();
};
class B : public A { // newly added class
public:
    virtual void bar(); // newly added virtual function
};
void A::foo()
{
    // here it's needed to call a new virtual function
    if( B* this2 = dynamic_cast< B* >( this ))
        this2->bar();
}

It is not possible to use this technique when there are other inherited classes that should also stay binary compatible because they'd have to inherit from the new class.

새로운 클래스를 상속 받아야 한다는 이유로 다른 상속 받은 클래스들이 존재한다면, 이 기술은 사용할 수 없습니다.

Using signals instead of virtual functions

가상 함수들 대신 시그널을 사용하기

Qt's signals and slots are invoked using a special virtual method created by the Q_OBJECT macro and it exists in every class inherited from QObject. Therefore adding new signals and slots doesn't affect binary compatibility and the signals/slots mechanism can be used to emulate virtual functions.

Qt 의 시그널과 슬롯은 Q_OBJECT 매크로를 사용하여 만들어지는 가상 메소드를 사용하여 호출되고, QObject 를 상속 받는 모든 클래스에 존재합니다. 그러므로 새로운 시그널과 슬롯으로 이진 호환성에 영향을 주지 않으며, 시그널/슬롯 매커니즘은 가상 함수를 모방하는데 사용될 수 있습니다.

class A : public QObject {
Q_OBJECT
public:
    A();
    virtual void foo();
signals:
    void bar( int* ); // added new "virtual" function
protected slots:
    // implementation of the virtual function in A
    void barslot( int* );
};
 
A::A()
{
    connect(this, SIGNAL( bar(int*)), this, SLOT( barslot(int*)));
}
 
void A::foo()
{
    int ret;
    emit bar( &ret );
}
 
void A::barslot( int* ret )
{
    *ret = 10;
}

Function bar() will act like a virtual function, barslot() implements the actual functionality of it. Since signals have void return value, data must be returned using arguments. As there will be only one slot connected to the signal returning data from the slot this way will work without problems. Note that with Qt4 for this to work the connection type will have to be Qt::DirectConnection.

함수 bar() 는 가상 함수처럼 동작할 것이고, barslot() 은 실제 동작성을 나타내고 있습니다. 시그널은 void 형 리턴 값을 가지고 있기 떄문에, 데이터는 인수를 사용하여 반환 되어야만 합니다. 하나의 슬롯에서 값을 반환하는 시그널에 슬롯이 연결되어야만 문제가 없을 것입니다. Qt4 에서 이런 작업을 위해서는 Qt::DirectConnection 이라는 연결 형이 되어야 할 것입니다.

If an inherited class will want to re-implement the functionality of bar() it will have to provide its own slot:

만약 상속 받은 클래스가 bar() 의 동작성을 재구현하고 싶다면, 자신만의 슬롯을 제공해야할 것입니다.

class B : public A {
Q_OBJECT
public:
    B();
protected slots: // necessary to specify as a slot again
    void barslot( int* ); // reimplemented functionality of bar()
};
 
B::B()
{
    disconnect(this, SIGNAL(bar(int*)), this, SLOT(barslot(int*)));
    connect(this, SIGNAL(bar(int*)), this, SLOT(barslot(int*)));
}
 
void B::barslot( int* ret )
{
    *ret = 20;
}

Now B::barslot() will act like virtual reimplementation of A::bar(). Note that it is necessary to specify barslot() again as a slot in B and that in the constructor it is necessary to first disconnect and then connect again, that will disconnect A::barslot() and connect B::barslot() instead.

B::barslot() 는 A::bar() 의 가상 구현형태(함수) 로 동작하게 될 것입니다. A::barslot() 을 끊고 B::barslot() 을 연결하는 방식으로 처음 연결된 것을 끊고 다시 연결하기 위해 B 와 그 생성자 안에서 barslot() 을 다시 슬롯으로 정의하고 구성할 필요가 있습니다.

Note: the same can be accomplished by implementing a virtual slot.

가상 슬롯을 구현하는 방법으로도 동일한 결과를 낼 수 있습니다.

Content is available under Creative Commons License SA 3.0 as well as the GNU Free Documentation License 1.2.