前回予告しました通り、出来上がったC++版をC#(Ver 5)へ移植してみました。C++版との違いなど、ご覧あれ。尚、Calc_Int.csはDLL版としてコンパイルするものです。そのまま実行ファイル(*.exe)にするにはMain関数でCalcインスタンスを作って、TryCalcメソッドを実行してください。

 

【Calc_Int.cs】

////////////////////////////////////////////////
// Calc Class - Evaluates String Expression and
// retruns end-result (integer value).
////////////////////////////////////////////////
/*
【Calc仕様】
1.四則演算子(+, -, *, /, %-opecode配列に収納)を用いた(整数の)算術計算式の値を取得する。
2.計算は左から右へ行う。
3.優先順位は単項演算子(+-)→()→*、/、%→+、-の順
4.値は10進数、16進数(Int32.ParseとSystem.Globalization.NumberStyles.HexNumberを使用)//https://learn.microsoft.com/ja-jp/dotnet/api/system.globalization.numberstyles?view=net-8.0
5.二項式は"operand0 四則演算子(+, -, *, /, %)operand1"とする。operand0と1は(空白文字を除く)文字列、その値はopevalu0と1整数とする。
6.最初に出てくること有るべき単項式(operand0が空文字""でopeval0が0)は単に"operand0 加減演算子(+, -)operand1"で処理し、*、/、%はエラーとする。
7.値はNumber(10進数)、HexNumber(16進数)対応とし、HexNumberには頭に"0x"か"0X"をつける必要がある。

【参考サイト】
String Class - https://learn.microsoft.com/ja-jp/dotnet/api/system.string?view=net-8.0
Chars[Int32] プロパティ
Length プロパティ
Substring(Int32 pos, Int32 len)

Char Class - https://learn.microsoft.com/ja-jp/dotnet/api/system.char?view=net-8.0
IsWhiteSpaceメソッド

Int32.TryParseメソッド
https://learn.microsoft.com/ja-jp/dotnet/api/system.int32.tryparse?view=net-9.0#system-int32-tryparse(system-string-system-globalization-numberstyles-system-iformatprovider-system-int32@)
*/

using System;
using System.Globalization;        //NumberStyle使用の為(オリジナルコードであれば不要)

namespace Calc_Int
{
    public class Calc
    {
        //クラス内定数
        const int ERR = -1;
        //メンバー変数
        public string Formula {get; set;}            //演算式記録用

        /////////////////////
        //コンストラクター
        /////////////////////

        public Calc(string str = "")
        {
            if(str != string.Empty)
                Formula = str;                        //strをFormulaにコピー
        }

        ///////////////////////////////////////////////
        //演算制御を行う主関数(+または-の処理を行う)
        //演算に成功するとtrue、失敗するとfalseを返す。
        //参照変数sol(ution)に解が入る。
        ///////////////////////////////////////////////

        public bool TryCalc(ref int sol, string str = "")
        {
            //ローカル変数
            int ptr, start, end;                    //順に演算式用、切り取り用ポインター
            int opeval = 0;                            //式評価用オペランド値
            int opec = 0;                            //オペコード(+は1、-は2)
            bool post_opec = false;                    //「オペコード後」サイン
            string operand;                            //文字列切り取り用
            //初期化
            sol = 0;                                //解(solution)が入る参照変数を初期化
            if(str == string.Empty)                    //strが既定値の空文であれば
                str = Formula;                        //計算式(str)をFormulaとする
            else                                    //strに指定があれば
                Formula = str;                        //strをFormulaにコピー
            ptr = 0;                                //ポインターを演算式文字列の起点に初期化
            end = start = ERR;                        //切り取りポインター初期化(値は0~str.Length - 1
            //ループ処理
            for(ptr = 0; ptr < str.Length; ptr++)    //strの最後まで継続(C#はヌル終端でない為、文字列長で判断)
            {
                //空白処理
                if(Char.IsWhiteSpace(str, ptr))
                {
                    if(start > ERR && end == ERR)    //startは設定され、endは設定されていない場合
                    end = ptr - 1;                    //endを空白文字直前に設定
                }
                //ポインターの指す文字により処理を振り分ける
                else
                {
                    switch(str[ptr])
                    {
                    case '+':                        //'+'の場合
                    case '-':                        //'-'の場合
                        if(post_opec)                    //オペレーター後にオペレーターが続けば
                            return false;                //エラー
                        if(start == ERR)                //単項演算(start == ERR)の場合
                            start = ptr + 1;            //startを+-符号(Chars[0])の次に進める
                        else                            //二項演算で
                        {
                            if(end == ERR)                //オペランド未設定(end == ERR)ならば
                                end = ptr - 1;            //'+'、'-'演算子の直前に設定
                            //文字列切り取り-string.Substring(int startIndex, int length);
                            operand = str.Substring(start, end - start + 1);
                            if(!TryParse(ref opeval, operand))        //整数値でない場合
                            {
                                if(!TryMulDiv(ref opeval, operand))    //乗除演算が不可能
                                    return false;        //エラー
                            }
                        }
                        if(opec == 0)                    //初期値の場合(単項式の場合
                            sol = opeval;                //第1オペランドであるsolに代入する
                        else if(opec == 1)                //'+'の場合
                            sol += opeval;                //(単項演算子を含む)加算または
                        else if(opec == 2)                //'-'の場合
                            sol -= opeval;                //(単項演算子を含む)減算を行う
                        if(str[ptr] == '+')                //新しいオペコードをopecに設定
                            opec = 1;                    //'+'の設定
                        else if(str[ptr] == '-')
                            opec = 2;                    //'-'の設定
                        end = start = ERR;                //切り取りポインターの初期化
                        post_opec = true;                //オペレーター後フラグ
                        break;
                    case '*':                        //'*'の場合
                    case '/':                        //'/'の場合
                    case '%':                        //'%'の場合
                        if(start == ERR)                //最初に'*'、'/'、'%'があれば
                            return false;                //エラー
                        if(post_opec)                    //オペレーター後にオペレーターが続けば
                            return false;                //エラー
                        end = start;                    //end == start(defaultでの処理の為)にして
                        post_opec = true;                //オペレーター後フラグ
                        break;
                    default:                        //'+'、'-'、'*'、'/'、'%'以外の非空白文字
                        if(start == ERR)                //startが未設定であれば
                            start = ptr;                //非空白文字の先頭に設定
                        else if(start == end)            //'*'、'/'、'%'があった場合の次の非空白文字
                            end = ERR;                    //オペランド未設定(end == nullptr)に戻す
                        else if(start > ERR && end > ERR)    //start、end共に設定済で演算子以外の場合
                            return false;                //構文エラー
                        if(str[ptr] == '(')                //括弧の場合
                        {
                            if(!SkipBraces(str, ref ptr))//括弧をスキップし、ポインターは')'を指す
                                return false;            //括弧が閉じられていなければ構文エラー
                            end = ptr;                    //endも')'を指す
                        }
                        post_opec = false;                //オペランド後なので、オペレーター後フラグを戻す
                        break;
                    }
                }
            }
            //ポインターを進めて終端になった場合(+-のラップアップ処理)
            if(post_opec)                        //オペレーター後にオペレーターが続けば
                return false;                    //エラー
            if(end == ERR)                        //endが未設定であれば
                end = ptr - 1;                    //終端の直前に設定
            //文字列切り取り-string.Substring(int startIndex, int length);
            operand = str.Substring(start, end - start + 1);
            if(!TryParse(ref opeval, operand))        //整数値でない場合
            {
                if(!TryMulDiv(ref opeval, operand))    //乗除演算が不可能
                    return false;                //エラー
            }
            if(opec == 0)                        //初期値の場合
                sol = opeval;                    //第1オペランドであるsolに代入する
            else if(opec == 1)                    //'+'の場合
                sol += opeval;                    //加算または
            else if(opec == 2)                    //'-'の場合
                sol -= opeval;                    //減算を行う
            return true;
        }

        ////////////////
        //乗除演算関数
        ////////////////

