LoginSignup
17

More than 1 year has passed since last update.

キャスト演算子を理解する

Last updated at Posted at 2020-02-28

はじめに

C言語のキャストに関する仕様について、諸々ちょっと調べてみました。

なお、本記事内のソースコードおよびエラーメッセージは以下のgcc(コンパイル時は-Wall -Wextraを付加)によりコンパイルしたものです。

$ gcc --version
gcc (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0
Copyright (C) 2017 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.

以降の点付き数字で書いてある記述(x.x.xx.x.x-xのようなもの)はC11規格のdraft(N1570)内番号に対応します。

キャスト演算子(cast operator)について

「型変換のうちキャスト演算子を用いて記述されるもの」を指してキャストと呼びます1。すなわち以下のようなものを指します。

サンプル
int *num = (int *)malloc(sizeof(int));

malloc()の戻り値はvoid *型ですが、これをint *型へと変換している2()キャスト演算子(cast operator)と呼びます。本稿ではこのキャスト演算子についての記載をメインにします。

キャスト演算子は(型名)式の形式になります。キャスト演算子には規格上以下のような規定(6.5.4)となっています。

  • 制約(Constraints)
    • 型名がvoid型でない場合、修飾された、あるいは修飾されないスカラ型(後述)であり、かつオペランド(キャスト対象の式)もスカラ型でなければならない。
    • ポインタを含む型変換は、6.5.16.1に規定されているものを除き、明示的なキャストで指定されなければならない。3
    • ポインタ型と浮動小数点数型との間の型変換を行ってはいけない。
  • 意味規則(Semantics)
    • 丸括弧つきで式の前に指定された型名によって、式の値は指定された型の値へと変換される。この構造をキャスト4と呼ぶ。変換が行われないキャストは、型や式の値に何の影響も及ぼさない。
    • 式の値が、キャスト(6.3.1.8)によって指定された型が要求する値よりも大きい範囲あるいは精度である場合、例え式の型が指定した型と同じであっても、そのキャストは型変換を明示し、いかなる超過した分の範囲や精度をも削除する。

要するにどういうこと? という点については後述します。

C言語における型

先ほど出てきた「スカラ型」という言葉には馴染みがないかもしれません。そもそもC言語における型の分類はどの程度の種類があるのでしょうか。どうやら以下のような分類のようです(6.2.5, const等による修飾型は除く)。

  • オブジェクト型(object type)
    • 完全型(complete type)
      • 基本型(basic type)
        • 算術型(arithmetic type)
          • 実数型(real type)
            • 整数型(integer type)
              • char
              • 符号付き整数型(signed integer type)
                • 標準符号付き整数型(standard signed integer type) signed char, short int, int, long int, long long int
                • 拡張符号付き整数型(extended signed integer type) 処理系定義の整数型を各処理系で作ってよい
              • 符号なし整数型(unsigned integer type)
                • 標準符号なし整数型(standard unsigned integer type) _Bool, unsigned char, unsigned short int, unsigned int, unsigned long int, unsigned long long int
                • 拡張符号なし整数型(extended unsigned integer type) 処理系定義の整数型を各処理系で作ってよい
              • 列挙型(enumeration type): enum 型名
            • 実数浮動小数点数型(real floating type) float, double, long double
          • 虚数型(complex type) float _Complex, double _Complex, long double _Complex
    • 不完全型(imcomplete type)
      • void
      • 配列型(array type)のうち、サイズ不明のもの
      • 構造体型(structure type), 共用体型(union type)のうち、サイズ不明のメンバを含むもの
    • 派生型(derived type)
      • 配列型(array type)5
      • 構造体型(structure type): struct 型名
      • 共用体型(union type): union 型名
      • 関数型(function type)6
      • ポインタ型(pointer type): 指示対象の型名 *7
      • アトミック型(atomic type): _Atomic ベースの型名8

ひとまず、基本の分類はこのくらいにしておきましょう。ただ、ここに「スカラ型」は存在しません。すなわち、別の分類がいくつかあるわけです。

  • 文字型(character type)
    • char
    • signed char
    • unsigned char
  • スカラ型(scalar type)
    • 算術型(arithmetic type)
    • ポインタ型(pointer type)
  • アグリゲート型(aggregate type)
    • 配列型(array type)
    • 構造体型(structure type)

スカラ型はここに現れます。それでは、キャスト演算子が許すキャストはどのようなものなのでしょうか。

キャスト演算子の仕様

すでに記載したキャスト演算子の仕様は以下の通りです。

  • 任意の型⇒voidへの変換を認める。
  • スカラ型⇔スカラ型の変換を認める。
    • ただし、浮動小数点数型⇔ポインタ型は禁止。

すなわち、以下のような感じです。

キャストいろいろ
int main(void)
{
    char c = 0;
    struct { int mi; char mc; } st = {0};

    // 規格適合(ただしwarningが出る場合あり)
    int i = (int)c;
    long l = (long)i;
    double d = (double)l;
    void *vp = (void *)i;           // warning: cast to pointer from integer of different size [-Wint-to-pointer-cast]
    long long ll = (long long)d;
    i = (int)ll;
    c = (char)vp;                   // warning: cast from pointer to integer of different size [-Wpointer-to-int-cast]
    (void)st;

    // 規格違反
    // float f = (float)vp;         // error: pointer value used where a floating point value was expected
    // vp = (void *)f;              // error: cannot convert to a pointer type
    // ll = (long long)st;          // error: aggregate value used where an integer was expected

    return 0;
}

まあ、ここまでは大して問題ないでしょう。規格に適合している中で整数型⇔ポインタ型のキャストなんかは警告されていますが、まぁそもそもこんなことをしなければならない状況もさほど多くはないでしょうから、そうそう拝む機会もないかと思います。

ポインタ型キャストとstrict aliasing rules

キャストについては以上のみかと思いきや、これの他に、一般にstrict aliasing rulesと呼ばれているルールが存在します。これは、以下のようなルールとなっています(6.5-7、拙訳)。

オブジェクトは以下のいずれかの型をもつlvalue9によってのみ、その保存された値にアクセスされなければならない。
・そのオブジェクトの有効型10と互換性のある型
・そのオブジェクトの有効型と互換性のある型の修飾型
・そのオブジェクトの有効型に対応する符号付き、または符号なし型
・そのオブジェクトの有効型に対応する符号付き、または符号なし型の修飾型
・上記の型のいずれかをメンバに含むアグリゲート型、または共用体型(再帰的にそのようなアグリゲート型メンバや共用体型メンバをもつものを含む)
・文字型

ここでいう「アクセス」とは、最初に型を指定されて宣言されたオブジェクトに対して、ある変数を介してその中のデータを読み取る/データに書き込むこと11全般を指します。

aliasingというのは、「別名をつける」という意味ですが、ここでは「ある識別子に対して別の名前の変数からアクセスできるようにすること」、すなわち C言語においては「ポインタ型の変数に既存のオブジェクトのアドレスを代入して、ポインタを介して元の変数に読み書きすること」 と言ってよいでしょう。以下に例を示しますが、これは厳密にはキャスト演算子を使う際の規定ではなくキャスト後のポインタ変数による間接アクセスが規格合致であるかどうかの問題となりますが、ともあれよくキャスト周りでは引っかかる部分です。

aliasingを用いる例
    int i = 0;
    int *pi = &i;

    // aliasを用いたオブジェクトへのアクセス
    printf("%d\n", *pi);

以上を見たところで、int変数を例にとり、上記のstrict aliasingを遵守したポインタ型キャストを見てみましょう。間違っていたらご指摘下さい。

strict_aliasing.c
#include <stdio.h>

// aliasingによるアクセスを行うマクロ
#define ALIASING_ACCESS(T, obj, fmt) \
    do { \
        T *palias = (T *)&obj; \
        printf(fmt "\n", *palias); \
    } while (0)

#define ALIASING_ACCESS_AGGREGATE(T, obj, fmt) \
    do { \
        T *palias = (T *)&obj; \
        printf(fmt "\n", palias->member); \
    } while (0)

struct st {int member;};

int main(void)
{
    // intで宣言されたオブジェクトを作成
    int i = 0;

    // strict aliasing遵守
    ALIASING_ACCESS(int, i, "%d");                      // 有効型
    ALIASING_ACCESS(const int, i, "%d");                // 有効型の修飾型
    ALIASING_ACCESS(unsigned int, i, "%u");             // 有効型の符号なし型
    ALIASING_ACCESS(volatile unsigned int, i, "%u");    // 有効型の符号なし修飾型
    ALIASING_ACCESS_AGGREGATE(struct st, i, "%d");      // メンバに互換を含むアグリゲート型
    ALIASING_ACCESS(char, i, "%c");                     // 文字型

    return 0;
}

逆に、このstrict aliasing rulesはある型で指定されたオブジェクトへのアクセス方法を規定しているものでしかないため、いったん汎用ポインタであるvoid *やあらゆる型の互換型としての文字型へのポインタを介したとしても規格違反となります(もちろんキャスト演算子自体の使い方としては問題ありません)。例えば、以下のコードはコンパイル時は何の警告もなかった12のですが、実行する度に違う値を出力するバイナリを生成しました。

汎用ポインタを介しても規格違反
    // intで宣言されたオブジェクトを作成
    int i = 0;

    // いったん汎用ポインタに入れて
    void *vp = &i;

    // int非互換型に突っ込む
    long *lp = vp;

    // strict aliasing違反
    printf("%ld\n", *lp);

上記のように規格合致でないコードを書いた場合、未定義動作(undefined behavior)を発生させることになります。

3.4.3
1 未定義動作
可搬性のない、あるいは誤ったプログラムの構造や、誤ったデータの使用に際した、それによって本国際標準が何の要求も課さない動作
2 メモ:未定義動作として可能な動作の幅は、状況を完全に無視して予測不能な結果をもたらすものから、(ソースコードの)翻訳やプログラムの実行の間に、環境に特有のドキュメント化された仕方で(診断メッセージの発行を伴うか伴わないかで)ふるまうもの、翻訳や実行を(診断メッセージの発行を伴って)終了させるものまである。

要するに「規格としては何が起こるか関知しないし処理系がどう処理してもいいもの」という恐ろしい奴です。PCを爆発させられても文句は言えません(まあそこまでの大惨事になることはないとは思いますが)。もちろん、意図通りに動いても問題ありません。あるコンパイラでは普通に動いているのに、別のコンパイラでコンパイルしたらおかしなことになるパターンも多いです。上記コードも、intlongのサイズが異なる処理系だったので毎回値が変わるバイナリが出来た訳で、intlongのサイズが等しければ問題なく動くこともありえるでしょう。

C/C++では未定義動作になるパターンが非常に多く、鼻から悪魔が出るなどと言われます。未定義動作を出したら悪魔が召喚される処理系はさすがにないと思いますが、それでも生粋のブラックメタラーでもない限りは悪魔召喚の儀式を行わないように注意しましょう。gccやclangには-fno-strict-aliasingオプションがあり、積極的な最適化を避けることにより生じるおかしな動きをできるだけ回避することができますがどの程度効果があるんだか分かりません

キャスト不要な型変換

型変換のうち、明示的に型変換を行う必要のないものもあります。いわゆる暗黙の型変換(implicit conversion)です。参考記事の欄に詳しい解説がありますので、詳細はそちらに任せるとして、ここにそのパターンを列挙します。(規格で確認できた分のみ、多分漏れがあると思うのでご指摘あればお願いします。)

  • 整数変換
    • 整数拡張(integer promotion) サイズがint以下の整数型オブジェクトが単項演算子+-~およびシフト演算子<<>>のオペランドとして出てくる場合(6.3.1.1-2)
      • 元の型の値域がintの値域で収まる場合⇒int
      • 元の型の値域がintでは収まらずunsigned intの値域で収まる場合⇒unsigned int
    • 通常の算術型変換の一部(以下)
  • 通常の算術型変換(usual arithmetic conversion) オペランドを複数とる演算子の内、複数のオペランドが共通の実数型(common real type)を要求する演算子13のオペランドに適用する暗黙変換(6.3.1-8) 以下、対象のオペランドについて、上から順番に優先され適用される。
    • いずれかがlong doubleの場合⇒long double
    • いずれかがdoubleの場合⇒double
    • いずれかがfloatの場合⇒float
    • いずれも整数型の場合、いずれのオペランドにも整数拡張(integer promotion)を行った後で以下を適用する。
      • 同一の型である場合⇒型変換なし
      • 双方が符号付き整数型or双方が符号なし整数型の場合⇒整数変換ランク14が上の型
      • 符号なし整数型のランクが符号付整数型より高い場合⇒符号なし整数型
      • 符号付き整数型が符号なし整数型の全値域をカバーできる場合⇒符号付き整数型
      • いずれでもない場合⇒符号付き整数型をもつオペランドと対応する符号なし整数型
  • typeの配列型⇒typeへのポインタ型(6.3.2.1-3) ただし、以下の例外15を除く。
    • sizeof演算子のオペランドである場合
    • _Alignof演算子のオペランドである場合(C11~)
    • 単項&演算子(アドレス演算子)のオペランドである場合
    • 配列初期化に使われる文字列リテラルである場合
  • typeを返す関数型⇒typeを返す関数へのポインタ型(6.3.2.1-4) ただし、以下の例外を除く。
    • sizeof演算子のオペランドである場合
    • _Alignof演算子のオペランドである場合(C11~)
    • 単項&演算子(アドレス演算子)のオペランドである場合
  • 規定の引数拡張(default argument promotion, 6.5.2.2-6) 関数の省略記法(func(const char *fmt, ...)...で省略された引数)にあたる箇所の引数は、整数型の場合は整数拡張(integer promotion)を適用し、floatの場合はdoubleに変換する。
  • 関数呼び出し時の引数への変数代入変換(6.5.2.2-7) 引数に関数定義・プロトタイプ宣言で指定された型と異なる型のオブジェクトを指定した場合、勝手に関数の引数で指定された型に「代入されたかのように」変換される。
  • 等価演算子(equality operator)のポインタ変換(6.5.9-5)
    • 片方のオペランドがNULL定数であり、もう一方がポインタ型である場合、後のポインタ型に合わせる形で前者の型を変換する。
    • 片方のオペランドがvoid *およびその修飾型をもち、もう一方がポインタ型である場合、後者をvoid *に型変換する。
  • 単純代入(simple assignment)変換(6.5.16-3, 6.5.16.1-2) 代入式(assignment expression)の返す型はlvalue conversion16の後の左辺のオペランドの型となり、単純代入時には右辺のオペランドは代入式の型に変換される。
  • return文の変数代入変換(6.8.6.4-3) returnされる式(expression)の型が、その関数の戻り値の型と異なる場合、関数戻り値の型をもつオブジェクトに「代入されたかのように」変換される。

キャスト演算子を使用したい状況

ここまで見て来ましたが、キャスト演算子を「使える」状況は非常に多いとはいえ、キャスト演算子を「使いたい」状況は驚くほど少なそうです。

整数同士の除算結果を浮動小数点数で出す

これは初歩的ですね。プログラミング塾のページとかでも割と見る気がします。

整数除算
#include <stdio.h>

int main(void)
{
    int a = 1;
    int b = 2;
    double answer[] = {(a / b), ((double)a / b)};
    printf("%d / %d = %f\n", a, b, answer[0]);
    printf("%d / %d = %f\n", a, b, answer[1]);
}
$ ./a.out
1 / 2 = 0.000000
1 / 2 = 0.500000

演算対象の型自体を浮動小数点数に事前にしておきましょうねという話です。演算子/は乗除演算子(multiplicative operator)の一種です。

6.5.5-6
整数同士で除算する場合、/演算子の結果は端数を切り捨てた代数的商となる。……

端数切り捨てが発生しないように片方をキャストすると良いでしょう。初心者講座かな?

符号が異なる整数型の比較

以下のコードを実行すると、直感に反する動作が見れます。

符号ありなし比較
#include <stdio.h>

int main(void)
{
    int s = -100;
    unsigned int u = 10;
    if (s < u) {
        puts("s < u");
    }
    else {
        puts("s >= u");
    }

    if (s < (int)u) {
        puts("s < u");
    }
    else {
        puts("s >= u");
    }
}
$ gcc -Wall -Wextra test_func.c 
test_func.c: In function ‘main’:
test_func.c:7:11: warning: comparison between signed and unsigned integer expressions [-Wsign-compare]
     if (s < u) {
           ^
$ ./a.out
s >= u
s < u

わざわざ警告を出してくれているので分かりますが、実行結果は(int)-100 >= (unsigned int)10になってしまいます。以下のような順番で処理されるためこんな意味不明なことになるのです。

  1. 関係演算子(relational operator)<のオペランドには「通常の算術型変換(usual arithmetic conversion)」が適用される。
    • 双方が整数型であるため、ルールに従い「整数拡張(integer promotion)」が行われる。ただし、intunsigned intも整数拡張段階では変換されない。
    • intunsigned intは型のランクが同じであり、かつintunsigned intの値域をすべてカバーできる訳ではないため、符号なし整数型であるunsigned intに統一される。
  2. -100unsigned intの値域外なので、符号なし型の値域内の値に変化する。

6.2.5-9
……符号なしのオペランドを含む評価はオーバーフローしない。符号なし整数型によって表現できない評価結果は、その結果の型によって表現できる最大値より1大きい数値により割った剰余になるからである。

つまり、(unsigned int)-100については、-100 mod (UINT_MAX + 1)が今回の結果となります。16bitなら65436、32bitなら4294967196となるため、10より大きいと判定されてしまう訳です。こういう場合は、明示的にキャストして暗黙変換の罠を回避することが有効な手段となります。

malloc()等の戻り値をキャストする

冒頭のコードを再掲します。

サンプル
int *num = (int *)malloc(sizeof(int));

このキャストは必要でしょうか。stackoverflowでは賛否両論出ています。英語版WikipediaのCでの動的メモリ割り当てのページには以下のように記載されています。

  • 長所
    1. CコードをC++からも使えるようにするのに必要17
    2. C89/C90以前のもともとのC言語ではmalloc()の戻り値がchar *18
    3. (戻り値を受ける)目的のポインタが型サイズの変更を受ける場合、キャストは開発者が型サイズの不一致を識別するのに役立つ。特に、(目的の)ポインタ宣言がmalloc()呼び出しから離れた所で行われている場合はそうである(最近のコンパイラや静的解析はキャストなしにそのような操作を警告できるが)。
  • 短所
    1. C標準下ではただ冗長。
    2. キャストを加えることでmalloc()の関数プロトタイプがあるstdlib.hのインクルード忘れを隠してしまう。malloc()のプロトタイプがないとき、C90標準はmalloc()intを返す関数であるとみなすことをCコンパイラに要求する。キャストがなければ、C90はその整数がポインタへ代入されるとき診断情報を要求する。しかし、キャストをつけるとこの診断情報は出ないかもしれず、バグを隠してしまう。(longとポインタが64bitでintが32bitの、64bitシステム上のLP64のような、)特定のアーキテクチャやデータモデルにおいて、この間違いは実際に未定義動作(undefined behavior)という結果となる。暗黙的に宣言されたmalloc()が32bit値を返すにも拘らず、実際の関数定義は64bit値を返すからである。関数呼び出しの仕様やメモリレイアウトによっては、これはスタックを破壊する結果となる。この問題はモダン・コンパイラにおいては比較的に気づかれにくいものとはなっていない。C99は暗黙的な宣言を許していないので、コンパイラはintを戻り値とみなすとしても診断情報を出さなければならないのである19
    3. ポインタの型宣言を変更する場合に、malloc()が呼び出されるすべての行とキャストをも変更する必要があるかもしれない20

上記の長所3を理由に、たとえばCERTでは明示的にキャストを行うことを推奨しています。以下のコードを見てみましょう。見ての通りmalloc()で確保しているサイズとポインタ型のサイズが違っています。

ポインタ型不一致
#include <stdlib.h>

#define MALLOC(type) ((type *)malloc(sizeof(type)))

int main(void)
{
    long *lp1 = malloc(sizeof(int));
    long *lp2 = (int *)malloc(sizeof(int));
    long *lp3 = MALLOC(int);

    long *lp[] = {lp1, lp2, lp3};
    for (size_t i = 0; i < (sizeof(lp) / sizeof(lp[0])); i++) {
        free(lp[i]);
    }
}

コンパイル結果は以下の通りです。

$ gcc -Wall -Wextra test_func.c 
test_func.c: In function ‘main’:
test_func.c:7:17: warning: initialization from incompatible pointer type [-Wincompatible-pointer-types]
     long *lp2 = (int *)malloc(sizeof(int));
                 ^
test_func.c:3:22: warning: initialization from incompatible pointer type [-Wincompatible-pointer-types]
 #define MALLOC(type) ((type *)malloc(sizeof(type)))
                      ^
test_func.c:8:17: note: in expansion of macro ‘MALLOC’
     long *lp3 = MALLOC(int);
                 ^~~~~~

キャストが行われているもののみ警告が表示されるという結果になります。そのため、malloc()においては戻り値の型を明示し、型チェックをコンパイラに肩代わりさせる目的でキャストを行う人も多いと思います。そういう人から「malloc()はキャストしろ」と言われてそういうものだとやっている人も多分にいます。

おそらく、これをメリットと思うかどうかによってこの方策をとるかどうかが決まることでしょう。たとえば、以下のような間違いは起こり得そうですが、これについては警告されません。

警告されないミス
#include <stdlib.h>

int main(void)
{
    long *lp = (long *)malloc(sizeof(int));
    free(lp);
}

malloc()が返すのはあくまで型を持たない記憶域であるため、キャストが間違っていれば型の不一致は起こらず、警告はなくなってしまいます。それはCERTも把握しており、最初のコードでさらっと書いていたMALLOC(type)マクロは更なる間違いへの対策のために紹介されていました。

ただし、このマクロの導入も意見が分かれると思います。このようなマクロを、一人で開発する際に作っておくのは悪くはありませんが、複数人での開発に「戻り値のあるmalloc()をマクロ定義したから使ってね!」と複数人に触れ回るのはそれだけでコストになりそうです。人員が頻繁に入れ替わる環境では余計そうなるでしょう。私はキャストする派ですが、そもそも私自身、これだけのためにマクロを作りたいとは思いませんし。

ということで、このキャストを使うかどうか自分で決めることができるのであれば、このあたりを秤にかけて判断するのがよいと思います。コーディング規約で決まっていれば別ですが。

戻り値を無視することを明示するvoidキャスト

ignore_return.c
#include <stdlib.h>

int main(void)
{
    (void)system("echo \"Hello, world!\"");
}

これも賛否あるかと思います。キャスト演算子は任意の型をvoidにキャストすることができますので、戻り値のvoidキャストは文法違反になりません。voidの値にアクセスすることもできないため、strict aliasing rules違反もありえませんね。CERTでも例外項目で推奨していますので、戻り値に興味がないことを明示したい際には使ってみてもいいでしょう。

参考記事

参照規格

  • C draft n1570
    実質C11のdraft。正規の規格書は高価なので、こちらが現在最もアクセスしやすい一次文献でしょう。残念ながらJISにはなっていないので日本語訳は存在しません。英語も勉強できて一石二鳥ですね。

  • JIS X 3010
    日本語版C99と思ってよいでしょう。JISの規格検索からX3010で検索すれば見れます。また、このサイトでも閲覧できます。訳に困ったりしたらここを見ていました。大部分についてはC11から変わっていないので、規格書リーディングに大いに参考になるかと思います。

C++のキャスト(名前付きキャスト)

暗黙の型変換

strict aliasing rules関係


  1. 規格上では、型変換を一部の演算子のオペランドによって行われる暗黙の型変換(implicit conversion)と、キャスト演算による明示的型変換(explicit conversion)の2種に分けています(6.3-1)。 

  2. malloc()のキャストを行うべきか否かについては人によりけりらしいです。後述。 

  3. 暗黙の型変換については後から軽く触れます。 

  4. この箇所には注がついています。曰く、「キャストの結果はlvalueにならない。そのため、修飾された型へのキャストと、その型の修飾されていないものへのキャストでは、その影響は同じである。」らしいです。でも、char *へのキャストとconst char *へのキャストでは違う気がするのはどうなんだろう……? 

  5. 説明中にelement typeとかいう文言が書いてありました(6.2.5-20)。配列の要素の型のことなんでしょうがそれって区別必要なんですかね? 

  6. 6.2.5-1では「object typefunction typeがあるよ」という書きぶりなのですが、その割に6.2.5.20ではfunction typederived typeの一部扱いになってしまっています。どっちやねん。 本文では後者の見方で記載しています。 

  7. ポインタ型の参照先の型をreferenced typeと呼ぶらしいです(6.2.5-20)。 

  8. C11以降の機能、かつオプション機能のためC11規格合致の処理系にすら存在しない可能性があります。 

  9. よく左辺値と訳されるlvalueは、6.3.2.1-1で規定されています。「lvalueはオブジェクトを指すことができる(void以外のオブジェクト型をもつ)式(expression)である。lvalueが評価される時にオブジェクトを指していない場合、動作は未定義である。オブジェクトが特定の型をもつと言われる場合、その型は、そのオブジェクトを指すのに使われるlvalueによって指示されている。変更可能なlvalue(modifiable lvalue)は配列型・不完全型・const修飾された型ではなく、(変更可能なlvalueが)構造体型・共用体型である場合は、(再帰的にアグリゲート型・共用型を含むあらゆるメンバ・要素をもつものを含み)const修飾された型をもつメンバを含まない。」C++に関してはこちらが分かりやすいです。 

  10. 有効型(effective type)については、6.5-6に規定されています。ざっくりまとめると以下のようになります。
    ・基本的に有効型は宣言に使った型。
    ・宣言時の型を伴わないオブジェクト(malloc()あたりで確保したものなど)は、有効型は最初にアクセスした時の型。
    ・宣言時の型を伴わないオブジェクトへの最初のアクセスがmemcpy()memmove()による場合、有効型はコピー元の型。 

  11. これについては日本語版スタックオーバーフローで質問させてもらいました。3.1でアクセスの定義が「オブジェクトの値を読むこと、または変更すること」とされているので、値の読み込み・書き込みが発生するまでは未定義動作を踏むことはないと見てよいでしょう。 

  12. gccとclang(version 6.0.0-1ubuntu2)で-Wstrict-aliasingしても警告されませんでした。静的な解析には限界があるということか……。ちなみに、両者ともvoidポインタを介さず、明示的キャストせずにlong *lp = &i;とした場合は-Wincompatible-pointer-typesに引っかかり警告が出ました。明示的キャストをした場合は警告が出ませんでした。 -Wstrict-aliasingとは一体……。 

  13. 例えば、条件演算子?:の第2・第3オペランドは同一の型であると措定されます。そのため、int i = 1; unsigned int ui = 2; long l = cond ? i : ui;などとした場合、iuiは同一の型へと変換されて処理されることになります。 

  14. 整数変換ランク(integer conversion rank)というのは、その名の通り整数が型変換される際の優先度をランクづけしたものであり、暗黙変換時に要求する型が決まっていない場合は基本的にこのランクを元に整数変換が行われます。以下のように(6.3.1.1-1)なっています。
    ・内部の表現が同じでも符号付き整数型同士で同じランクの型はないこととする。(例:shortintが揃って16bitの処理系でも、両者のランクは異なる)
    ・整数型同士での型のランクは、より精度が低い型のランクより高くなる。(例:shortが16bit、intが32bitの場合、intの方がランクが上でなければならない)
    ・符号付き整数型のランクは次の通り。signed char < short int < int < long int < long long int
    ・符号なし整数型のランクは、対応する符号付き整数型に等しい。
    ・標準整数型のランクは、同じ精度の拡張整数型よりもランクが高い。(例:int(16bit)と、処理系独自に作成された同精度の整数型i_int(16bit)があった場合、intの方がランクが高くなる)
    charのランクはsigned charunsigned charのランクに等しい。
    _Boolのランクは、他のいかなる整数型のランクよりも低い。
    ・列挙型のランクは、対応する整数型と同じランクになる。(補足:6.7.2.2.4によると、列挙型の値域は、その型の列挙定数がすべて表現できる整数型のどれかと等しくなります。その制約内でどれになるかは処理系定義です。)
    ・同精度の拡張符号付き整数型同士のランクは他の規則に従った上で処理系定義。
    ・ランクについて、T1 > T2 かつ T2 > T3 なら T1 > T3 になる。(ランクの上下が T1 > T2 > T3 > T1 > ... のような構造になることはない。) 

  15. C89/90まではrvalueは暗黙変換の対象にならないので注意。 

  16. lvalue conversionとは、「配列型以外のlvalueが、sizeof_Alignof++--演算子のオペランドおよび.=演算子の左オペランドでない場合、lvalueが指すオブジェクトの値をもつrvalueへと変換される。その際の型は、そのlvalueの型の非修飾型(unqualified version)となる。」(6.3.2.1-2)といった変換のことを指します。 

  17. C++17のdraftを確認(8.2.10-7)しましたが、「オブジェクトポインタは異なる型のオブジェクトポインタへと明示的に変換できる」と記載されているので、明示キャストが必要なようです。g++でキャストを排除するとerror: invalid conversion from ‘void*’ to ‘int*’ [-fpermissive]というエラーでコンパイルできません。そもそもC++からmalloc()を使うのが最終手段というか末期的な気がします。 

  18. そもそも規格化以前の原初のC言語にはvoid *がなかったという話だそうです。今どき気にする必要がある現場がどれだけあるかですね。 

  19. 実際、gccは普通に「インクルード忘れてるぞ」な警告を出してくれるので、この点はあまり気にしなくていいと思います。 

  20. まず一つのポインタに入れ替わり立ち替わりmalloc()されるコードの保守が割と地獄な気がしないでもないです。今どきはポインタ宣言の時点で即malloc()が普通だと思うので、短所に挙げるほど手間が増えるかはちょっと疑問です。 

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17