【C++/CLI&C#】CLR環境下でstd::async, std::future


常套手段

概要

C++/CLIでも、ネイティブコード内で非同期処理を走らせたいが、CLIでfutureをincludeすると

#error:  <future> is not supported when compiling with /clr or /clr:pure.

とエラーを吐き出すので、それの対処法。

なお、c++11から導入された非同期処理全てに該当するので、試していないがmutexであろうがthreadであろうが同じようなエラーを発する。

参考記事

Stackoverflowにてそれに類似した内容があったので、参照.
こちらはstd::futureではなく、std::mutex.
(最後のマイナスが付いているreplyはあてにならないので、これは参照してはいけない.)

ちなみに、Microsoft公式の掲示板では、「clrで非同期処理を使える仕様ではありません。なのでclrを外してくれ」という掲示板のレスにありがちな解決策を示しているようで示していない回答があったので、検索してこちらの記事が出た場合はスルー推奨だ。

なお、こちらの記事にも内容があるが、別のやり方で対処します。

エラーが発生するコード

コードの問題点

ヘッダ内に、いくつかのメンバ関数がいてて、非/clrからは、何かの自動制御をする関数"MoveAuto"と状態を取得する関数"GetStatusText"が定義されているものとする.
なお、/clr部分はDLL化し、非/clr化部分はスタティックライブラリとする。

このとき、futureがOPMeasure.hのincludeによって度々includeされ、結果的にfutureにまでたどり着いてしまうので、エラーが発生する。

ソースコード

OPMeasure.h
/*
 以下のプログラムは、CLR拡張でビルドされるものとする。
 それ以外は、C++ Nativeでビルドされる。
*/
#pragma once
#include "CommandController.h"

namespace OPMeasure {
    public ref class OPMeasureApi {
    private:
        CommandController* _controller;
    public:
        OPMeasureApi() {
            _controller = new CommandController();      
        }
        OPMeasureApi(const OPMeasureApi% obj) { }
        ~OPMeasureApi() {
            this->!OPMeasureApi();      
        }

        !OPMeasureApi() {
            if( _controller != nullptr) {
                delete _controller;
            }
        }

        System::String^ GetStatusText(System::String^ mtext) {
            return marshal_as<System::String^>(_controller->GetStatusText(L"piyo"));
        }
        System::Void MoveAuto() {
            _controller->MoveAuto(L"Move");
        }
    };
}
CommandController.h
#pragma once
#include <string>
#include <vector>
#include <map>

#include "CommandController.h"

class CommandController {
private:
    TaskWithStatus* _internal_task;

public:
    CommandController() {
        _internal_task = nullptr;
    }

    CommandController(const CommandController& obj ) {

    }

    ~CommandController() {

    }

    std::wstring GetStatusText(std::wstring text) {
        return _internal_task->ToString();
    }

    void MoveAuto(std::wstring param_text) {
        _internal_task = new TaskWithStatus();
        _internal_task->Start();
    }
};
TaskWithStatus.h
#pragma once
#include <Windows.h>
#include <future>

class TaskWithStatus {
private:
    std::wstring _text_status;
    std::future<bool> _ftr_result;
public:
    TaskWithStatus() { }
    ~TaskWithStatus() { }

    void Start() {
        _ftr_result = std::async(std::launch::async, [this] { Run(); return true; });
    }

    void Run() {
        _text_status = L"実行開始";
        Sleep(5000);
        _text_status = L"実行終了";
    }

    std::wstring ToString() {
        return _text_status;
    }
};

修正コード

修正コードの概要

(1) ヘッダファイルではなく、cppファイルに書くことで、OPMeasure.hからincludeされるのを避ける。

これは当たり前のことで、ヘッダファイルが連続してインクルードされていてビルドエラーが発生しているので、それを抑制させる。
つまり、CommandController.cppを用意すれば、includeの問題は回避できる。

(2) ITask.hを定義し、インタフェースとして使用する

(1)だけでは、システム上の問題がある。それは、CommandControllerに、TaskWithStatusのメンバ変数を直接持たせるためには、ヘッダファイルCommandController.hにTaskWithStatusをincludeする必要があり、これも結果的に、clr側から見てfutureをインクルードしてしまう。

回避する方法として、ITask.hを定義して、TaskWithStatus.hのインタフェースを定義して、CommandController.hからはITaskを参照するようにする。

そして、CommandController.cpp側に、具体化されたTaskWithStatusを生成するように変更すれば、TaskWithStatusも、CommandControllerのメンバ変数として持たせることが出来る。

ソースコード

OPMeasure.hは変更なし.

CommandController.h
#pragma once
#include <string>
#include <vector>
#include <map>

#include "ITask.h"

class CommandController {
private:
    ITask* _internal_task;

public:
    CommandController() {
        _internal_task = nullptr;
    }
    CommandController(const CommandController& obj ) {

    }
    ~CommandController() {  
    }


    std::wstring GetStatusText(std::wstring text);
    void MoveAuto(std::wstring param_text) ;
};


CommandController.cpp
#include "pch.h"
#include "CommandController.h"
#include "TaskWithStatus.h"

std::wstring CommandController::GetStatusText(std::wstring text) {
    return _internal_task->ToString();
}

void CommandController::MoveAuto(std::wstring param_text) {
    _internal_task = new TaskWithStatus();
    _internal_task->Start();
}

ITask.h
#pragma once

#include <string>

class ITask {
public:
    ITask() { }
    virtual ~ITask() { }
public:
    virtual void Start() = 0;
    virtual void Run() = 0;
    virtual std::wstring ToString() {
        return L"ITask";
    }
};
TaskWithStatus.h
#pragma once

#include "ITask.h"
#include <Windows.h>
#include <future>


class TaskWithStatus : public ITask {
private:
    std::wstring _text_status;
    std::future<bool> _ftr_result;
public:
    TaskWithStatus() { }
    virtual ~TaskWithStatus() { }

    void Start() override {
        _ftr_result = std::async(std::launch::async, [this] { Run(); return true; });
    }

    void Run() override {
        _text_status = L"実行開始";
        Sleep(5000);
        _text_status = L"実行終了";
    }

    std::wstring ToString() override {
        return _text_status;
    }
};

実行結果(おまけ)

ASP. NET MVC 5にて実行.
セッションを使って、状態保持をするプログラムなどを使いましたが、省略します.
同じようなことは、C++/CLIを使用する、.NET Framework/.NET Coreの各フレームワークにも適用できるでしょう.

実行直後

5秒後