3

私は昨日次の奇妙な行動に遭遇しました。それは私にはコンパイラのバグのように見えますか、それとも私が見逃したものがありますか?Facebook Connect for iPhoneのObjective-CクラスをObjective-CからC++へのアダプタークラスでラップして、独自のOpenGL /C++コードからより便利に使用できるようにしました。

次のコードは問題を明らかにします。以下の最初のバリアントでは、コンパイラはコンパイルしますが、vtableを台無しにするため、間違ったメソッドが呼び出されます。2番目のバリアントでは、gccが混乱していることを示すコンパイラエラーが発生します。

コメントは、状況をより詳細に説明しようとします。

#include <iostream>
#import <Foundation/Foundation.h>

// An abstract C++ interface
class Foo_cpp {
public:
    virtual void foo() = 0;
};

// Another abstract C++ interface
class Bar_cpp {
public:
    virtual void bar() = 0;
};


// An Objective-C to C++ adaptor. 
// It takes a C++ interface Foo. When it's do_foo method is called it
// delegates call to Foo::foo.
@interface Foo_objc : NSObject {
    Foo_cpp* foo_cpp_;
}
@end

@implementation Foo_objc

- (id)init:(Foo_cpp*)foo {
    self = [super init];
    if (self) {
        foo_cpp_ = foo;
    } 
    return self;
}

- (void) do_foo {
    std::cout << "do_foo: ";
    foo_cpp_->foo();
}
@end 

// Another Objective-C to C++ adaptor. 
@interface Bar_objc : NSObject{
    Bar_cpp* bar_cpp_;
}
@end 

@implementation Bar_objc

- (id)init:(Bar_cpp*)bar {
    self = [super init];
    if (self) {
        bar_cpp_ = bar;
    }
    return self;
}

- (void) do_bar {
    std::cout << "do_bar: ";
    bar_cpp_->bar();
}
@end 

// Main class implements both abstract C++ interfaces (which will
// confuse the compiler as we shall see). 
// It constructs two Objective-C to C++ adaptors as a members and
// tries to pass itself as a C++ delegate for these adaptors.
class Main : public Foo_cpp, public Bar_cpp {
public:
    Foo_objc* foo_;
    Bar_objc* bar_;

    Main() {
        // We try to construct two objective-c to c++ adaptors Foo_objc and
        // Bar_objc. 
        // 
        // We expect output of 
        // [foo_ do_foo];
        // [bar_ do_bar];
        // to be
        //   do_foo: foo
        //   do_bar: bar
#if 0
        // This variant compiles but the compiler messes up
        // the vtables. When do_bar() is called, we expect
        // bar() to be called via Bar_objc, but instead
        // foo() is called from both adaptors.
        // Output is
        //    do_foo: foo
        //    do_bar: foo  !!!! Calls wrong method !!!!
        foo_ = [[Foo_objc alloc] init:this];
        bar_ = [[Bar_objc alloc] init:this];

        [foo_ do_foo];
        [bar_ do_bar];
#else 
        // Now, this variant tries to help the compiler by passing 
        // |this| via a variable of the correct interface type.
        // It actually reveals the confusion that the compiler
        // is having. Seems like a bug in the compiler.
        Foo_cpp* iface = this;
        foo_ = [[Foo_objc alloc] init:iface];

        Bar_cpp* iface2 = this;
        // Error we get is on the next code line.
        //   $ g++ -x objective-c++ -lobjc mheritance_test.mm
        //   mheritance_test.mm: In constructor ‘Main::Main()’:
        //   mheritance_test.mm:107: error: cannot convert ‘Bar_cpp*’ to ‘Foo_cpp*’ in argument passing
        bar_ = [[Bar_objc alloc] init:iface2];

        [foo_ do_foo];
        [bar_ do_bar];
#endif

    }

    ~Main() {
        delete foo_;
        delete bar_;
    }

    virtual void foo() {
        std::cout << "foo" << std::endl;
    }

    virtual void bar() {
        std::cout << "bar" << std::endl;
    }

};

int main() {
    Main m;
}