        private bool TryMulDiv(ref int sol, string str)
        {
            //ローカル変数
            int ptr, start, end;                    //順に演算式用、切り取り用ポインター
            int opeval = 0;                            //式評価用オペランド値
            int opec = 0;                            //オペコード(*は1、/は2、%は3)
            string operand;                            //文字列切り取り用
            //初期化
            sol = 0;                                //解(solution)が入る参照変数を初期化
            ptr = 0;                                //ポインターを演算式文字列の起点に初期化
            end = start = ERR;                        //切り取りポインター初期化(値は0~str.Length - 1)
            //ループ処理
            for(ptr = 0; ptr < str.Length; ptr++)    //strの最後まで継続(C#はヌル終端でない為、文字列長で判断)
            {
                //空白処理
                if(Char.IsWhiteSpace(str, ptr))
                {
                    if(start >= 0 && end < 0)        //startは設定され、endは設定されていない場合
                    end = ptr - 1;                    //endを空白文字直前に設定
                }
                //ポインターの指す文字により処理を振り分ける
                else
                {
                    switch(str[ptr])
                    {
                    case '*':                        //'*'の場合
                    case '/':                        //'/'の場合
                    case '%':                        //'%'の場合
                        if(str[start] == '(')            //startが'('であれば
                        {
                            start++;                    //startを'('の次に(endは')'の直前)進める
                            //文字列切り取り-string.Substring(int startIndex, int length);
                            operand = str.Substring(start, end - start + 1);
                            if(!TryCalc(ref opeval, operand))    //再帰処理
                                return false;            //処理不能であればエラー
                        }
                        else
                        {
                            if(end < 0)                    //オペランド未設定(end == ERR)ならば
                                end = ptr - 1;            //'*'、'/'、'%'演算子の直前に設定
                            //文字列切り取り-string.Substring(int startIndex, int length);
                             operand = str.Substring(start, end - start + 1);
                            if(!TryParse(ref opeval, operand))    //整数値でない場合
                                return false;            //エラー
                        }
                        if(opec == 0)                    //初期値の場合
                            sol = opeval;                //第1オペランドであるsolに代入する
                        else if(opec == 1)                //'*'の場合
                            sol *= opeval;                //乗算、
                        else if(opec == 2)                //'/'の場合
                            sol /= opeval;                //除算または
                        else if(opec == 3)                //'%'の場合
                            sol %= opeval;                //剰余算を行う
                        if(str[ptr] == '*')                //新しいオペコードを設定
                            opec = 1;                    //'*'の設定
                        else if(str[ptr] == '/')
                            opec = 2;                    //'/'の設定
                        else if(str[ptr] == '%')
                            opec = 3;                    //'%'の設定
                        end = start = ERR;                //切り取りポインターの初期化
                        break;
                    default:                        //'*'、'/'、'%'以外の非空白文字
                        if(start < 0)                    //startが未設定であれば
                            start = ptr;                //非空白文字の先頭に設定
                        else if(start >= 0 && end >= 0)    //start、end共に設定済で演算子以外の場合
                            return false;                //構文エラー
                        if(str[ptr] == '(')                //括弧の場合
                        {
                            if(!SkipBraces(str, ref ptr))    //括弧をスキップし、ポインターは')'を指す
                                return false;            //括弧が閉じられていなければ構文エラー
                            end = ptr - 1;                //endに')'の前のアドレスを記録
                        }
                        break;
                    }
                }
            }
            //ポインターを進めて終端になった場合(ラップアップ処理)
            if(str[start] == '(')                    //startが'('であれば
            {
                start++;                            //startを'('の次に(endは')'の直前)進める
                //文字列切り取り-string.Substring(int startIndex, int length);
                operand = str.Substring(start, end - start + 1);
                if(!TryCalc(ref opeval, operand))    //再帰処理
                    return false;                    //処理不能であればエラー
            }
            else
            {
                if(end < 0)                            //オペランド未設定(end == ERR)ならば
                    end = ptr - 1;                    //'*'、'/'、'%'演算子の直前に設定
                //文字列切り取り-string.Substring(int startIndex, int length);
                operand = str.Substring(start, end - start + 1);
                if(!TryParse(ref opeval, operand))    //整数値でない場合
                    return false;                    //エラー
            }
            if(opec == 0)                            //初期値の場合
                sol = opeval;                        //第1オペランドであるsolに代入する
            else if(opec == 1)                        //'*'の場合
                sol *= opeval;                        //乗算、
            else if(opec == 2)                        //'/'の場合
                sol /= opeval;                        //除算または
            else if(opec == 3)                        //'%'の場合
                sol %= opeval;                        //剰余算を行う
            return true;
        }
 

と、

 

書いてきたら「6,000字超えたから」と無情にもエラーが...

 

後は

 

(2)へ続く。

さて、前回まででEmbarcadero C++版のCalcクラスのサンプルプログラムを完成させました。

 

これはこれでよいと思いますが、当初のコミットメントではC#版も作るようなことを言ってしまいました。(

:言わなけりゃよかった!(「コンソールベースで(Cではなく)C++で再現に務め、出来上がったらそれをC#に移植でもして処理時間を比べてみよう。」)

 

仕方がないので、C++版をC#に移植して、それを基に

 

初期設定

時間計測開始

 Calc処理

時間計測完了

 

というように時間計測して比べようと思います。

 

所が、

 

C++からC#への移植に

 

結構手間取ってしまいました!

 

その理由は、

 

(1)C++版では演算式文字列を(ユニコードベースなので)wchar_tの配列にして、頭からポインターで読み込み、文字列の終了はヌル終端(wchar_t '\0'-2バイトの0)で判断する、というものでしたが、

 

(2)C#はMicrosoftの開発言語ですが、BorlandのDelphi開発陣が開発に携わったことで伝統的なC/C++とはちょっと異なるところがあり、今回のC#版でいえば

 

「文字列にヌル終端がない」

 

ことから、C++版で文字のヌル終端を当てにして書いていた処理(例えば"while(*ptr != L'\0')  {}")を書き直す必要がありました。

 

(3)又、ヌル終端がないために「括弧を飛ばしてゆく」SkipBraces関数などは、ポインターを「括弧の次(C++版なら最悪ヌル終端がある)」に進めると、

 

C#版ではエラー(IndexOutOfRangeException)

 

になってしまうことです。この為、C#版ではSkipBracesメソッドを「ポインターを'('から')'まで進める」ように仕様変更しています。

 

(4)尚、C++版ではoperatorを使ってCALCクラスインスタンスに直接「=(等号)」を使って演算式文字列を与えることが出来ましたが、「C#とC++は互換性がないため、C#でC++のオペレーターのようなことはできません」(Search Labs|AI

 

(5)逆にC#が良い点はメソッドが豊富で、C++版ではプリミティブな処理(例:TryParse関数)を書いていたところを既存のメソッドで置き換えることが出来ます。(

:但し、Windows 11に同梱されているC#はVersion 5.0なので、2進数は取り扱えない、というような問題もあるのですが...

 

何れにしても、C#版を作ってみた

 

ので、年末のご挨拶に紹介させていただきましょうか?

 

ps. 少し気の早いお年玉として、ネイティブベースで走らせるEmbarcadero C++版と、Microsoft 共通言語基盤(CLI)ベースのC#版との処理時間比較を最後にご紹介させていただきます。

 

前回、Clacプログラムの心臓となるCLACクラスの概要を説明いたしました。今回はそれを実際のC++のコードで解説します。なお、前回も書きましたが、以下のコードは備忘として書き込んだコメントが多く、それだけで十分だと思いますが、補足はいつも通り解説:で付記します。

 

【Calc.h】

/*
【Calc仕様】
1.四則演算子(+, -, *, /, %-opecode配列に収納)を用いた(整数の)算術計算式の値を取得する。
2.計算は左から右へ行う。
3.優先順位は単項演算子(+-)→()→*、/、%→+、-の順
4.値は2進数、10進数、16進数整数を扱う。
5.二項式は"operand0 四則演算子(+, -, *, /, %)operand1"とする。operand0と1は(空白文字を除く)文字列(解説:operandになりました。)、その整数値はopeval0と1とする。(解説:これはsolとopevalになりました。)
6.最初に出てくること有るべき単項式(operand0が空文字""でopeval0が0)は単に"operand0 加減演算子(+, -)operand1"で処理し、*、/、%はエラーとする。
7.値はNumber(10進数)、HexNumber(16進数)対応とし、HexNumberには頭に"0x"か"0X"をつける必要がある。(解説:これに2進数が追加になり、頭に"0b"か"0B"をつけるようになり

ました。)
*/
//ワイド文字を使用する

#define        UNICODE    1    //Unicode使用の為(解説:ウィンドウズ版はECCSkelton.hにあるので不要)
#include    <wchar.h>    //Unicode使用の為(解説:同上)

////////////////////////////////
// Calcクラス定義ファイル
//(整数算術式演算ライブラリー)
// Copyright (c) 2024 by Ysama
////////////////////////////////

class CALC
{
private:          //解説:外部からのアクセス不能
    //メンバー変数
    wchar_t *m_formula = nullptr;            //演算式記録用
    //メンバー関数
    void Init(const wchar_t*);                //初期化関数(演算式の記録)
    bool TryMulDiv(int&, const wchar_t*);    //乗除演算、括弧演算を行う
    bool TryParse(int&, const wchar_t*);    //10、2または16進数文字列から整数値を返す
    wchar_t* SkipBraces(wchar_t*);            //括弧をスキップし、次のアドレスを返す

public:           //解説:外部からのアクセス可能
    CALC();                                    //コンストラクター1
    CALC& operator = (wchar_t*);            //operator '='(wchar_t文字列の代入)
    CALC(wchar_t*);                            //コンストラクター2
    ~CALC();                                //デストラクター
    bool TryCalc(int&, const wchar_t*);        //加減演算を行う主関数

    ////////////////////////////////
    //クラス内クラス Part Of String
    //st - ed 間の文字列を保有し返す
    ////////////////////////////////

    //解説:クラス内クラスです。元々はwchar_t配列とnew、deleteで処理していましたが、Chat-GPTのアドバイスからこのように書き換えました。
    class CPOS
    {
    private:
        wstring m_partstr;    //解説:切り取り文字列用の、ユニコード文字列操作クラスwstirngインスタンスです。
    public:
        CPOS() : m_partstr(L"") {}    //解説:コンストラクター。初期値として空白文字列が与えられます。
        CPOS(const wchar_t* st, const wchar_t* ed) {    //解説:コンストラクター。初期値でst~edまでの文字列が与えられます。
            Set(st, ed);
        }
        void Set(const wchar_t* st, const wchar_t* ed) {    //解説:st~edまでの文字列が与えられ、m_partstrにコピーします。
            if(ed >= st)    //引数順序st (<=) ed
                m_partstr = wstring(st, ed + 1);
            else            //引数順序間違い対応
                m_partstr = wstring(ed, st + 1);
        }
        const wchar_t* Get() const {    //解説:切り取られた文字列をwchar_t配列として返します。
            return m_partstr.c_str();
        }
    };
};

/////////////////////
//コンストラクター1
/////////////////////

CALC::CALC() {

    //ワイド文字使用のためロケールの初期化(日本語)
    setlocale(LC_CTYPE, "ja_JP");
    Init(L"");        //初期化関数
}

//////////////////////////////////////
//operator '='(wchar_t文字列の代入)
//////////////////////////////////////

CALC& CALC::operator = (wchar_t* str) {

    Init(str);        //初期化関数
    return *this;
}

/////////////////////
//コンストラクター2
/////////////////////

CALC::CALC(wchar_t* str) {

    CALC();            //引数無しコンストラクター
    Init(str);        //初期化関数
}

//////////////////
//デストラクター
//////////////////

CALC::~CALC() {

    delete [] m_formula;        //解説:メモリーを確保した場合は確実にdeleteで開放する。
}

//////////////
//初期化関数
//////////////

void CALC::Init(const wchar_t* str) {

    if(m_formula)                    //既存の文字列があれば破棄
        delete [] m_formula;
    int len = wcslen(str) + 1;        //引数文字列の長さ
    m_formula = new wchar_t[len];    //同じ長さのワイド文字配列を作成
    //数字、アルファベット、空白文字等以外を除外してm_formulaにコピーする

    int j = 0;
    for(int i = 0; i < len; i++) {
        if(    (str[i] >= u'\u0030' && str[i] <= u'\u0039') ||    //0-9の場合(半角)
            (str[i] >= u'\u0041' && str[i] <= u'\u0046') ||    //A-Fの場合(半角)
            str[i] == L'X'    ||    //(半角X)
            (str[i] >= u'\u0061' && str[i] <= u'\u0066') ||    //a-fの場合(半角)
            str[i] == L'x'    ||    //(半角x)
            (str[i] == L'('    || str[i] == L')') ||    //()の場合(半角)
            iswspace(str[i])||    //(空白文字)
            str[i] == L'+'    ||    //(半角+)
            str[i] == L'-'    ||    //(半角-)
            str[i] == L'*'    ||    //(半角*)
            str[i] == L'/'    ||    //(半角/)
            str[i] == L'%') {    //(半角%)
            m_formula[j] = str[i];
            j++;
        }
    }
    m_formula[j] = L'\0';            //ヌル終端
/*    旧初期化関数(コントロールコードが入っていると混入し得る)
    if(m_formula)                    //既存の文字列があれば破棄
        delete [] m_formula;
    int len = wcslen(str) + 1;        //引数文字列の長さ
    m_formula = new wchar_t[len];    //同じ長さのワイド文字配列を作成
    wcscpy(m_formula, str);            //m_formulaに算術式文字列をコピー
*/

    //解説:ここはキャラクターコンタミを疑って書き換えたので、ひょっとすると単にコピーするだけで十分かもしれませんが、為念です。
}

///////////////////////////////////////////////
//演算制御を行う主関数(+または-の処理を行う)
//演算に成功するとtrue、失敗するとfalseを返す。
//参照変数sol(ution)に解が入る。
///////////////////////////////////////////////

bool CALC::TryCalc(int& sol, const wchar_t* str = L"") {
//解説:C、C++は基本的にスタックによる「値渡し」ですが、solは&を使って「参照渡し」にしています。

    //ローカル変数
    wchar_t *ptr, *start, *end;                //順に演算式用、切り取り用ポインター
    int opeval = 0;                            //式評価用オペランド値
    int opec = 0;                            //オペコード(+は1、-は2)
    bool post_opec = false;                    //「オペコード後」サイン
    CPOS operand;                            //文字列切り取り用CPOSインスタンス
    //初期化
    sol = 0;                                //解(solution)が入る参照変数を初期化
    if(*str != L'\0')                        //strが既定値の空文でなければ
        Init(str);                            //計算式(str)でm_formulaを初期化
    ptr = m_formula;                        //ポインターを演算式文字列の起点に初期化
    end = start = nullptr;                    //切り取りポインターの初期化
    //ループ処理
    while(*ptr != L'\0') {                    //ヌル終端に至るまで継続
        //空白処理
        if(iswspace(*ptr)) {
            if(start && !end)                //startは設定され、endは設定されていない場合
                end = ptr - 1;                //endを空白文字直前に設定
            ptr++;                            //空白文字をスキップ
        }
        //ポインターの指す文字により処理を振り分ける
        else {
            switch(*ptr) {
            case L'+':                        //'+'の場合
            case L'-':                        //'-'の場合
                if(post_opec)                    //オペレーター後にオペレーターが続けば
                    return false;                //エラー
                if(!start)                        //単項演算(start == nullptr)の場合-問題+-の後に+-が書かれていたら
                    start++;                    //startを+-符号の次に進める
                else {                            //二項演算で
                    if(!end)                    //オペランド未設定(end == nullptr)ならば
                        end = ptr - 1;            //+-演算子の直前に設定
                    operand.Set(start, end);    //文字列切り取り
                    if(!TryParse(opeval, operand.Get()))        //整数値でない場合
                        if(!TryMulDiv(opeval, operand.Get()))    //乗除演算が不可能
                            return false;                        //エラー
                }
                if(opec == 0)                    //初期値の場合(単項式の場合)
                    sol = opeval;                //第1オペランドであるsolに代入する
                else if(opec == 1)                //'+'の場合
                    sol += opeval;                //(単項演算子を含む)加算または
                else if(opec == 2)                //'-'の場合
                    sol -= opeval;                //(単項演算子を含む)減算を行う
                if(*ptr == L'+')                //新しいオペコードをopecに設定
                    opec = 1;                    //'+'の設定
                else if(*ptr == L'-')
                    opec = 2;                    //'-'の設定
                ptr++;                            //次のオペランドへ進む
                end = start = nullptr;            //切り取りポインターの初期化
                post_opec = true;                //オペレーター後フラグ
                break;
            case L'*':                        //'*'の場合
            case L'/':                        //'/'の場合
            case L'%':                        //'%'の場合
                if(!start)                        //最初に'*'、'/'、'%'があれば
                    return false;                //エラー
                if(post_opec)                    //オペレーター後にオペレーターが続けば
                    return false;                //エラー
                end = start;                    //end == start(defaultでの処理の為)にして
                ptr++;                            //ポインターを進める
                post_opec = true;                //オペレーター後フラグ
                break;
            default:                        //'+'、'-'、'*'、'/'、'%'以外の非空白文字
                if(!start)                        //startが未設定(nullptr)であれば
                    start = ptr;                //非空白文字の先頭に設定
                else if(start == end)            //'*'、'/'、'%'があった場合の次の非空白文字
                    end = nullptr;                //オペランド未設定(end == nullptr)に戻す
                else if(start && end)            //start、end共に設定済で演算子以外の場合
                    return false;                //構文エラー
                if(*ptr == L'(') {                //括弧の場合
                    ptr = SkipBraces(ptr);        //括弧をスキップし、ポインターは')'の次を指す
                    if(!ptr)                    //括弧が閉じられていなければ(ptr == nullptr)
                        return false;            //構文エラー
                }
                else
                    ptr++;                        //ポインターを進める
                post_opec = false;                //オペランド後なので、オペレーター後フラグをfalseに戻す
                break;
            }
        }
        //ポインターを進めてヌル終端になった場合(+-のラップアップ処理)
        if(*ptr == L'\0') {
            if(post_opec)                    //オペレーター後にオペレーターが続けば
                return false;                //エラー
            if(!end)                        //endが未設定であれば
                end = ptr - 1;                //ヌル終端の直前に設定
            operand.Set(start, end);        //文字列切り取り
            if(!TryParse(opeval, operand.Get()))        //整数値でない場合
                if(!TryMulDiv(opeval, operand.Get()))    //乗除演算が不可能
                    return false;                        //エラー
            if(opec == 0)                    //初期値の場合
                sol = opeval;                //第1オペランドであるsolに代入する
            else if(opec == 1)                //'+'の場合
                sol += opeval;                //加算または
            else if(opec == 2)                //'-'の場合
                sol -= opeval;                //減算を行う
        }
    }
    return true;
}

////////////////
//乗除演算関数
////////////////

bool CALC::TryMulDiv(int& sol, const wchar_t* str) {

    //ローカル変数
    wchar_t *ptr, *start, *end;                //演算式ポインターと切り取り用ポインター
    int opeval = 0;                            //式評価用オペランド値
    int opec = 0;                            //オペコード(*は1、/は2、%は3:初期値は何もしない0)
    CPOS operand;                            //文字列切り取り用CPOSインスタンス
    //初期化
    sol = 0;                                //解(solution)が入る参照変数を初期化
    ptr = (wchar_t*)str;                    //ポインターを引数文字列の起点に初期化
    end = start = nullptr;                    //切り取りポインターの初期化
    //ループ処理
    while(*ptr != L'\0') {                    //ヌル終端に至るまで
        //空白処理
        if(iswspace(*ptr)) {
            if(start && !end)                //startは設定され、endは設定されていない場合
                end = ptr - 1;                //endを空白文字直前に設定
            ptr++;                            //空白文字をスキップ
        }
        //ポインターの指す文字により処理を振り分ける
        else {
            switch(*ptr) {
            case L'*':                        //'*'の場合
            case L'/':                        //'/'の場合
            case L'%':                        //'%'の場合
                if(*start == L'('){                //startが'('であれば
                    start++;                    //startを'('の次に(endは')'の直前)進める
                    operand.Set(start, end);    //オペランド文字列(含括弧)の切り取り
                    if(!TryCalc(opeval, operand.Get()))    //再帰処理
                        return false;                    //処理不能であればエラー
                }
                else {
                    if(!end)                    //endが未設定であれば
                        end = ptr - 1;            //'*'、'/'、'%'演算子の直前に設定
                    operand.Set(start, end);    //オペランド文字列(含括弧)の切り取り
                    if(!TryParse(opeval, operand.Get()))    //整数値でない場合
                        return false;                        //処理不能であればエラー
                }
                if(opec == 0)                    //初期値の場合
                    sol = opeval;                //第1オペランドであるsolに代入する
                else if(opec == 1)                //'*'の場合
                    sol *= opeval;                //乗算、
                else if(opec == 2)                //'/'の場合
                    sol /= opeval;                //除算または
                else if(opec == 3)                //'%'の場合
                    sol %= opeval;                //剰余算を行う
                if(*ptr == L'*')                //新しいオペコードを設定
                    opec = 1;                    //'*'の設定
                else if(*ptr == L'/')
                    opec = 2;                    //'/'の設定
                else if(*ptr == L'%')
                    opec = 3;                    //'%'の設定
                ptr++;
                end = start = nullptr;            //切り取りポインターの初期化
                break;
            default:                        //'*'、'/'、'%'、'('以外の非空白文字
                if(!start)                        //startが未設定(nullptr)であれば
                    start = ptr;                //非空白文字の先頭に設定
                else if(start && end)            //start、end共に設定済で演算子以外の場合
                    return false;                //構文エラー
                if(*ptr == L'(') {                //括弧の場合
                    ptr = SkipBraces(ptr);        //括弧をスキップし、ポインターは')'の次を指す
                    if(!ptr)                    //括弧が閉じられていなければ(ptr == nullptr)
                        return false;            //構文エラー
                    end = ptr - 2;                //endに')'の前のアドレスを記録
                }
                else
                    ptr++;                        //ポインターを進める
                break;
            }
        }
        //ポインターを進めてヌル終端になった場合(ラップアップ処理)
        if(*ptr == L'\0') {
            if(*start == L'('){                //startが'('であれば
                start++;                    //startを'('の次に(endは')'の直前)進める
                operand.Set(start, end);    //オペランド文字列(含括弧)の切り取り
                if(!TryCalc(opeval, operand.Get()))    //再帰処理
                    return false;                    //処理不能であればエラー
            }
            else {
                if(!end)                    //endが未設定であれば
                    end = ptr - 1;            //'*'、'/'、'%'演算子の直前に設定
                operand.Set(start, end);    //オペランド文字列(含括弧)の切り取り
                if(!TryParse(opeval, operand.Get()))    //整数値でない場合
                    return false;                        //処理不能であればエラー
            }
            if(opec == 0)                    //初期値の場合
                sol = opeval;                //第1オペランドであるsolに代入する
            else if(opec == 1)                //'*'の場合
                sol *= opeval;                //乗算
            else if(opec == 2)                //'/'の場合
                sol /= opeval;                //除算または
            else if(opec == 3)                //'%'の場合
                sol %= opeval;                //剰余算を行う
        }
    }
    return true;
}

//////////////////////////////////////////
//文字列の整数化関数
//2、16または10進数文字列から整数値を返す
//先頭詞:2進数は0B||0b、16進数は0X||0x
//////////////////////////////////////////

bool CALC::TryParse(int& sol, const wchar_t* str) {

    sol = 0;                                    //solを初期化する
    int len = wcslen(str);                        //strの長さ-1(NULL文字非算入)
    if(!len)                                    //strが空文字列なら("")
        return false;                            //エラーとする
    ////////////////////////////////////////////////////////////////////////////////
    //int head = 0;                                //整数先頭アドレス(既定値は10進数)
    //int base = 10;                            //基数(底)(既定値は10進数)
    //wchar_t max = L'9';                        //エラー用最大値(既定値は10進数)
    //2進数はhead = 2, base = 2, max = L'1'とすればループ処理を共通化できる
    ////////////////////////////////////////////////////////////////////////////////
    //2進数(0Bまたは0b)文字列から整数値を返す

    if(str[0] == L'0' && (str[1] == L'B' || str[1] == L'b')) {
        for(int i = 2; i < len; i++) {
            if(str[i] < L'0' || str[i] > L'1')    //2進数以外の文字があればエラー
                return false;
            sol = sol * 2 + (str[i] - L'0');
        }
    }
    //16進数(0Xまたは0x)文字列から整数値を返す
    else if(str[0] == L'0' && (str[1] == L'X' || str[1] == L'x')) {
        for(int i = 2; i < len; i++) {
            if(str[i] < L'0' || (str[i] > L'9' && str[i] < L'A') || (str[i] > L'F' && str[i] <L'a') || str[i] > L'f')
                return false;                    //16進数以外の文字があればエラー
            if(str[i] >= L'0' && str[i] <= L'9')
                sol = sol * 16 + (str[i] - L'0');
            else if(str[i] >= L'A' && str[i] <= L'F')
                sol = sol * 16 + (str[i] - L'A' + 10);
            else
                sol = sol * 16 + (str[i] - L'a' + 10);
        }
    }
    //10進数文字列から整数値を返す
    else {
        for(int i = 0; i < len; i++) {
            if(str[i] < L'0' || str[i] > L'9')    //10進数以外の文字があればエラー
                return false;
            sol = sol * 10 + (str[i] - L'0');
        }
    }
    return true;
}

////////////////////////////////////////////
//括弧をスキップし、次のアドレスを返す
////////////////////////////////////////////

wchar_t* CALC::SkipBraces(wchar_t* str) {

    wchar_t* ptr = str;            //str文字列のpointer
    if(*ptr != L'(')            //文字列引数がL')'で始まらない場合
        return 0;                //エラー
    else {
        ++ptr;                    //ポインターを一つ進める
        while(*ptr != L'\0' &&    //文字列列が終了せず
                *ptr != L')') {            //L')'でもなければ
            if(*ptr == L'(')            //多重括弧は再帰的に処理し
                ptr = SkipBraces(ptr);    //括弧の次から進める
            else
                ptr++;                    //ptrを一つ進める
        }
        if(*ptr == L'\0')        //括弧が閉じられずに文字列が終了
            return nullptr;        //エラー
        return ++ptr;            //括弧の次のアドレスを返す(含む終端文字)
    }
}

 

如何だったでしょうか、C++によるCalc?ひょんな郷愁からやや久しく遠ざかっていたC++でプログラムを書き、また今まで経験したことのなかった(そして未だ原因が分からない)メモリーオーバーラップのトラブルシューティングをする、というExhaustingな時間を過ごすことが出来、「(単に)整数演算式の評価、演算プログラム」というちっぽけなプロジェクトでしたが、私にとって実りの多いプロジェクトでした。

 

前回まででCalcのバイプレーヤーを総て紹介いたしましたので、今回は最後にトリ、Calc.h(コンソール版-ウィンドウズ版のCalc_Class.hと同じ)を紹介いたします。

 

(1)CALCクラスは四則演算を行うインスタンスを作るクラスです。(CALCクラスのコードの最初の/*~*/コメントに仕様があります。)

 

(2)インスタンスを作って計算するには、

 

①単に「CALC calc;」として、後で「calc = "演算式";」「calc.TryCalc(answer);」としても、

②最初から「CALC calc("演算式");」とし、「calc.TryCalc(answer);」としても、

③「CALC calc;」として、後で「calc.TryCalc(answer, "演算式");」としても

 

良いです。なお、既に演算式1を設定していても「calc.TryCalc(answer, "演算式2");」とすれば演算式2が設定されて計算されます。

 

(3)アルゴリズムというと大げさですが、動作の概要を以下に記します。

 

①クラスの処理を行う主たるメソッド(関数等)は「主となる(加減算を行う)TryCalc」、「乗除剰余算を行うTryMulDiv」、「2、16、10進数文字列を整数に変換するTryParse」(注1)と、支援処理として括弧をスキップするSkipBraces関数と文字列の一部をコピーとして持つCPOSクラス注2)があります。

注1:これら関数の引数と戻り値は統一させ、C#のTryParseをまねて、戻り値はboolで「成功、不成功」を表し、成功した場合の演算値は参照渡しの変数sol(ution-解)に渡されます。

注2:元々はwchar_t配列を文字列として使うものでしたが、今も何故だかわからない不測のメモリー侵害(オーバーラップ書き込み)の問題からwstringクラスを使って書き直しました。

 

②↑で書いたように、引数として「演算式文字列」を与えると、それをInit関数により演算式で許容される文字だけ(wchar_t配列のポインターである)メンバー変数m_formulaに渡します。

 

③演算式文字列がある状態でTryCalcメソッド(関数)を実行すると、(基本構造はTryMulDivも同じですが)先ず第1オペランドで戻り値となるsol第2オペランドとなるCPOSインスタンスのoperand第2オペランドの整数値を表すopevalオペコードを表すopec注1)を初期化し、演算式文字列を指すptr、operandを使った文字列切り取り用startendを、夫々m_formula(注2)、nullptr(C++11から導入されたヌルポインター定数)で初期化します。

注1:初期値0は単にopevalsolに代入します。1以降はTryCalcTryMulDiv夫々で加減、乗除剰余演算子に値を割り振っています。

注2:演算式文字列を参照するptrは、TryCalc関数では(引数strが空文でなければ、それでm_formulaを初期化し)m_formulaを参照しますが、TryMulDivでは直接引数strを参照します。尚、ptr代入に型変換(wchar_t*)が与えられているのはstrが(const wchar_t*)型であるからです。

 

④演算式文字列の解釈はwhileループで行い、演算式文字列の最後(ヌル終端)迄続きます。又ループ処理の基本構造はptrが指す文字によるswitch文を使った分岐であり、各処理の最後にptrを進める為、ptrがヌル終端を指す場合の最終処理(ラップアップ処理)をswitch文直後に設けています。

 

TryCalcTryMulDivの役割分担は夫々「主演算関数で加減処理、構文エラー処理」と「副演算関数で乗除剰余演算処理、括弧内再帰処理)」を担当し、TryCalc → TryMulDiv → 括弧処理の順で呼び出し、解(戻り値)がTryCalc ← TryMulDiv ← 括弧処理の順で帰ってくることにより演算順位を定めています。

