Почему я получаю ошибки компоновщика при попытке построить код C ++ с использованием чисто виртуальных функций в шаблоне? - PullRequest
1 голос
/ 18 ноября 2010

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

template <typename T> class Foo
{
public:
    Foo(T a)
    {
        x = a;
        echo();
    }

protected:
    T x;
    virtual void echo() = 0;
};

class Bar : public Foo<int>
{
public:
    Bar(int a) : Foo<int>(a)
    {
    }

    void echo();
};

void Bar::echo()
{
    cout << "value: " << x << endl;
}

int main(int argc, char* argv[])
{
    Bar bar(100);
    return 0;
}

Ошибка компоновщика выглядит следующим образом в MSVC:

purevirttest.obj: ошибка LNK2019: неразрешенный внешний символ "защищен: виртуальный пустота Foo (int) "(?? 0? $ Foo @ H @@ QAE @ H @ Z) * ​​1007 *

Если я перенесу вызов echo () из конструктора Foo, код соберется и прекрасно выполнится, я могу без проблем вызвать bar.echo (). Проблема в том, что мне действительно нравится эта функция в конструкторе. Любое объяснение этой тайны высоко ценится.

Ответы [ 2 ]

4 голосов
/ 18 ноября 2010

Джеймс МакНеллис ответил, что «Вы не можете вызвать echo() из конструктора Foo<T>» почти правильно.

Вы не можете вызвать его практически изFoo<T> конструктор, потому что, хотя тело конструктора Foo<T> выполняет, объект имеет тип Foo<T>.Там еще нет производной части класса.И виртуальный вызов echo(), как в вашем коде, затем переходит к чисто виртуальной функции: bang, dead.

Однако вы можете предоставить реализацию чисто виртуальной функции, напримерecho(), а затем назовите его не виртуально, как Foo::echo(), из конструктора Foo.:-) За исключением того, что это вызывает реализацию Foo.Хотя кажется, что вы хотели бы вызвать реализацию производного класса.

Теперь по поводу вашей проблемы:

"Я бы очень хотел, чтобы эта функция в конструкторе."

Итак, когда я пишу этот код, ваш (недействительный) код выглядит следующим образом:

template <typename T> class Foo
{
public:
    Foo(T a)
    {
        x = a;
        echo();
    }

protected:
    T x;
    virtual void echo() = 0;
};

class Bar : public Foo<int>
{
public:
    Bar(int a) : Foo<int>(a)
    {
    }

    void echo();
};

void Bar::echo()
{
    cout << "value: " << x << endl;
}

int main(int argc, char* argv[])
{
    Bar bar(100);
    return 0;
}

И, насколько я понимаю описание вашей проблемы, вам нужен конструктор Fooдля вызова echo реализации любого класса, который наследуется от Foo.

Существует несколько способов сделать это;все они направлены на то, чтобы донести знания о реализации производного класса до базового класса.

Один из них известен как CRTP , шаблон любопытно повторяющегося шаблона и адаптировандля вашей конкретной проблемы это может выглядеть так:

#include <iostream>

template< class XType, class Derived >
class Foo
{
public:
    Foo( XType const& a )
        : state_( a )
    {
        Derived::echo( state_ );
    }

protected:
    struct State
    {
        XType   x_;
        State( XType const& x ): x_( x ) {}
    };

private:
    State   state_;
};

class Bar
    : public Foo< int, Bar >
{
private:
    typedef Foo< int, Bar >     Base;
public:
    Bar( int a ): Base( a ) {}
    static void echo( Base::State const& );
};

void Bar::echo( Base::State const& fooState )
{
    using namespace std;
    cout << "value: " << fooState.x_ << endl;
}

int main()
{
    Bar bar(100);
}

Выше приведено решение, которое не является плохим, но и не является хорошим.Если ваша настоящая проблема заключается в вызове нестатической функции-члена производного класса из конструктора базового класса, то единственный «хороший» ответ - это Java или C #, которые позволяют вам делать такие вещи.Он преднамеренно не поддерживается в C ++, потому что очень легко непреднамеренно попытаться получить доступ к еще не инициализированному материалу в объекте производного класса.

В любом случае, как почти всегда, где есть решение чего-то во время компиляции,есть также решение во время выполнения.

Вы можете просто передать функцию для выполнения в качестве аргумента конструктора, например, так:

#include <iostream>

template< class XType >
class Foo
{
protected:
    struct State
    {
        XType   x_;
        State( XType const& x ): x_( x ) {}
    };

public:
    Foo( XType const& a, void (*echo)( State const& ) )
        : state_( a )
    {
        echo( state_ );
    }

private:
    State   state_;
};

class Bar
    : public Foo< int >
{
private:
    typedef Foo< int >  Base;
public:
    Bar( int a ): Base( a, echo ) {}
    static void echo( Base::State const& );
};

void Bar::echo( Base::State const& fooState )
{
    using namespace std;
    cout << "value: " << fooState.x_ << endl;
}

int main()
{
    Bar bar(100);
}

Если вы изучите эти две программы, вы, вероятно, заметитенебольшая разница (в дополнение к передаче времени компиляции и передачи знаний во время выполнения).

Наконец, есть решения, включающие грязное приведение типов, и в системе типов C ++ есть лазейка, позволяющая получить доступ к этому защищенному состоянию базового класса.без приведения, используя указатели членов.Первый опасен, а второй неясен и, возможно, неэффективен.Так что, не надо.

Но, надеюсь, вам подойдет одно из приведенных выше решений или подходящая адаптация.

Кстати, более общий набор проблем, который кажется вамбыть экземпляром, известным как DBDI , Динамическое связывание во время инициализации .Вы можете найти более общую трактовку этого в пункте часто задаваемых вопросов C ++ 23.6 Хорошо, но есть ли способ смоделировать это поведение, как если бы динамическое связывание работало с объектом this в конструкторе моего базового класса? .Кроме того, для особого случая, когда DBDI требует, чтобы часть конструкции базового класса контролировалась / предоставлялась производным классом, см. Запись в моем блоге "Как избежать пост-конструирования с помощью фабрики деталей" .

Приветствия и hth.,

4 голосов
/ 18 ноября 2010

Вы не можете вызвать echo() из конструктора Foo<T>.

Внутри конструктора Foo<T> динамический тип объекта - Foo<T>.Только после того, как конструктор Foo<T> завершит работу, динамический тип станет Bar.

Поскольку echo() является чисто виртуальным в Foo<T> и поскольку Foo<T> является динамическим типом объекта, выне может вызвать echo() в конструкторе Foo<T>.

Если вы не очень хорошо знаете, как изменяется динамический тип объекта во время строительства и разрушения, было бы хорошей идеей не пытаться вызывать виртуальные функцииот конструкторов и деструкторов.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...