2016-10-29 12 views
6

Может ли кто-нибудь объяснить это неожиданное поведение?Неожиданно можно вызвать виртуальную функцию производного класса из базового класса ctor

предпосылка

Я создал класс тему, которая содержит член std::thread переменные. Thread ctor создает элемент std::thread, предоставляя указатель на статическую функцию, которая вызывает чистую виртуальную функцию (которая будет реализована базовыми классами).

Кодекс

#include <iostream> 
#include <thread> 
#include <chrono> 

namespace 
{ 

class Thread 
{ 
public: 
    Thread() 
     : mThread(ThreadStart, this) 
    { 
     std::cout << __PRETTY_FUNCTION__ << std::endl; // This line commented later in the question. 
    } 

    virtual ~Thread() { } 

    static void ThreadStart(void* pObj) 
    { 
     ((Thread*)pObj)->Run(); 
    } 

    void join() 
    { 
     mThread.join(); 
    } 

    virtual void Run() = 0; 

protected: 
    std::thread mThread; 
}; 

class Verbose 
{ 
public: 
    Verbose(int i) { std::cout << __PRETTY_FUNCTION__ << ": " << i << std::endl; } 
    ~Verbose() { } 
}; 

class A : public Thread 
{ 
public: 
    A(int i) 
     : Thread() 
     , mV(i) 
    { } 

    virtual ~A() { } 

    virtual void Run() 
    { 
     for (unsigned i = 0; i < 5; ++i) 
     { 
      std::cout << __PRETTY_FUNCTION__ << ": " << i << std::endl; 
      std::this_thread::sleep_for(std::chrono::seconds(1)); 
     } 
    } 

protected: 
    Verbose mV; 
}; 

} 

int main(int argc, char* argv[]) 
{ 
    A a(42); 
    a.join(); 

    return 0; 
} 

Проблема

Как вы уже заметили, есть тонкая ошибка здесь: Thread::ThreadStart(...) вызывается из контекста Thread CTOR, поэтому называя чистый/виртуальная функция не будет вызывать реализацию производного класса. Это подтверждается ошибка выполнения:

pure virtual method called 
terminate called without an active exception 
Aborted 

Однако, есть неожиданное поведение во время выполнения, если удалить вызов std::cout в Thread CTOR:

virtual void {anonymous}::A::Run(){anonymous}::Verbose::Verbose(int): : 042 

virtual void {anonymous}::A::Run(): 1 
virtual void {anonymous}::A::Run(): 2 
virtual void {anonymous}::A::Run(): 3 
virtual void {anonymous}::A::Run(): 4 

И.Э. удаление вызова в std::cout в Thread ctor, по-видимому, вызвано тем, что он может вызвать конструктор «чистый/виртуальный» производного класса из контекста конструктора базового класса! Это не соответствует предыдущему обучению и опыту.

среды сборки в Cygwin x64 на Windows, 10. GCC версии:

g++ (GCC) 5.4.0 
Copyright (C) 2015 Free Software Foundation, Inc. 
This is free software; see the source for copying conditions. There is NO 
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 

Я сбит с толку этим наблюдением и сгораю от любопытства, что происходит. Может ли кто-нибудь пролить свет?

+0

Ничего неожиданного здесь. Когда объект строится только по любому конкретному базовому классу A, единственными доступными реализациями виртуальных функций являются те, которые видны от A. – EJP

+0

Жаль, что у нас нет post-ctor. Было бы так полезно здесь ... – Deduplicator

ответ

9

Поведение этой программы не определено из-за состояния гонки.

Но, если вы хотите рассуждать об этом, давайте попробуем.

Для строительства A «s, вот что происходит:

  1. mThread инициализируется. OS планирует запустить его в какой-то момент в будущем.
  2. std::cout << __PRETTY_FUNCTION__ << std::endl; - это довольно медленная операция с точки зрения программы.

  3. A конструктор работает - инициализирует его виртуальные таблицы (это не является обязательным по stanard, но, насколько я знаю, все реализации этого).

    Если это произойдет до того, как начнется mThread, вы получите наблюдаемое поведение. В противном случае вы получите чистый виртуальный вызов.

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

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

Если вы немного измените свой пример, вы заметите, что удаление кода ввода-вывода затрудняет поиск гонки, но ничего не исправлено.

virtual void Run() 
{ 
    for (unsigned i = 0; i < 1; ++i) 
    { 
     std::cout << __PRETTY_FUNCTION__ << ": " << i << std::endl; 
//  std::this_thread::sleep_for(std::chrono::seconds(1)); 
    } 
} 

главная:

for(int i = 0; i < 10000; ++i){ 
    A a(42); 
    a.join(); 
} 

demo

+1

Хорошее объяснение! –

+0

Если вы правильно поняли: если 'Thread' не имеет' cout', он инициализируется быстро, поэтому его vtable _may_ будет инициализирован до того, как ОС раскроет 'std :: thread', и если да, то к моменту' std: : thread' вызывает 'ThreadStart', запись vtable для' Run() 'будет заполнена, тем самым вызывая' A :: Run() '. Я правильно понял вас? – StoneThrow

+0

@StoneThrow да. – krzaq