TryCalcでは括弧('('、')')や'*'、'/'、'%'を含む文字列を「そのまま丸ごと」TryMulDivにぶん投げ、TryMulDivは'*'、'/'、'%'を含む文字列は加減乗除剰余演算を行い、括弧文は'('、')'を外した演算式文字列をTryCalcに投げ返して(再帰処理)

います。

 

⑥最終的に整数値が得られる2、16、10進数文字列はTryParseにより整数値に変換され、↑で書かれた順に値が戻ってゆき、最終的にTryCalcの終わりまでくればtrueを返します。この際「解」は参照引数solに代入されます。

 

⑦途中でTryCalcで構文的におかしい、またはTryParseで整数に変換できない場合をエラー()とし、falseを返します。

:演算式文頭の単項式は'+'か'-'だけなので、'*'、'/'、'%'があればエラーとなります。文中ではオペランドが二つオペコード無しに連続した場合(最初その検出用にpost_opecフラグを使用しました)と、オペコードがオペランドを挟まずに連続した場合(最初その検出用にフラグを使用しましたが、後に"start == end"条件に切り替えました)エラーになります。又、括弧を扱う(再帰処理にしているので多重括弧にも対応しています)SkipBraces関数は'('があった場合相対する')'が無ければエラーを返します。

 