この問題は、iPhoneSDKとMac独自のg++、およびバージョン4.0.1と4.2で発生します。私が間違って理解したことがありますか、それともこれはg ++のバグですか?

更新 私の例には、タイラーとマーティンヨークが指摘した偶発的なバグが含まれていましたが、ここでは問題ではありません。以下は更新された例です。

#include <iostream>
#import <Foundation/Foundation.h>

// An abstract C++ interface
class Foo_cpp {
public:
    virtual void foo() = 0;
};

// Another abstract C++ interface
class Bar_cpp {
public:
    virtual void bar() = 0;
};

// An Objective-C to C++ adaptor. 
// It takes a C++ interface Foo. When it's do_foo method is called it
// delegates call to Foo::foo.
@interface Foo_objc : NSObject {
    Foo_cpp* foo_cpp_;
}
@end

@implementation Foo_objc

- (id)init:(Foo_cpp*)foo {
    self = [super init];
    if (self) {
        foo_cpp_ = foo;
    } 
    return self;
}

- (void) do_foo {
    std::cout << "do_foo: ";
    foo_cpp_->foo();
}
@end 

// Another Objective-C to C++ adaptor. 
@interface Bar_objc : NSObject{
    Bar_cpp* bar_cpp_;
}
@end 

@implementation Bar_objc

- (id)init:(Bar_cpp*)bar {
    self = [super init];
    if (self) {
        bar_cpp_ = bar;
    }
    return self;
}

- (void) do_bar {
    std::cout << "do_bar: ";
    bar_cpp_->bar();
}
@end 

class Main : public Foo_cpp, public Bar_cpp {
    void foo() { 
        std::cout << "foo" << std::endl;
    }
    void bar() {
        std::cout << "bar" << std::endl;
    }
};

int main() {
    Main* m = new Main;    
#if 0 
    // Compiles but produces
    //   do_foo: foo
    //   do_bar: foo !!! incorrect method called !!!
    Foo_objc* fo = [[Foo_objc alloc] init:m];
    Bar_objc* bo = [[Bar_objc alloc] init:m];
#else 
    // Doesn't compile
    Foo_objc* fo = [[Foo_objc alloc] init:(Foo_cpp*)m];
    Bar_objc* bo = [[Bar_objc alloc] init:(Bar_cpp*)m];
    // A line above produces following error
    //    mheritance_test2.mm: In function ‘int main()’:
    //    mheritance_test2.mm:82: error: cannot convert ‘Bar_cpp*’ to ‘Foo_cpp*’ in argument passing
#endif
    [fo do_foo];
    [bo do_bar];
}

UPDATE 2 init:FooobjcとBarobjcのメソッドの名前がinitfoo:とinitbar:に変更された場合、正しく機能しますが、コードの問題点を説明することはできません。Objective-Cがメソッドシグネチャを作成する方法に関連している可能性がありますか?

4

4 に答える 4

6

ミッションを完了したので、答えのヒントを編集しています;-)

私はObjective-Cプログラマーではありませんが、好奇心に駆られて、何が起こっているのか疑問に思い、コードで少し遊んでいました。Foo*私が見つけたのは、Bar*パーツ以外のすべてをコメントアウトし、次の行をmain():に追加した後、問題が表面化することです。

Bar_objc *bo = [[Bar_objc alloc] init:(Bar_cpp*)0];

少し遊んだ後、私はそれがallocメッセージの明確に定義されていない結果と関係があるに違いないと思いました。これは、上記の割り当てを2つに分割することで修正されます。

Bar_objc *bo = [Bar_objc alloc]; [bo init:(Bar_cpp*)0];

それはうまくいきます。結果のキャストも同様allocです(以下のコードを参照)。あるいは、これは初期化子の異なる名前で修正することができます(私は信じています)。多分また再実装しallocます。わからない。

多重継承を伴う完全なコード(他にもいくつかの小さな変更があります-簡潔にするためにクラス/パブリックペアを構造体に変更し、コンストラクターでの仮想呼び出しを削除し、メッセージへのdelete呼び出しを変更しましたdealloc、おそらく他の何か):

#include <iostream>
#import <Foundation/Foundation.h>

