平成27年11月30日
汎用の高速シリアル通信機能(22) C++のハマリ所(2) 構造体
2.構造体
構造体の取り扱いはC言語に比べて非常に扱い難くなっています。
特にvolatile修飾された構造体の取り扱いは非常に限定的な使い方しか許してもらえません。
組み込みではvolatile修飾は頻繁に使用されるので、この制約は非常に不便です。
以下に具体的な例を示します。下記はC言語ではエラーなく動作しますが、C++ではコメントのように多くがコンパイルエラーとなります。
typedef struct TEST_t {
uint16_t ms;
uint16_t us;
} TEST_t;
TEST_t s1 = {0, 1};;
TEST_t s2;
volatile TEST_t v1 = {1, 2};
volatile TEST_t v2;
s2 = s1; //OK volatile修飾なし同士の代入
v2 = v1; //コンパイルエラー volatile修飾あり同士の代入
s1 = v1; //コンパイルエラー volatile修飾ありからvolatile修飾なしへの代入
v2 = s2; //コンパイルエラー volatile修飾なしからvolatile修飾ありへの代入
このようにvolatile修飾された構造体はvolatile修飾のない構造体のみならず、自分と同じvolatile修飾のある構造体にすら代入出来ません。
これは構造体にデフォルト定義される代入式(operator=でオーバーライトできます)がvolatile修飾された自身の構造体を異なる型として認識するために代入が成立しないためのようです。
同じことを組み込み型の変数に対して行った場合の結果は次のようになります。C, C++共に正しく動作します。この違いが混乱の元になります。
int a, b;
volatile int c, d;
a = b; //OK
c = d; //OK
a = c; //OK
c = a; //OK
構造体の定義部に代入式を追加で登録する方法もありますが、先日説明した構造体のPOD型に関する制約が問題になります。C言語の範疇を超えてC++で追加された機能を使うことになります。従って、この方法は使いません。
では、どうするか。
一つ目の方法としては、volatile修飾を構造体に対して行うことはせず、構造体のメンバーに対して行います。
typedef struct TEST_t {
volatile uint16_t ms; //構造体メンバーをvolatile修飾する
volatile uint16_t us;
} TEST_t;
この方法の欠点は、この構造体のみを使用する限り本来はvolatile修飾が必要ない変数に対してもvolatile修飾がされたままになります。volatile修飾があることのデメリットは最適化を妨げるだけなので実害はさほど多くないのですが、プログラムを読んで理解する上では非常に目障りに写ります。
volatile修飾が必要な場面はIOポートの入力から入ってくる変数と割り込み処理関数内で値を変更する変数に対してです。従って、これらの変数には上記のようにメンバー変数をvoklatile修飾した構造体を使用し、volatile修飾が必要ない状態にするときは、別の変数や構造体にデータをコピーすることで行います。以下のようになります。
typedef struct TEST_t { //データ更新はCPUの動作に従う(割り込み処理やIOで更新されない)
uint16_t ms;
uint16_t us;
} TEST_t;
typedef struct VTEST_t { //CPUの動作とは無関係にデータ内容が更新される可能性がある
volatile uint16_t ms;
volatile uint16_t us;
} VTEST_t;
//データをvolatileでない状態にするためにコピーを行う。
//コピーの途中で割り込みによるデータの更新が起きると変数の信頼性が無くなるので割り込みを禁止する必要がある。
void copyTEST_tObj(TEST_t& dist, const VTEST_t& src)
{
uint8_t intFlag = GIDI(); //現在の割り込みフラグの状態を返す。同時に割り込みを禁止にする。
dist.ms = src.ms; //dist = src;としていないことに注意。異なる構造体なので一括代入はできない。
dist.us = src.us;
RI(); //割り込みを禁止した前の状態に戻す。
}
volatileな変数とそうでない変数で構造体が分かれるので、その定義分だけ面倒なコード記述が必要になるのが欠点です。
二つ目の方法は、構造体はPOD型のままで無理やりにコピーします。
元ネタはこちらです。
volatile TEST_t とTEST_t は従来どおりに使用しますが、データのコピーを"="による代入ではなく、専用の関数で行います。テンプレート関数なので、これ一つで大半の構造体をコピー出来る点がメリットです。
template <class T>
inline void copyVolatileObj(volatile T& dest, const volatile T& src)
{
const_cast<T&>(dest) = const_cast<const T&>(src);
}
volatile型が関係するデータコピーを"="による代入で記述できない不自然さは残りますが、余計な構造体を定義する必要がない分、この記述方法の方が違和感が少ない気がします。
次はテンプレートについてですが、日を改めます。