以上でCALCクラスの解説を終えますが、実際のコードは次回紹介します。又、CALCクラスのコードは、正直今まで(特にC++のnewdeleteで)経験したことのないメモリー割り当てのトラブルを始めとするいくつかの躓きがありましたので備忘として書き込んだコメントが多く、それだけで十分だと思いますが、補足はいつも通り解説:で付記します。

 

ps. 今回コードを入れたら字数超過になってアップできませんでした。プログラムの処理をコード無しで説明して分かりづらかったと思います。ゴメンナサイ。

 

今回は、前回紹介したECCSkelton用の5ファイル

 

 

を簡単に紹介します。

 

1.リソース関連ファイル

(1)Calc.rc

//解説:以下はBCCFormが作成するリソースファイルと、リソースのIDファイルです。

//-----------------------------------------
//             BCCForm Ver 2.41
//    An Easy Resource Editor for BCC
//  Copyright (c) February 2002 by ysama
//-----------------------------------------

#include    "ResCalc.h"

//----------------------------------
// ダイアログ (IDD_CALC)
//----------------------------------

IDD_CALC DIALOG DISCARDABLE 0, 0, 327, 138
EXSTYLE WS_EX_DLGMODALFRAME
STYLE WS_POPUP | WS_DLGFRAME | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | DS_SETFONT | DS_CENTER
CAPTION L"Calc"
FONT 8, L"MS 明朝"
{
 CONTROL L"整数の算術演算式を演算子(+, -, *, /, %)を使って入力してください。", IDC_LABEL, L"STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY | SS_CENTER, 6, 9, 312, 15
 CONTROL L"", IDC_EDIT, L"EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP | WS_VSCROLL | ES_WANTRETURN | ES_MULTILINE | ES_AUTOVSCROLL | ES_LEFT, 6, 27, 312, 57, WS_EX_CLIENTEDGE
 CONTROL L"", IDC_SOL, L"STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY | SS_SUNKEN | SS_RIGHT, 9, 87, 306, 15
 CONTROL L"計算", IDC_CALC, L"BUTTON", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON, 243, 108, 63, 18
 CONTROL L"終了", IDOK, L"BUTTON", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON, 18, 108, 63, 18
}
//解説:ダイアログ(IDD_CALC)を作り、そこに複数行用のエディットコントロール(IDC_EDIT)、答え表示用のラベル(IDC_SOL)、計算用ボタン( IDC_CALC)、終了ボタン(IDOK)とユーザー指示用ラベル(IDC_LABEL)を貼っています。

//--------------------------
// イメージ(IDI_ICON)
//--------------------------

IDI_ICON    ICON    DISCARDABLE    "Icon.ico"

 

(2)ResCalc.h

//-----------------------------------------

//             BCCForm Ver 2.41
//   Header File for Resource Script File
//   Copyright (c) February 2002 by ysama
//-----------------------------------------
//---------------------
//  ダイアログリソース
//---------------------