struct Foo_cpp { virtual void foo() = 0; };
struct Bar_cpp { virtual void bar() = 0; };

@interface Foo_objc : NSObject {
    Foo_cpp* foo_cpp_;
}
- (id)init:(Foo_cpp*)foo;
- (void)do_foo;
@end
@implementation Foo_objc : NSObject {
    Foo_cpp* foo_cpp_;
}
- (id)init:(Foo_cpp*)foo {
    if( self = [super init] ) foo_cpp_ = foo;
    return self;
}
- (void) do_foo { std::cout << "do_foo: "; foo_cpp_->foo(); }
@end 

@interface Bar_objc : NSObject {
    Bar_cpp* bar_cpp_;
}
- (id)init:(Bar_cpp*)bar;
- (void)do_bar;
@end 
@implementation Bar_objc : NSObject {
    Bar_cpp* bar_cpp_;
}
- (id)init:(Bar_cpp*)bar {
    if( self = [super init] ) bar_cpp_ = bar;
    return self;
}
- (void) do_bar { std::cout << "do_bar: "; bar_cpp_->bar(); }
@end 


struct Main : public Foo_cpp, public Bar_cpp {
    Foo_objc* foo_;
    Bar_objc* bar_;

    Main() {
        foo_ = [(Foo_objc*)[Foo_objc alloc] init:this];
        bar_ = [(Bar_objc*)[Bar_objc alloc] init:this];
    }

    ~Main() { [foo_ dealloc]; [bar_ dealloc]; }

    virtual void foo() { std::cout << "foo" << std::endl; }
    virtual void bar() { std::cout << "bar" << std::endl; }
};

int main() {
    Main m;
    [m.foo_ do_foo];
    [m.bar_ do_bar];
}

結果:

do_foo: foo
do_bar: bar

結論:型付けがやや弱く、型に関係なくオブジェクトにメッセージを送信する可能性があるため、同じ名前でパラメーターが異なるメッセージを使用しない方がよいと思います。

于 2009-09-22T07:51:09.020 に答える
3

コンストラクター内から仮想メソッドを呼び出すことは未定義の動作です。

Main()からfoo()とbar()の両方を呼び出しているので、明確に定義された出力があるとは思いません。ただし、オブジェクトが構築された後に呼び出しを実行すると、機能するはずです。試す:

int main()
{
    Main m;
    [m.foo_ do_foo];
    [m.bar_ do_bar];
}
于 2009-09-22T07:27:25.280 に答える
3

Apple Developerフォーラムからのコメントで、問題が説明されました。

問題は、-init:を呼び出す複数のメソッドがあり、それらが異なるパラメータータイプを受け取ることです。+ allocは(id)を返すため、コンパイラはどの-init:メソッドを呼び出すかを推測する必要があります。この場合、それは間違った推測をし、間違ったポインタを多重継承オブジェクトに渡します。

-Wstrict-selector-matchはコンパイラの警告を強制しますが、コンパイラはそのオプションがなくてもあいまいさについて警告する必要があります。バグレポートを提出する必要があります。

1つの解決策は、混乱を避けるために、メソッドの名前を-initWithFoo:および-initWithBar:に変更することです。別の解決策は、-init:を呼び出す前に+allocの結果を型キャストすることです。

@hackerの答えは正しかったので、正解としてマークします。-Wstrict-selector-matchについて知っておくと、追加情報として役立ちます。

于 2009-09-22T19:25:35.740 に答える
2

これは、コンストラクターの実行が完了する前に、オブジェクトの仮想メソッドを呼び出そうとしているためです。

私はあなたの最初の方法を使ってテストをしました

[foo_ do_foo];
[bar_ do_bar];

コンストラクターの外のメソッド、そしてそれは私のために働いた。

追加 これは基本的にScottMeyersのEffectiveC++の項目9 です。構築中または破棄中に仮想関数を呼び出さないでください。

Meysterは、おそらくデストラクタが非仮想であることに適合します(項目7)。

通常、人々がマイヤーズを私に引用するとイライラしますが、この場合、それがほとんど役立つことを願っています!

于 2009-09-22T07:26:12.927 に答える