としおの読書生活

田舎に住む社会人の読書記録を綴ります。 主に小説や新書の内容紹介と感想を書きます。 読書の他にもワイン、紅茶、パソコン関係などの趣味を詰め込んだブログにしたいです。

カテゴリ:C++ > 忘備録

3753817_s

本記事ではVisual Studio Codeで利用できるコンパイラのMinGwでDLLを作成して、呼び出す方法までを紹介します。

DLLを作成するとか難しそうというイメージをもっている人もいるかもしれませんが、やってみると意外と簡単なのでこの機会に作り方を覚えてしまいましょう。



DLLを作成する


今回は、サンプルとして足し算と引き算を行う関数が実装されているDLLを作成します。

ソースコードは以下の通りです。


DLLのヘッダファイル


// Calc.h
#ifndef H_CALC
#define H_CALC

int addition(int xint y);     // x+yをする
int subtraction(int xint y);  // x-yをする

#endif // H_CALC


DLLのソースファイル


// Calc.cpp
#include "Calc.h"

int addition(int xint y){
    return x+y;
}

int subtraction(int xint y){
    return x-y;
}


オブジェクトファイルの作成(.o)


ソースコードを用意したところでMinGwのG++コマンドを使ってDLLを作成していきましょう。

先ほどのソースコードにもコメントで書いているのですが、ソースコード名はCalc.cppです。

まず最初に-cオプションを使ってオブジェクトファイルを作成します。コマンドは以下のとおりです。

g++ -c .\Calc.cpp

このコマンドを実行するとCalc.cppと同じディレクトリにCalc.oというファイルが作成されます。


DLLの作成


DLLは-sharedオプションを使用することで作成することができます。コマンドは以下のとおりです。

g++ .\Calc.o -o Calc.dll -shared

上記のコマンドを実行するとCalc.dllというファイルが作成されます。

DLLの作成は以上で終了になります。
たったこれだけでいいの問う感じですが、やってみるとむちゃくちゃ簡単なんですよね。



作成したDLLを使う


次に先ほど作成したCalc.dllを呼び出す方法を紹介していきます。


DLLの呼び出し元のソースコード


// main.cpp
#include <iostream>
#include "Calc.h"