// ダイアログ IDD_CALC
#define    IDC_LABEL        100
#define    IDC_EDIT        101
#define    IDC_SOL            102
#define    IDC_CALC        103

//---------------------
//  イメージリソース
//---------------------

#define    IDI_ICON        200
//解説:IDOKはシステムで定義してあり、再定義は不要です。
 

2.プログラム関連ファイル

ユーザーが書き込む部分を茶色にしておきました。

 

(1)Calc.h

//////////////////////////////////////////
// Calc.h
// Copyright (c) 12/13/2024 by ECCSkelton
//////////////////////////////////////////
//ECCSkeltonのヘッダー-これに必要なヘッダーが入っている

#include    "ECCSkelton.h"
//リソースIDのヘッダー
#include    "ResCalc.h"
//CALCクラスのヘッダー(解説:これはCalcシリーズの最後に解説します。)
#include    "Calc_Class.h"

#define        MAXLEN    1024    //解説:演算式文字列音最大長さです。「もっと長く」という場合、ここを弄ってください。

/////////////////////////////////////////////////////////////////////
//CMyWndクラスをCDLGクラスから派生させ、メッセージ用の関数を宣言する
/////////////////////////////////////////////////////////////////////

class CMyWnd : public CDLG
{
private:
    WCHAR m_Formula[MAXLEN];        //計算式取得用(1,024 bites)

public:    //以下はコールバック関数マクロと関連している
    //2重起動防止用のMutex用ID名称
    CMyWnd(WCHAR* UName) : CDLG(UName) {}
    //メニュー項目、ダイアログコントロール関連(解説:今回はこの二つのボタンの未コードを書きます。)
    bool OnCalc();
    bool OnIdok();
    //ウィンドウメッセージ関連
    CMDTABLE        //OnCommand()関数宣言(解説:WM_COMMAND関連で、上記の「根っこ」です。)
    bool OnClose(WPARAM, LPARAM);
    bool OnDestroy(WPARAM, LPARAM);
};

////////////////////////////////////////////////////////////////////////
//派生させたCMyWndクラスのインスタンスとコールバック関数(マクロ)の作成
//主ウィンドウはダイアログと違い、コールバック関数は一つしか作れない
////////////////////////////////////////////////////////////////////////

CMyWnd Calc(L"Calc");    //ウィンドウクラスインスタンスの生成

BEGIN_CMDTABLE(CMyWnd)    //クラス名がCMyWndではない場合、クラス名、テーブルを適宜マニュアルで修正してください
    //メニュー項目、ダイアログコントロール関連
    ON(IDC_CALC, OnCalc())
    ON(IDOK, OnIdok())
END_CMDTABLE

 

(2)CalkcProc.h

ここがプログラムコードでユーザー処理の中心になるものです。ウィンドウズは割込み処理を基本としており、ウィンドウメッセージやコントロールによる割り込みを使って処理を行います。

 

//////////////////////////////////////////
// CalcProc.h
// Copyright (c) 12/13/2024 by ECCSkelton
//////////////////////////////////////////

/////////////////////////////////
//主ウィンドウCMyWndの関数の定義
//ウィンドウメッセージ関数
/////////////////////////////////

bool CMyWnd::OnClose(WPARAM wParam, LPARAM lParam) {

    //解説:OnCloseとOnDestroyはSkeltonWizardが自動作成します。
    if(MessageBox(m_hWnd, L"終了しますか", L"終了確認",
                    MB_YESNO | MB_ICONINFORMATION) == IDYES)
        //処理をするとDestroyWindow、PostQuitMessageが呼ばれる
        return TRUE;
    else
        //そうでなければウィンドウではDefWindowProc関数をreturn、ダイアログではreturn FALSEとなる。
        return FALSE;
}

//ダイアログベースの場合はこれが必要
bool CMyWnd::OnDestroy(WPARAM wPram, LPARAM lParam) {

    PostQuitMessage(0);
    return TRUE;
}

/////////////////////////////////
//主ウィンドウCMyWndの関数の定義
//メニュー項目、コントロール関数
/////////////////////////////////

bool CMyWnd::OnCalc() {

    //解説:以下はコンソール版のテストプログラムから移植しました。

    //エディットボックスの演算式文字列を取得する
    SendItemMsg(IDC_EDIT, WM_GETTEXT, MAXLEN - 1, (LPARAM)m_Formula);
    //CALCクラスインスタンスを作成し、演算を行う
    int result;        //整数の演算値が入る
    WCHAR solution[MAXLEN];

    //使い方1
    CALC calc;
    if(calc.TryCalc(result, m_Formula)) {

    //解説:コンソール版と同じです。

    //使い方2
    //CALC calc(m_Formula);
    //if(calc.TryCalc(result))
    //使い方3
    //CALC calc;
    //calc = m_Formula;
    //if(calc.TryCalc(result))

        CSTR sol(result);    //解説:CSTRはBCCSkelton、ECCSkeltonの文字列操作クラスです。
        SendItemMsg(IDC_SOL, WM_SETTEXT, 0, (LPARAM)sol.ToChar());
    }
    else
        MessageBox(m_hWnd,  L"演算式が正しくありません。", L"エラー", MB_OK | MB_ICONEXCLAMATION);

    return TRUE;

}

bool CMyWnd::OnIdok() {

    SendMsg(WM_CLOSE, 0, 0);
    return TRUE;

}

 

(3)Calc.cpp

プログラムとしては此処がメインになりますが、実際は何も書く必要はありません。(実際はもっと多くのものが描かれているのですが、必要なものを除き削除しています。)

 

//////////////////////////////////////////
// Calc.cpp
//Copyright (c) 12/13/2024 by ECCSkelton
//////////////////////////////////////////

#include    "Calc.h"
#include    "CalcProc.h"

////////////////
// WinMain関数(
解説:Unicode版ではwWinMainとなります。)
////////////////

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    LPWSTR lpCmdLine, int nCmdShow) {

    //モードレスダイアログを作成Create(hParent, DlgName, DlgProc);
    if(!Calc.Create(NULL, hInstance, L"IDD_CALC"))
        return 0L;

    //メッセージループに入る
    return Calc.Loop();
}

 

以上です。これらファイルはBCCForm and BCCSkeltonパッケージの「ECCSkelton」-「ECCSkeltonSample」-「14 Calc」に入っています。ご興味がある方はどうぞ。

 

前回で一応仕様通りのコードが書けましたので(また、現在まで新たなバグは出ていないので)、公開できますが、その前に無味乾燥なコンソールではなく、ユニコードで書くECCSkeltonによるダイアログベースのテストプログラムの作り方を(私も長らくやっていなかったので忘れてしまい、復習を兼ねて)紹介します。

 

1.先ずBCCFormで(Win32)リソースを決める

