2016-10-29 13 views
6

誰もこの予期しない動作を説明できますか?基底クラスctorから派生クラスの仮想関数を呼び出すことが予期せず可能です

前提

私はメンバーstd::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 

しかし私はThread CTORにstd::coutへの呼び出しを削除すると、予期しない実行時の動作があります:

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 

すなわち、 Thread ctorのstd::coutへの呼び出しを削除すると、派生クラス '基本クラス'コンストラクタのコンテキストから純粋/仮想関数を呼び出すことができるようになります。これは、事前の学習と経験に合致しません。 Windowsの10 gccのバージョンのCygwinのx64の中

ビルド環境は次のとおりです。私はこの観察によって困惑だし、何が起こっているかについての好奇心に燃えています

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の構築のために、ここでは何が起こるかです:

  1. mThreadが初期化されます。 OSは、ある時点で開始する予定です。
  2. std::cout << __PRETTY_FUNCTION__ << std::endl; - これはプログラムの観点からはかなり遅い操作です。

  3. Aコンストラクタが実行されます - 初期化し、そのvtableの(これはstanardで義務付けされていませんが、私の知る限り、すべての実装がこれを行います)。

    mThreadがスケジュールされる前にこのようなことが起こると、観察された動作が得られます。それ以外の場合は、純粋仮想呼び出しを取得します。

これらの操作の順序は決まっていないため、動作は定義されていません。

ベースのコンストラクタからかなり遅い操作を削除して、派生オブジェクトとそのvtableをより早く初期化することができます。 OSが実際に開始する前にmThreadのスレッドをスケジュールする前に、言ってください。それは言われている、これは問題を解決しなかった、ちょうどそれに遭遇する可能性が低い。

例を少し変更すると、IOコードを削除するとレースが難しくなりましたが、何も修正されていないことがわかります。メイン

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'を持たない場合、OSは 'std :: thread'をスケジュールする前にvtable _may_を初期化し、そうであれば' std: :thread'が 'ThreadStart'を呼び出すと、' Run() 'のvtableエントリが読み込まれ、' A :: Run() 'が呼び出されます。私はあなたを正しく得ましたか? – StoneThrow

+0

@StoneThrowはい。 – krzaq

関連する問題