C

(C言語)Array-to-pointer conversionの例外は3つだけではなかった

C言語では、配列型をもつ式は、その要素型を指すポインタに型変換されます。ただし例外が3つあり

  • 配列型をもつ式が、sizeof演算子のオペランドである。
  • 配列型をもつ式が、&演算子のオペランドである。
  • 配列型をもつ式が、文字型配列を初期化する文字列リテラルである。

配列型をもつ式が上記のいずれかに当てはまる場合、型変換は成立せず、配列型のままとなります。

ここまでは、まあそうだね…という話ですが、C90とC11においては上記3つの例外のほかに、各々の規格固有の例外が存在します。

C90の場合

C90の場合、配列→ポインタの型変換には「配列型をもつ式がlvalueであること」という条件が存在します。

「ISO/IEC 9899:1990」より該当箇所を引用します(太字引用者)。

6.2.2.1 Lvalues and function designators
〔...〕
 Except when it is the operand of the sizeof operator or the unary operator, or is a character string literal used to initialize an array of character type, or is a wide string literal used to initialize an array with element type compatible with wchar_t, an lvalue that has type “array of type” is converted to an expression that has type “pointer to type” that points to the initial element of the array object and is not an lvalue.

以下は、rvalueの配列に添字演算子を適用したコード例です。式f().aはrvalueの配列型であるため、要素型を指すポインタに型変換されません。一方、添字演算子[]は一方のオペランドにオブジェクトへのポインタをとるという制約があります。結果、C90においては式f().a[0]は添字演算子[]の制約に違反するため、ill-formedな式となります。

C90ではill-formed
#include <stdio.h>

typedef struct X_ {
    int a[10];
} X;

X f()
{
    X x;
    x.a[0] = 0;
    return x;
}

int main(void)
{
    printf("%d\n", f().a[0]);  /* warning: ISO C90 forbids subscripting non-lvalue array [-Wpedantic] */

    return 0;
}

以下は、rvalueの配列からポインタへの明示的型変換を期待しています。キャスト演算子はオペランドに算術型・ポインタ型・構造体型・共用体型をとる一方、式f().aはC90では配列型をもつため、C90ではill-formedとなります。

C90ではill-formed
(int *)f().a;  /* error: cannot convert to a pointer type */

以下の二例は、rvalueの配列からポインタへの代入変換を期待しています。代入変換において代入元に配列型をとることはできないため、C90ではill-formedな式となります。

C90ではill-formed
   int *p; 
    p = f().a;  /* error: invalid use of non-lvalue array */
C90ではill-formed
void g(int *p) {}

int main(void)
{
    g(f().a);  /* error: invalid use of non-lvalue array */

    return 0;
}

以上はC90のみの制約です。C99以降では先述の規格の該当箇所の「an lvalue」が「an expression」に修正されており、「lvalue」という制限が撤廃されています。

C11の場合

C11の場合、冒頭にあげた3つの例外に加えて、_Alignof演算子(C11で追加)のオペランドの配列型は要素型へ型変換されないという例外が存在します。以下、N1570より引用(太字引用者)。

6.3.2.1 Lvalues, arrays, and function designators
〔...〕
3 Except when it is the operand of the sizeof operator, the _Alignof operator, or the unary & operator, or is a string literal used to initialize an array, an expression that has type “array of type” is converted to an expression with type “pointer to type” that points to the initial element of the array object and is not an lvalue. If the array object has register storage class, the behavior is undefined.

6.5.3.4 The sizeof and _Alignof operators
〔...〕
3 The _Alignof operator yields the alignment requirement of its operand type. The operand is not evaluated and the result is an integer constant. When applied to an array type, the result is the alignment requirement of the element type.

上記の引用にもあるように、_Alignof演算子のオペランドに配列型が指定された場合、結果はその要素型のアラインメント値となります。

(C11)_Alignof演算子の一例
#include <stdio.h>                                       

int main(void)
{
    printf("%lu\n", _Alignof(char[256]));  // 1
    printf("%lu\n", _Alignof(char *));     // 8

    return 0;
}

なお_Alignof演算子のシンタックスは_Alignof(type-name)であるため、オペランドに式をとることはできないようです。