20年以上前にフリーのBCC用に開発したBCCFormでリソース(C#でいう奴よりも広く、ダイアログ等も含みます)を作ります。BCCFormはANSIべ-スですが気にしなくてよいです。序にプログラムアイコンも作りましょう。

 

 

 

2.作成したリソースを基にSkeltonWizardで必要ファイルを作成する

BCCFormで作成したリソースファイルCalc.rc(その他ResCalc.hもできます)を使って、SkeltonWizardを起動します。

今回はダイアログベースで作ります。ツールバーもステータスバーもコモンダイアログも使わず、

 

 

次々ページの割込み処理もボタン二つだけ

 

なので簡単です。

 

3.作成されたBCCSkeltonファイルをBCC2ECCでECC用にする

BCCFormとSkeltonWizardで作成された5つのプログラムファイル

 

Calc.rc

ResCalc.h

Calc.h

CalcProc.h

Calc.cpp

 

の他に、(昔使ってもらうことを想定したBCCDeveloper用の)

 

Calc.bdp

 

が作られるので、これをBcc2Ecc(ECCSkeltonのサンプルの中に入っています)に読み込ませ、変換します。

 

 

その結果、ECCSkelton用のファイルが出来上がります。

 

 

後はANSIベースのBCC用ファイルを削除し、"_ECC"を外してECC用のファイルとします。

 

 

4.最後にBatchGoodを使ってコンパイルします。

BatchGoodを起動し、Calc.cppとCalc.rcを読み込み、オプションの詳細ダイアログを開いて、「実行ファイル形式」で「Windowsアプリケーション(Unicode)」を選択し、「インクルードパス」にECCSkelton.hのあるフォールダーを選択します。

 

 

こうしてコンパイルすることにより、「がらんどう」のウィンドウプログラムが出来ます。

 

 

後は「計算」ボタンと「終了」ボタンの処理を書くだけです。(作成されたECCSkelton用コードは次回紹介します。)

 

久々にC++でプログラムを書きたくなったので始めたCalc。

 

プログラムイメージは直ぐにできたのですが、直ぐに古希を迎えた「脳体力」の限界を感じ始めます。

 

一応は書いてみたのですが、プログラムイメージにしっくりこない。「あーでもない、こーでもない」と試行錯誤を繰り返して、そうこうするうちに一応の形を見ますが、同時に

 

(1)演算式の文字列にU+0001Hという制御コードが混入し、

(2)文字列の最終にヌル終端(L'\0'-U+0000H)を置いていたのに、それが知らないうちに空白(U+0020H)に変わるという、

 

奇怪な現象

 

を経験します。自分でも解析し、プログラムコードに致命的欠陥がないので、Chat-GPT様にも相談しますが、原因が分からないまま、一応の対処療法を施しました。

 

あー、一安心

 

と思ったのもつかの間、(これは疑いもなく私のミスですが)「整数化できない入力ミス」によってスタックオーバーフローが生じ、プログラムが落ちるというバグを発見したことから、「ユーザー入力ミス対応が脆弱」であることが判明しました。しかし、これが

 

怪我の功名

 

でバグフィックス中に「newでメモリーに確保した文字列(wchar_t配列)のデータが知らない間に書き換えられる瞬間」を確認することが出来ました。

 

 

↑の出力画像の上の赤枠は括弧内の演算式文字列「'-' (45),' ' (32), '9' (57),'' (0)」(即ち"... - 9")で、"38035334"のメモリーアドレスにあります。この後括弧内の演算式(紫枠)の評価処理があり、戻ってきて'-'を読んだ後が"38035336"(wchar_tは1文字2バイト)のメモリーアドレスにある下の赤枠の文字列になりますが、「' ' (32), '9' (57), '' (1),'' (0)」とあるのがお判りでしょうか?

 

これはキャラクターコンタミだと思っていたU+0001Hコードであり、ここでハタと「キャラクターコンタミ」ではなく、

 

L'\0'がL' '(空白)に変わったように、L'\0'がL'\1'(と続くL'\0')に書き換えられた

 

のだ、ということが判りました。要すれば全ての奇怪現象の原因は知らない間のメモリー書き換えという

 

真実はひとつ

 

だった、と考えられ、且つ考えること合理性があるということです。

 

では、それはどこで発生したのか?と考えた時にそれは「クラス内クラスで定義したCPOSクラスの仕業」に違いない!

 

    ////////////////////////////////
    //クラス内クラス Part Of String
    //st - ed 間の文字列を保有し返す
    ////////////////////////////////

    class CPOS
    {
    private:
        wchar_t* m_partstr = nullptr;    //解説:切り取り文字列用ポインターです。(初期値はnullptr-ゼロ)
    public:
        CPOS() {    //解説:コンストラクターです。
            m_partstr = new wchar_t[1];    //解説:1文字(2バイト)の配列を作成(メモリーを確保)
            *m_partstr = L'\0';    //解説:そこにヌル終端を置く。
        }
        CPOS(wchar_t* st, wchar_t* ed) {    //解説:引数付きコンストラクターの場合はSet関数を呼ぶ。
            Set(st, ed);
        }
        ~CPOS() {    //解説:デストラクターです。
            delete [] m_partstr;    //解説:newで確保したメモリーはdeleteで確実に開放する。
        }
        void Set(wchar_t* st, wchar_t* ed) {    //解説:文字列の切り取り処理です。
            if(!m_partstr)    //解説:nullptrであれば、

                delete [] m_partstr;    //解説:既成の文字列を開放します。
            if(ed >= st) {    //引数順序st (<=) ed解説:使用はstが開始文字、edが終了文字です)
                int len = ed - st + 1;    //解説:文字列の長さです。
                m_partstr = new wchar_t[len + 1];    //解説:+1しているのはヌル終端分が必要だからです。
                wcsncpy(m_partstr, st, len);    //解説:開始文字から終了文字までコピーし、
                m_partstr[len] = L'\0';    //解説:最後にヌル終端を入れます。
            }
            else {            //引数順序間違い対応

                //解説:エラー対応としてstとedを間違えても大丈夫なように入れ替えています。)
                int len = st - ed + 1;
                m_partstr = new wchar_t[len + 1];
                wcsncpy(m_partstr, ed, len);
                m_partstr[len] = L'\0';
            }
        }
        wchar_t* Get() {    //解説:切り取った文字列を取得します。
            return m_partstr;
        }
    };

 

見た限りにおいて全く問題が無いように見えます

 

が、

 

試しに、このプログラムのnewする長さを「+1(2バイト)」増やし、ヌルを追加するようにした所、総ての

 

奇怪現象

 

が消えました。矢張り元々はm_formulaという配列をnewで確保し、そこから都度CPOSインスタンスで文字列を一部newでコピー文字列を作成して解釈処理を行っていましたが、このローカル変数用メモリー達が最後の所でオーバーラップ(上書き)していたということです。

 

この「ヌル二重化対応」で行こうと思いましたが、Chat-GPT様が「手動でポインタを操作してメモリを管理する方法から、標準ライブラリ(例: std::wstringstd::string)への移行を検討してください。これにより、ポインタの解放忘れやオーバーランといったメモリ関連のバグを大幅に削減できます。」というので完全に書き換えました。

 

    ////////////////////////////////
    //クラス内クラス Part Of String
    //st - ed 間の文字列を保有し返す
    ////////////////////////////////

    class CPOS
    {
    private:
        wstring m_partstr;    //解説:可変長のUnicocde(ワイド文字)用文字列クラス
    public:
        CPOS() : m_partstr(L"") {}
        CPOS(const wchar_t* st, const wchar_t* ed) {
            Set(st, ed);
        }
        void Set(const wchar_t* st, const wchar_t* ed) {
            if(ed >= st)    //引数順序st (<=) ed
                m_partstr = wstring(st, ed + 1);
            else            //引数順序間違い対応
                m_partstr = wstring(ed, st + 1);
        }
        const wchar_t* Get() const {
            return m_partstr.c_str();
        }
    };
 

矢張りChat-GPT様の言われる通り、wstringクラスを使うようになってメモリーオーバーラップ現象は消失しました。

 

イヤー、疲れた(><)

 

というのが本音ですが、当初の目的は達成したといえます。

 

ps. 一応「お詫び」で紹介したバグは全てフィクスしました。序にWindows版のテストプログラムも作りました。

こいつのよい所は、(医療費控除などで行う)卓上計算機が出来ない一部のキーミスを修正して長い加減算を再実行できることです。

何れ紹介しましょう。

 

一部の方は既にご覧になられていたと思いますが、昨日一旦Calcのコードを掲載しましたが、その後致命的なバグが発見され、取り急ぎブログを取り下げさせていただきました。

 

その時気が付いたバグとは、TryCalcが自分で整数化できないオペランドをTryMulDivに渡し、同様にTryMulDivは自分で整数化できないオペランドをTryCalcに渡す構造をとってしまった為に、「決して整数化できない文字列」(即ちユーザーエラー、例:「演算式に"abc"と入力したような場合」)は

 

「堂々巡り」からスタックオーバーフロー

 

となることです。

 

この問題は既に解決しておりますが、もう一つ「(オペランドが連続する「演算子抜け」エラーは検知できますが)オペランドが無くて、演算子が連続する

 

「オペランド抜け」エラー

 

例:「演算式に"-3 *  +  2"と入力したような場合」)」が未対応であったことが判りました。これに対しても(不承不承ながら)フラグ対応で解決しました。

 

が、

 

「これで一安心」と思ったのもつかの間、「(演算子は連続しませんが)演算子後、オペランドが来ない

 

「オペランド抜け Part II」エラー

 

例:「演算式に" 2 * 4 - 3 +"と入力したような場合」)」が未対応であることに気が付きました。

 

と、いうことで、

 

Calcのコード公表までもう少しお時間をください。

 

スミマセン...

 

さて、前回お話しした二つの大きな謎に対して、何度か見直し、プログラム的に見てどこも致命的におかしくはないと考えられたことから、前者は「何故U+0001Hという文字が混入するのか?」が分からず、後者は

そして私はその答えを持っていませんし、*ptrが「ヌル終端でありながら、ヌル終端ではない」ものを指しているという事実に、どのように対処すべきか想像もつきません。」という

 

なんと気弱な発言!

 

をしてしまいました。

 

しかし、こういう時は自分で考えてもドツボに嵌るので、人の意見を聞くことが肝要です。とういことで、今回も

 

ChatGPT様

 

にご登場いただき、貴重なアドバイスを頂きました。

 

1.前者の謎「何故かU+0001Hが演算式文字列に混入するようだ?」

原因として次のような見解を頂きました。

 

原因の可能性

  1. 入力ソースの問題

    • コピー&ペースト時の混入: 特定のアプリケーションや環境からテキストをコピー&ペーストした際に、制御文字が意図せず混入することがあります。
    • 不正なデータ: 入力ソース(ファイルや外部システム)が文字エンコーディングを正しく処理しておらず、制御文字が含まれている可能性があります。
  2. エンコーディングの問題

    • UTF-8/UTF-16エンコーディングの誤変換: 文字列が異なるエンコーディング間で変換される際に、U+0001のような制御文字が混入することがあります。これは、エンコード変換時のデコードエラーやデフォルトの置き換え処理によるものです。
    • BOM(Byte Order Mark)や他の特殊マーカー: テキストに含まれる特殊マーカー(例えばBOM)が解析時に誤ってU+0001などの文字として認識されることがあります。
  3. プログラムのバグ

    • 文字列操作の不具合: プログラム内での文字列操作中に、バッファやデータ処理ロジックのバグによって制御文字が挿入される場合があります。
    • トークナイザーの設計ミス: トークン化やパース処理中に、不正な制御文字が認識されるケース。
  4. 外部システムの干渉

    • ネットワーク通信: APIやネットワーク通信で送受信されたデータに、不要な制御文字が混入することがあります。
    • ファイルの破損: 入力ファイルが破損している場合や、不適切に編集された場合、制御文字が含まれることがあります。
  5. 悪意あるデータ

    • セキュリティ攻撃: 入力データを汚染する目的で、制御文字を挿入する攻撃(例: SQLインジェクション対策を回避するための特殊文字)が行われている可能性もあります。

思い当たるのは、プログラムコードを書くエディターを見たら、文字コードセットがUTF-8になっていたので、それをShift-JISに変えたことでしょうか?でも、それであれば制御文字の混入(Character Contamination)は他の演算式に起こってもおかしくないとは思いませんか?

 

要すれば、原因はよく分からない

 

ということです。

 

何れにしても、これに対してはキャラクターコンタミの対策としてキャラクターローンダリング(Character Laundering)を行うこととし、丁度スペースカット用のコードがあったのでそれとiswspace関数で認められる空白文字だけをコピーするようにしました。その結果、

 

 

U+0001(第6番目、他はヌル終端コードのU+0000H)というコードが検出され、それを除外した計算式で正しく答え(39)が得られるようになりました。

 

2.後者の謎「ポインターが指していたヌル終端が空白文字に置き換わったようだ?」

この問題については、より深刻な

 

 

であり、結構なやり取りをChat-GPTと行いました。特に重要なところを抜粋して紹介します。

 

(私)
もう一つの問題があります。演算解析は正しく行われ、答えもあっているのですが、関数を呼ぶ際に演算式文字列のヌル終端を指していたポインターが処理後に帰ってきた際に空白(U+0020H)を指していてループが継続し、エラー(戻り値false)になることです。この為、次のように、プログラムにモニタリング用のコンソール出力を入れ、動作を確認しました。


(Chat-GPT)
「問題の核心は、演算式処理後にポインタが期待した位置(ヌル終端)を指していないため、後続のループが継続してエラーを引き起こすことです。調査を行った箇所では、ポインタ操作に起因する予期しない動作が発生している可能性があります。

具体的な要因として考えられるのは以下の点です:

括弧スキップ後のポインタ位置の不整合:
SkipBraces関数が適切な位置を返していない可能性。
ptrが意図しない位置を指している。
ヌル終端に到達した後の処理ミス:
endまたはstartの設定が適切でなく、文字列処理が正しく終了しない。」


なにか、私のプログラミングミスのような書き方で、昔若い頃の「会社の上司が、よく見もせずに『よく見直してみなさい』と言う」経験を思い出しました。当然私は、当時も、今も自分の意見を主張します。

(私)
まだ終わっていません。これらを挿入して確認した該当出力は次のようになっていました。「(TC) ptr after SkipBraces:, *ptr = '' (0) at 38034048」および「final *ptr: (32) at 38034048.」これから明らかなとおり、最初はヌル終端で、後は空白となっており、そのメモリーアドレスは同一です。コードを見ても*ptrにアクセスしてはおらず、プログラムとは別の作用でU+0000HがU+0020Hに変更されたとしか考えられません。どう思われますか?