int main(void){
    int xy;

    std::cin >> x >> y;
    std::cout << "addition:" << addition(xy<< std::endl;
    std::cout << "subtraction:" << subtraction(xy<< std::endl;
    return 0;
}


コンパイルする


ソースコードができたらコンパイルしていきましょう。

コンパイルのコマンドは呼び出し元のコードをコンパイルするときにDLLの名前を教えてあげるだけです。

g++ -o .\main.exe .\main.cpp .\Calc.dll


実行結果


.\main.exe
3 5
addition:8
subtraction:-2


コンパイルして作成したmain.exeを実行してみると、DLLの関数が呼び出されていることが分かりますね。



まとめ


今回は、MinGwでDLLを作成して、その後作成したDLLを使う方法を紹介しました。

今回はコマンドを全て手打ちで実行していきましたが、ソースコードが増えるとすごく面倒です。

なのでそいうときはmakeファイルを作成してみましょう。

makeファイルの作り方については後日紹介させていただきます。




3753817_s

本記事では、C言語からC++で作成したDLLを呼びだす方法の失敗例と成功例を紹介していきます。

結果だけが知りたいという人はC言語からC++のDLLを呼び出すの章を読むだけで大丈夫です。



C++のDLLが呼び出せない例


まず最初にCからC++のDLLの呼び出しが失敗する例を紹介します。


C++のDLLのコード


ヘッダファイル

#ifndef H_SAMPLE
#define H_SAMPLE

void PrintSample();

#endif // H_SAMPLE


ソースファイル

#include "sample.h"
#include <iostream>

void PrintSample(){
    std::cout << "Hello CPP!!" << std::endl;
}


C言語のコード


#include "sample.h"

int main(void){
    PrintSample();
    return 0;
}


上記のコードをコンパイルすると下記のエラーコードがでてコンパイルすることができません。

undefined reference to `PrintSample' collect2.exe: error: ld returned 1 exit status




C言語からC++のDLLを呼び出す


上記のコードからC++のDLLのヘッダファイルを修正するだけで、C言語からC++の関数を呼びだせるようになります。

修正内容はヘッダファイルにextern "C"を追加するだけです。

修正したコードを以下にのせます。DLLのヘッダファイル以外は先ほどの失敗例で紹介したものと同じです。

ヘッダファイル

#ifndef H_SAMPLE
#define H_SAMPLE

#ifdef __cplusplus
extern "C"{
#endif /* __cplusplus */

    extern void PrintSample();

#ifdef __cplusplus
#endif /* __cplusplus */
#endif // H_SAMPLE


ソースファイル

#include "sample.h"
#include <iostream>

void PrintSample(){
    std::cout << "Hello CPP!!" << std::endl;
}


C言語のコード


#include "sample.h"

int main(void){
    PrintSample();
    return 0;
}


実行結果


Hello CPP!!




まとめ


extern "C"をC++のDLLのヘッダファイルに追加するだけで、簡単にC言語からC++のDLLを利用することができました。

ちなみに今回作成したDLLはC++でもそのまま利用することができるので、C言語用とC++用でDLLを2つ作成するひつようもありません。



3753817_s

静的メンバ変数を宣言した時に、コンパイルエラーが起きて
undefined reference to `Hogehoge::m_hogeNum'
といった感じに、宣言してたはずのメンバー変数が宣言していないとのエラーが発生しました。

本記事では、このエラーに対する対処方法をまとめていきます。




"undefined reference to"のエラーが出る場合のコード


以下のコードではをコンパイルすると冒頭で説明したように、「undefined reference to `Hogehoge::m_hogeNum'」というエラーメッセージが表示されます。


ソースコード

#include <iostream>

class Hogehoge{
public:
    Hogehoge(int num);
    ~Hogehoge();

private:
    static intm_hogeNum;
};

Hogehoge::Hogehoge(int num){
    m_hogeNum = &num;
}

Hogehoge::~Hogehoge(){}

int main(void){
    int x = 5;
    Hogehogehogehoge = new Hogehoge(5);

    return 0;
}



エラーメッセージ

undefined reference to `Hogehoge::m_hogeNum'
collect2.exe: error: ld returned 1 exit status



対策


調べてみると、staticメンバ変数も関数のように明示的にすることで"undefined reference"エラーを防ぐことができることが分かりました。

先ほど上記で紹介したコードを改造して、コンパイルがとおるようにしてみます。

#include <iostream>

class Hogehoge{
public:
    Hogehoge(int num);
    ~Hogehoge();

private:
    static intm_hogeNum;
};

int* Hogehoge::m_hogeNum = nullptr;

Hogehoge::Hogehoge(int num){
    m_hogeNum = &num;
}

Hogehoge::~Hogehoge(){}

int main(void){
    int x = 5;
    Hogehogehogehoge = new Hogehoge(5);

    return 0;
}

赤字にしている部分が先ほどのコードから追加した部分です。

intHogehoge::m_hogeNum = nullptr;

といった感じに明示的に宣言することで、"undefined reference"エラーを回避することができました。



まとめ


staticメンバ変数は、関数などとどうように明示的に定義しておく必要があることが分かりました。

久しぶりにC++を使ったりするとこういったくだらないミスで詰まってしまうことがありますので気をつけましょう。




3753817_s

C++の実行時間を計測するときにWindowsだとどうしたらいいのか迷うことが多かったので、精度別の実行時間の計測方法をまとめていきます。



clock(10ミリ秒程度)


clockはC言語でも使用できる標準形の関数で、プログラム実行開始からの経過時間をミリ秒単位で返します。

精度は処理系に依存しますが、Windowsでは10ミリ秒程度です。

C言語の標準ライブラリに含まれる関数なのでWindows以外でも使える点が嬉しいです。

以下は、実行時間計測のサンプルプログラムです。

#include <iostream>
#include <vector>
#include <time.h>

using namespace std;

int main(void){    
    clock_t start = clock();

    // 計測対象(今回はvectorに1000000個要素を追加する)
    vector<inttestVec;
    for (int i=0i<1000000; ++i){
        testVec.push_back(i);
    }
    clock_t end = clock();

    cout << end-start << endl;


    return 0;
}



timeGetTime(1ミリ秒)


timeGetTimeはWin32 APIで用意されている関数で、システム起動開始からの経過時間をミリ秒単位で返します。

精度は1ミリ秒程度です。

timeGetTimeを使用するためには、windows.hのインクルードと、winmm.libのリンクが必要です。

以下は、実行時間計測のサンプルプログラムです。

#include <iostream>
#include <vector>
#include <windows.h>

using namespace std;

int main(void){    
    DWORD start = timeGetTime();

    // 計測対象(今回はvectorに1000000個要素を追加する)
    vector<inttestVec;
    for (int i=0i<1000000; ++i){
        testVec.push_back(i);
    }
    DWORD end = timeGetTime();

    cout << end-start << endl;


    return 0;
}






QueryPerformanceCounter(マイクロ秒)


こちらもtimeGetTimeと同様にWin32 APIで用意されている関数です。

精度は環境に依存しますが、マイクロ秒~10ナノ秒程度で、Windowsで使える時間計測の方法で最も精度が高いです。

しかし、実行時間の計測開始前にQueryPerformanceFrequency関数でカウントアップする周波数を取得するなど少し他のAPIに比べて手間がかかるという欠点もあります。

以下は、実行時間計測のサンプルプログラムです。

#include <iostream>
#include <vector>
#include <windows.h>

using namespace std;

int main(void){    
    LARGE_INTEGER frequency;
    QueryPerformanceFrequency(&frequency);

    LARGE_INTEGER start;
    QueryPerformanceCounter(&start);

    // 計測対象(今回はvectorに1000000個要素を追加する)
    vector<inttestVec;
    for (int i=0i<1000000; ++i){
        testVec.push_back(i);
    }
    
    LARGE_INTEGER end;
    QueryPerformanceCounter(&end);

    cout << (double)(end.QuadPart - start.QuadPart) / frequency.QuadPart << endl

    return 0;
}



まとめ


今回はWindows環境でC++の実行時間を計測する方法をまとめました。

Linuxと違ってナノ秒単位で正確に計測することはできませんが、QueryPerformanceCounterを使えばかなり正確に実行時間が計測できることが分かりました。

他にも良い計測方法があればコメントで教えていただけますと幸いです。



3753817_s


C++でvectorに対してループを行う方法が色々あります。

今回は、どの書き方が一番効率が良いのかが気になったので調べてみました。




カウンタ方式


カウンタ変数を用意して、データにアクセスする方式。

今回時間の計測は、windows APIのQueryPerformanceCounterを使用して計測します。

「計測対象」とコメントで書かれている部分のループが実行時間の計測対象になります。
カウンタ方式以外では計測対象以外の部分のコードは省略します。

#include <iostream>
#include <vector>
#include <windows.h>

#define LOOPNUM 1000000
using namespace std;

int main(void){
    vector<inttestVec;
    for(int i=0i<LOOPNUM; ++i){
        testVec.push_back(i);
    }

    LARGE_INTEGER frequency;
    QueryPerformanceFrequency(&frequency);

    LARGE_INTEGER start;
    QueryPerformanceCounter(&start);

    // 計測対象
    for(int i=0i<testVec.size(); ++i){
        int a = testVec[i];
    }

    LARGE_INTEGER end;
    QueryPerformanceCounter(&end);

    DWORD end_tm = timeGetTime();
    std::cout << (double)(end.QuadPart - start.QuadPart) / frequency.QuadPart * 1000000 << std::endl

    return 0;
}



イテレータ方式


C++のコンテナクラスのイテレータを使用してデータにアクセスする方式。

for(auto itr=testVec.begin(); itr!=testVec.end(); ++itr){
    int a = *itr;
};



範囲ベース方式


範囲ベース方式は、C++ 11で追加されたコンテナオブジェクトの範囲分ループを行うという方式。

for(intvec : testVec){
    int a = vec;
}



for_each


for_eachは範囲内の全てのイテレータに特定の関数を適用させるときに使用する方法。

void substitute(int x){
    int a = x;
}

for_each(testVec.begin(), testVec.end(), substitute);






実行時間計測結果


今回は、最適化なしと最適化ありでそれぞれ10回ずつ計測して、平均値を実行時間とします。

vectorのサイズは1000000、実行時間の単位はμs(マイクロ秒)です。

実行時間(μs)
カウンタ方式2400.42
イテレータ方式7285.76
範囲ベース方式5402.42
for_each66284.8


最適化なしだとカウンタ方式が最速ということになりました。

余談になりますが、最適化オプション最高(-Ofast オプション)でコンパイルして、実行してみたところ全ての実行時間が0.1μs未満になり正確に計測できませんでした。

最適化オプション込みで実行時間を比較するにはどうすればよかったのだろうか…。



まとめ


今回はvectorのループの実行時間を計測していきました。

最適化オプションなしでの計測になってしまいましたが、Debug環境ではカウンタ方式が最速みたいです。

正直イテレータとかの方が早いのかなと思っていたので意外な結果になりました。



↑このページのトップヘ