C ++ множественное наследование и Objective-C. Это ошибка в GCC? - PullRequest
3 голосов
/ 22 сентября 2009

Я столкнулся со следующим странным поведением вчера. Мне кажется, ошибка компилятора или я что-то пропустил? Я обернул Facebook Connect для классов Objective-C в iPhone с классами адаптера Objective-C к C ++, чтобы их было удобнее использовать из нашего собственного кода OpenGL / C ++.

Следующий код выявляет проблему. В первом варианте, представленном ниже, компилятор компилирует, но портит vtables, и поэтому вызывается неправильный метод. Во втором варианте мы получаем ошибку компилятора, которая указывает, что 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;
}

Проблема возникает с iPhone SDK и Mac g ++, а также с версиями 4.0.1 и 4.2. Я что-то неправильно понял или это ошибка в g ++?

UPDATE Мой пример содержал случайную ошибку, на которую указывали Тайлер и Мартин Йорк, но здесь проблема не в этом. Ниже приведен обновленный пример.

#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];
}

ОБНОВЛЕНИЕ 2 Если init: методы Fooobjc и Barobjc переименованы в initfoo: и initbar: тогда он работает правильно, но я все еще не могу объяснить, в чем проблема с кодом. Может ли это быть связано с тем, как Objective-C создает сигнатуры методов?

Ответы [ 4 ]

6 голосов
/ 22 сентября 2009

Я редактирую свои подсказки при ответе, так как я выполнил миссию; -)

Я не программист на Objective-C, но из-за любопытства я не мог не задаться вопросом, что происходит, и немного поиграл с кодом. Что я обнаружил, что проблема появляется после того, как закомментировал все, кроме Foo* и Bar* частей и добавив следующую строку в main():

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

Немного поиграв, я подумал, что это как-то связано с не совсем определенным результатом сообщения alloc. Что фиксируется путем разделения вышеупомянутого назначения на два:

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

Суть: я понимаю это из-за слабой типизации и возможности посылать сообщения объектам независимо от типов, лучше не иметь сообщений с одинаковыми именами, но разными параметрами.

3 голосов
/ 22 сентября 2009

Комментарий с форумов разработчиков Apple объяснил проблему:

Проблема в том, что у вас есть несколько вызов методов -init: что взять разные типы параметров. + Alloc возвращает (id), поэтому компилятор должен угадайте, какой -init: метод для вызова. В в этом случае он угадывает и проходит неправильный указатель на множественно наследуемый объект.

-Wstrict-selector-match заставит предупреждение компилятора, но компилятор должен был предупредить о двусмысленности даже без этой опции. Вам следует подать отчет об ошибке.

Одно из решений переименовать ваши методы -initWithFoo: и -initWithBar: чтобы избежать спутанность сознания. Другое решение состоит в том, чтобы приведите тип результата + alloc перед вызывающий -init:.

Я отмечу ответ @ хакера как правильный, поскольку он был правильным. Знание о -Wstrict-selector-match - хорошая дополнительная информация.

3 голосов
/ 22 сентября 2009

Это неопределенное поведение для вызова виртуальных методов внутри конструктора.

Поскольку вы вызываете как foo (), так и bar () из Main (), я не ожидаю, что вы получите четко определенный вывод. Но выполнение вызовов после того, как объект был построен, должно работать. Попробуйте:

int main()
{
    Main m;
    [m.foo_ do_foo];
    [m.bar_ do_bar];
}
2 голосов
/ 22 сентября 2009

Это потому, что вы пытаетесь вызвать виртуальные методы объекта до того, как конструктор завершит работу.

Я сделал тест, используя ваш первый метод, просто переместив

[foo_ do_foo];
[bar_ do_bar];

методов вне конструктора, и у меня это сработало.

Добавлено Это, в основном, пункт 9 из «Эффективного C ++» Скотта Мейерса: Никогда не вызывайте виртуальные функции во время строительства или разрушения.

Мейстер, вероятно, подумал бы, что ваши деструкторы тоже не виртуальны (пункт 7).

Обычно меня раздражает, когда люди цитируют Мейерса, но в этом случае я надеюсь, что это в основном полезно!

...