(Chat-GPT)
「ご提供いただいたデバッグ結果を考慮すると、*ptr の内容が U+0000 (ヌル文字) から U+0020 (空白文字) に変更されており、ポインタが指しているメモリアドレス自体は変わっていないという現象が確認されます。この場合、以下のような原因が考えられます。

1. 別のスレッドやプロセスによる干渉
プログラムが単一スレッドではなく、複数スレッドで動作している場合、別のスレッドが同じメモリを操作している可能性があります。また、他のプロセスや外部の要因(特に共有メモリ領域を扱っている場合)も、予期しない値の変更を引き起こす可能性があります。
確認ポイント
スレッドセーフであるか確認。
共有メモリを利用している場合、アクセス範囲を特定し、保護(ロックなど)を導入。

 

2. 未定義動作(Undefined Behavior)
プログラム内で未定義動作が発生している可能性があります。典型的な原因として以下が挙げられます:

メモリのダングリングポインタ(すでに解放されたメモリへのアクセス)。
ポインタが本来アクセスするべきでない範囲を指している。
バッファオーバーランやバッファアンダーランによる他メモリ領域の上書き。
確認ポイント
ptr がどのメモリを指しているか、具体的に特定。
アロケーションや解放の順序に矛盾がないかを確認。
valgrind や AddressSanitizer を利用して、メモリ関連のエラーをチェック。

 

3. ライブラリや外部コードの副作用
プログラム中で使用している関数(例えば、SkipBraces 内や他の呼び出し)で、ptr が間接的に操作されている可能性があります。たとえば、参照渡しやポインタ渡しによる副作用です。

確認ポイント
SkipBraces 内で直接的、または間接的に ptr の値やその指す内容を変更しているコードがないか確認。
プログラムの他部分で同じメモリ領域が操作されていないか調査。

 

4. コンパイラの最適化による副作用
高い最適化レベルでコンパイルした場合、変数の実際の値がプログラム中の予想と異なる振る舞いをすることがあります。特に、コンパイラが変数の再利用やメモリの最適化を行った結果、予期しない変更が発生する場合があります。
確認ポイント
コンパイルオプションを低最適化(-O0)に変更して再コンパイルし、同じ現象が起こるか確認。
コンパイラの警告を最大限有効にして(例:-Wall -Wextra)、潜在的な問題を検出。

 

5. OSやハードウェアの要因
ごく稀ですが、メモリ保護が無効化されている場合や、ハードウェア障害が原因でメモリが予期せず変更されることも考えられます。ただし、これは通常、最後の可能性として考慮します。
確認ポイント
同じコードを別の環境で実行して再現性があるか確認。
メモリ診断ツール(例:memtest86)を用いてハードウェアのエラーチェックを実行。」


飽くまでプログラムミスとする姿勢から、ややファクトを理解し、原因の範囲をプログラムミスからコンパイラーやOSまで広げてきました。

(私)
この問題の部分のコードは短いので最後にコピーします。

スイッチ文の"default:"は'+''-'等演算子以外の文字で振り分けされ、startが初期値のnullptrであればptrを記録し、それが'('であればSkipBracesで')'の次のアドレスにptrを進めます。その際にendは一つ前の')'を指すことになります。

この段階で*ptrがヌル終端を指していたことは既にお話しした通りであり、その結果プログラムは「if(*ptr==L'\0')」の条件式に入っていきます。endは既にnullptrではないので、次のTryParse関数を呼ぶ条件式でfalseとなり、次にTryMulDiv関数を呼んで括弧文の解釈処理に入ります。その結果48という値(opeval)が返され、演算式の解(sol)から控除されて正しい48という解になります。(前に書いた

 

(TC) final operand:( ( 3 * 4 ) * 3 + 12), opeval: 48 and sol: 96
final *ptr: (32)
-この段階で空白(U+0020H)を指している
(TC) final sol: 48-この段階でsolは正しく48になっている

 

を参照してください。)

この条件式を実行された段階で条件である(*ptr == L'\0')が真であり、その後のTryMulDiv関数から戻ってきた時の値も正常ですが、その段階の*ptrが空白文字になっているということです。

勿論TryParseやTryMulDivにはptrもそれを参照するような引数も与えていませんので*ptrがアクセスされるはずがありません。強いて考えるならばoperand.Set()というptrが差している文字列アドレスの文字列の一部コピーメソッドがstartとendで参照しているともいえるでしょうが、start、endはptrよりも不作為に前にある為、それらからptrを特定することもできません。どう思われますか?

(Chat-GPT)
「ご説明を読む限り、
非常に興味深い現象です。プログラムの構造や動作は論理的に正しく見えますが、それにもかかわらず *ptr の値が外部の要因によって変更されているようです。この状況を整理して考えてみましょう。

可能性のある原因
以下に考えられる原因を挙げ、それぞれの可能性を検討します。

1. メモリの上書き(Heap Corruption)
operand.Set() など、ポインタやバッファに関連する操作が間接的に ptr の指すメモリを上書きしている可能性があります。
特に、operand.Set() が内部でバッファの操作を行う場合、その影響で ptr の指すメモリが書き換えられた可能性があります。
確認方法
operand.Set() の実装を確認し、その中で ptr が指しているアドレスと同じメモリ範囲を操作していないか調べる。
操作範囲が重複している場合、デバッグツール(例:valgrind や AddressSanitizer)で詳細なメモリの動きを追跡する。

 

2. コンパイラの最適化による影響
高度な最適化(例:-O2 や -O3)が有効になっている場合、コンパイラが ptr の内容やメモリ操作を想定外の方法で最適化した可能性があります。
例えば、コンパイラが *ptr の値をキャッシュしない場合や、条件分岐を省略する最適化を行った場合、値の変更が発生することがあります。
確認方法
最適化を無効化して(例:-O0)、再コンパイルして同じ現象が発生するか確認する。

 

3. スレッドや外部ライブラリの影響
複数スレッドが関与している場合、同じメモリアドレスを異なるスレッドが変更している可能性があります。
また、外部ライブラリが内部で ptr の指すメモリに干渉している可能性も考えられます。
確認方法
単一スレッド環境で実行し、現象が再現するか確認する。
外部ライブラリの使用箇所を調査し、メモリ操作を行っていないか確認する。

 

4. 未定義動作(Undefined Behavior)
プログラム内でポインタ操作やメモリ管理に関する不整合があり、未定義動作が発生している可能性があります。
例えば、start や end の設定が不正確な場合、operand.Set() が意図しない範囲を操作していることも考えられます。
確認方法
start や end の設定値が適切かどうか、ログを追加して確認する。
境界チェックやエラーハンドリングを追加して、予期しないメモリ操作を防ぐ。」


(私)
参考までに文字列の一部コピークラスであるCPOSのコードを載せます。このクラスのSetメソッドではオリジナルのstr引数のヌル終端を書き換える動作は無いようですが。


(Chat-GPT)
引数とm_partstrについて
おっしゃる通り、m_partstr は new によって明示的にメモリを確保しているため、str(引数として渡される文字列)とは通常別のメモリ領域を指します。もしコンパイラやランタイムにバグがない場合、これらがオーバーラップする可能性は非常に低いと考えられます。
そのため、Set メソッド内の操作が直接問題を引き起こす可能性は低いように見えますが、間接的な問題(ポインタが誤った範囲を指しているなど)については引き続き調査が必要です。」

 

矢張り、若い時の会社の上司の対応に似ています。「引き続き調査が必要です。」は、

 

お前がもっと調べて原因を特定し、俺に報告しろ

 

ってことですよね?プログラムコードも示し、問題点と不明点を示してもこの結論(なんで俺がコンパイラーやOSの動作まで調査するんだぃ!プンプン!)では、いい加減

 

使えねーな

 

と考えざるを得ず、

 

結局、原因は全く分からない

 

のですが、仕方がないので、

 

原因不明のまま、除霊を行う

 

ことにしました。えっ、どうするのか?って?

 

if(*ptr == L'\0') {        //ポインターがヌル終端に来たならば

 

(処理)

 

    *ptr = L'\0';            //ポインターが指すメモリーにヌル終端を代入する

}

 

え"~、そんな馬鹿な!

 

とお感じでしょう。そう、「AがBなら、AにBを代入する」ということですので、全く論理的に不要無用ですよね?

 

でも論理的に無害だし、原因が特定できない問題を解決できる

 

のであれば、

 

最高、最良のSolution!

 

ではないでしょうか?

 

悪霊退散!

(問題となった演算式全てが正しく回答されている。)

 

ps. 既にCalcクラスのテストプログラムコードは紹介しているので、次回は最後にCalcそれ自体のコードを紹介しましょう。(尚、現在は入力文字コードセットをShift-JISに戻しているので、"SetConsoleCP(CP_UTF8);   //入力コードページをUTF-8に設定")はコメントアウトしています。

 

前回、Clac開発に関わる悪戦苦闘の記を書きましたが、赤字追記で書いたCalcの動作の「」についてもう少し詳しく考える叩き台をこしらえてみました。

 

先ず、合理的に考えてみてCalcに与える算術式のスペースを増やしたから動作が変わる、というのは理解しがたい所です。加減算を扱う関数(メソッド)であるTryCalc()(と呼んでいます)や乗除剰余算を扱うTryMulDiv()関数(メソッド)で使っているスペーススキップは、

 

    //ループ処理
    while(*ptr != L'\0') {                    //ヌル終端に至るまで継続
        //空白処理
        if(iswspace(*ptr)) {
            if(start && !end)                //startは設定され、endは設定されていない場合
                end = ptr - 1;                //endを空白文字直前に設定(解説:startとendのサブポインターでオペランドを挟む)
            ptr++;                            //空白文字をスキップ
        }
        //ポインターの指す文字により処理を振り分ける
        else {
        ・

        ・

        ・

 

こんな感じになっており、そもそも空白の数で動作が変わる筈がないのです。

 

しかし、思い込みは禁物、常にfactsを直視して、evidenceを固めることが肝要であるが原則であることは変わりません。

 

ということで、

 

Visual StudioのようなDebug環境のない私は、プログラム中にモニタリングコードを挿入して確認します。

小さいですが、これは左から

(A)「最初のカッコ内の2の前の空白2つ」((  2 * 9 +  6) * 4 - ( ( 3 * 4 ) * 3 + 12) - 9 の場合)-「演算式が正しく有りません。

(B)「同一つ」(( 2 * 9 +  6) * 4 - ( ( 3 * 4 ) * 3 + 12) - 9 の場合)-「演算式の答え:39

(C)「同なし」((2 * 9 +  6) * 4 - ( ( 3 * 4 ) * 3 + 12) - 9 の場合)-「演算式の答え:39

の実行プロセスの流れを表しております。そしてそれは終了直前まで全く同一であり、最後の所で↓の様に差異が生じています。

 

(  2 * 9 +  6) * 4 - ( ( 3 * 4 ) * 3 + 12) - 9 の場合
(略)
(TMD)start at null terminal: ' ( 3 * 4 ) * 3 + 12)'
(TMD)opeval, sol: 48, 48
(TC)sol: 96, opeval48
operand(TC): 9

TryMulDiv Loop
(TMD)start, ptr: 9,
(TMD)start, ptr: 9,
(TMD)start at null terminal: '9'
(TMD)final operand: 9

演算式が正しく有りません。

( 2 * 9 +  6) * 4 - ( ( 3 * 4 ) * 3 + 12) - 9 の場合
(略)
(TMD)start at null terminal: ' ( 3 * 4 ) * 3 + 12)'
(TMD)opeval, sol: 48, 48
(TC)sol: 96, opeval48
operand(TC): 9

(TC)sol: 48, opeval9
演算式の答え:39    (解説:前回は暴走しました。末尾「追記」参照のこと。)

(2 * 9 +  6) * 4 - ( ( 3 * 4 ) * 3 + 12) - 9 の場合
(略)
(TMD)start at null terminal: ' ( 3 * 4 ) * 3 + 12)'
(TMD)opeval, sol: 48, 48
(TC)sol: 96, opeval48
operand(TC): 9

(TC)sol: 48, opeval9
演算式の答え:39

赤字がエラー発生時の動作です。3例とも、TryCalc関数の「-(減算)」演算処理のオペランド"9"を文字列として捉えている(operand(TC): 9の部分)のですが、何故か加減算処理のTryCalcでは文字列の整数化(TryParse関数)が出来ず(エラー)、プログラム通りTryMulDivに処理を移すのですが、当然TryMulDivは減算処理が出来ないので構文エラーになるという流れです。成功している2例は同じオペランド"9"の文字列(operand(TC): 9の部分)をTryParse関数で整数化できているのでオペランドの値(opeval)「9」を解(sol)の値「48」から減算して(sol: 48, opeval9)、解の「39」を出しています。

面白いのは、この"- 9"部分を誤って入力し忘れ、偶然「適法」算術式である↓を入力してしまった時、それまでの解(96)から括弧内の値(48)を引くところで、そのように演算結果が出ているのですが(
sol: 96, opeval48)結果は演算式構文のエラーになっています。

(2 * 9 +  6) * 4 - ( ( 3 * 4 ) * 3 + 12) の場合
(略)
(TMD)start at null terminal: ' ( 3 * 4 ) * 3 + 12)'
(TMD)opeval, sol: 48, 48

(TC)sol: 96, opeval48
演算式が正しく有りません。

 

この結果だけを見ると、エラーの原因は↑の3つは文字列の整数化(TryParse)処理のエラーの筈ですが、"9"を指していて何故整数化できないのか(空白の違いだけで、現実に同じ式でできているわけであり)よくわかりません。

 

最後の括弧で終了する例()についてプログラムを追うと、以下の赤文字列の形で進み、矢張りTryParse処理のエラーを起こしているのではないかと考えられます。

:別途括弧()で終わる式を与えても正しく処理されます。

 

(前略-以下エラー部分)

            default:                        //'+'、'-'、'*'、'/'、'%'以外の非空白文字
                if(!start)                        //startが未設定(nullptr)であれば
                    start = ptr;                //非空白文字の先頭に設定
                else if(start == end)            //'*'、'/'、'%'があった場合の次の非空白文字
                    end = nullptr;                //オペランド未設定(end == nullptr)に戻す
                else if(start && end)            //start、end共に設定済で演算子以外の場合
                    return false;                //構文エラー
                if(*ptr == L'(') {                //括弧の場合
                    ptr = SkipBraces(ptr);        //括弧をスキップし、ポインターは')'の次を指す
                    if(!ptr)                    //括弧が閉じられていなければ(ptr == nullptr)
                        return false;            //構文エラー
                }
                else
                    ptr++;                        
//ポインターを進める
                break;

        }
        //ポインターを進めてヌル終端になった場合(+-のラップアップ処理)
        if(*ptr == L'\0') {
            if(!end)                        //endが未設定であれば
                end = ptr - 1;                //ヌル終端の直前に設定
            operand.Set(start, end);        //文字列切り取り
            if(!TryParse(opeval, operand.Get()) &&    //整数値でない場合且つ
                !TryMulDiv(opeval, operand.Get()))    //乗除演算が不可能
                return false;                        //エラー

wcout << "operand : opeval - " << operand.Get() << " : " << opeval << endl;

            if(opec == 0)                    //初期値の場合
                sol = opeval;                //第1オペランドであるsolに代入する
            else if(opec == 1)                //'+'の場合
                sol += opeval;                //加算または
            else if(opec == 2)                //'-'の場合
                sol -= opeval;                //減算を行う

wcout << "sol : opeval - " << sol << " : " << opeval << endl;

        }
 

追記

 

因みに前回「暴走(延々とスクロールして終了しない)」したという空白一つの"( 2 * 9 +  6) * 4 - ( ( 3 * 4 ) * 3 + 12) - 9"の再現実験を、前回のプログラムで行ったところ、

 

 

再現に成功しましたが、その際に気になったのが、入力式は正しく表示されていますが、確認表示では文字化けしている点です。(「確認:( 2 * 9 + ツ」と読めます。)

 

またまたUnicodeの呪い

 

のような嫌な感じがします。何れにしても更に調査を行ってゆく所存です。

 

追々記

↑の後、プログラムコードを再確認して、何ら不合理なところはないと思われたので、「文字列の整数化(TryParse)処理のエラー」の追跡を行いました。

 

先ず、これがエラーを起こして「暴走(延々とスクロールして終了しない)」しているプログラムの最後のエラー部分です。

最後のオペランドのstartポインターは'9'を指していて正しいです。又endポインターもプログラム通り0となっており、この後endに「ヌル終端の一つ手前」、即ち'9'のアドレスを入れ、文字列→整数変換用のTryParse関数を呼びます。因みにopecというのはオペコードで'-'は2で表されるので、これも、そして全てプログラム通りとなっています。

 

では、何故エラーが出るのか?

 

今度はTryParse関数にわなを仕掛け、どの文字でエラーが出ているかを確認しました。

先の推察通り、TryParse関数はfalse(即ち0、trueは1です。↓の正常終了出力画像参照)となり、その際にエラーとなった文字がU+0001であることが判ります。

TryParse(opeval, operand.Get(): Errorneous character:   (1)

0

の部分を見てください。これは"TryParse(opeval, operand.Get()): 0"(整数変換に失敗してfalse(0)を返した)という出力の間に"Errorneous character:  (1)"(エラーとなった文字はU+0001)という出力が割り込んだものです。

 

対比の為に、↓に正常終了したプログラムを載せます。正しく48からオペランド'9'がTryParse関数で9となり、opec(2、即ち'-')通りに減算され、48 - 9 = 39となっています。整数化関数の実行結果も

TryParse(opeval, operand.Get()): 1

となっていますね。(正常処理されたので"Errorneous character:  ()"は出力されません。)

 

何故U+0001という文字が混入するのか、は未だに分かりません。調べた所、

 

U+0001 Unicode文字
「「(u+0001)という文字」と言えば、コンピューターの世界では制御文字と呼ばれる特殊な文字です。通常のテキストには表示されず、プログラムの実行や通信などに使われます。 しかし、このような制御文字が使われることで、様々な表現が可能となります。例えば、テキストの中に絵文字や特殊な文字を挿入することができます。また、改行やタブ、フォントの変更なども制御文字を使うことで実現できます。 また、''はコードの中でも重要な役割を果たします。ASCIIコードでは、''は1という数字を表しています。プログラムの中で数値を扱う場合には、''を使って変数に値を代入することができます。 さらに、''はセキュリティにも関係しています。コンピューターウイルスなどの悪意のあるプログラムでは、''を使ってシステムに悪影響を与えることがあります。そのため、セキュリティ対策の一環として制御文字の使用を制限する場合があります。 ''という文字は、コンピューターの世界で非常に重要な役割を果たしていることがわかります。一見何の意味もなさそうな文字でも、正しく使うことで様々な表現や操作が可能となります。しかし、誤った使い方をすると大きな被害を引き起こす可能性もあるため、注意が必要です。」

出典:https://0g0.org/unicode/0001/

見出しの始まり
「記号の意味:見出しの始まり. 基本ラテン文字.
シンボル「見出しの始まり」は「基本ラテン文字」ブロックの「C0コントロール」サブブロックに含まれており、1993年にUnicodeバージョン1.1の一部として承認されました。」

出典:https://symbl.cc/jp/0001/

 

ということで、制御コードのようです。テストでコピペで貼るからなのか、文字数やその内容で自動的に挿入されるのか、取り敢えず

です。

 

ps. ↑のテストで使った演算式の最後の"- 9"を外すと、適法な式であるにもかかわらず、演算式が正しくない(TryCalc関数がfalseを返す)となる理由も一緒に確認してみました。

先ず、"Errorneous character:  ()"が出力されていないので、原因はTryParse関数ではないことが判ります。又、TryCalc関数が、演算式文字列のポインターがヌル終端を検知した時に出力される"(TC)final start: 12, (int)end: 0, opec: 1"があるので、括弧部分("( 3 * 4 ) * 3 + 12")は最後まで処理され、最後の最後の処理であるヌル終端時処理終了時の"(TC)sol : opeval - 48 : 48"が表示されており、「96 - 48 = 48」という演算を正しく終了した後にエラーを出していることが判ります。これは(ちょっと理解しがたいことですが)論理的には、

 

    //ループ処理
    while(*ptr != L'\0') {                    //ヌル終端に至るまで継続

 ・

 ・

 ・

        if(*ptr == L'\0') {    //解説:↑で書いたヌル終端時処理
 ・

 ・

 ・

wcout << "(TC)sol : opeval - " << sol << " : " << opeval << endl;    //解説:↑に書いたデバッグ用の最後の最後の処理
        }
    }

    return true;    //解説:whileループを脱出できなかったので実行されない。(falseが返されると「演算式が正しくありません」と表示される)

演算式文字列のポインターを使ったwhileループ、で最後のヌル終端を条件とするif条件式を実行しておきながら、whileループを抜け出せずに処理を継続してエラーになった、としか考えられません。
こっちの方がより深刻な

 

 

です。(そして私はその答えを持っていませんし、*ptrが「ヌル終端でありながら、ヌル終端ではない」ものを指しているという事実に、どのように対処すべきか想像もつきません。)