ちょっと話題の記事

[Objective-C]__attribute__ディレクティブを使ってみる

2013.12.25

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Objective-Cのオープンソースライブラリのコードを眺めていると

__attribute__

という文に出くわすことがしばしばあります。
これは属性を表すキーワードで、普段そこまで使う機会がないのですが、調べるうちに何かと多人数の開発の時にはご利益がありそうな機能ばかりだと感じたので、まとめておきます。尚、使用環境はXcode5, ARCです。

何ができる?

__attribute__はコンパイラディレクティブの一種で、変数、型、関数(Objective-Cではクラスやメソッドも)の属性を決めることができます。コンパイラによる静的チェックや関数の最適化の役に立ちます。
__attribute__の後にはカッコが二重に続き、二重括弧の中でコンマ区切りの属性リストを付与することができます。
__attribute__は変数、型、関数宣言の前後に置かれます。Objective-Cではメソッドに適用されている場合が多く、この記事でもメソッドの宣言に絞って解説します。

Objectve-Cメソッドへのattribute属性

Objective-Cに適用できるattribute属性を実際に使ってみます。

format属性

関数、メソッドの可変長引数の型に対してprintf, scanf等の関数宣言のような引数形式が保たれていることを静的チェックしてくれます。具体例を見てみましょう。

Sample.h
#import <Foundation/Foundation.h>

@interface Sample : NSObject

- (void)logSample:(NSString*)log, ... __attribute__((format(__NSString__, 1, 2)));

extern void logMe(int num, char *arg , ...) __attribute__((format(printf, 2, 3)));

@end
Sample.m
#import "Sample.h"

@implementation Sample

- (void)logSample:(NSString *)log, ...
{
    va_list args;
    va_start(args, log);
    NSLogv(log, args);
    va_end(args);
}

void logMe(int num, char *arg , ...)
{
    va_list args;
    va_start(args, arg);
    vprintf(arg, args);
    va_end(args);
}

@end

5行目のlogSampleメソッド宣言直後の__attribute__((format(__NSString__, 1, 2)));では、1番目、2番目の引数にNSString +stringWithFormatメソッドにおける引数の型対応がされることをコンパイラが期待します。
7行目のlogMe関数宣言直後の__attribute__((format(printf, 2, 3)));では、2番目、3番目の引数にprintf関数における引数の型対応がされることをコンパイラが期待します。

実際に間違った形式の引数を渡して型チェックが行われているか確認してみます。 attribute1
NSString +stringWithFormatメソッドやprintf関数のように、引数内の文字列で宣言された型を以降の引数が満たさないとwarningを出すようになることがわかります。

Sample.mの中で使われているva_list,va_start,va_endは可変長引数を扱うための型、マクロです。詳しくはSTDARG - Linux Programmer's Manualを御覧ください。
NSLogv, vprintfは可変長引数に対応したNSLog, printfです。

尚、Foundation.framework内のNSObjCRuntime.hヘッダファイル内にあるように、NS_FORMAT_FUNCTION(F,A)マクロは__attribute__((format(__NSString__, F, A)))と同等の役割を果たします。

availability属性

関数、メソッドに動作対象のOSプラットフォームとバージョンを付与できます。availabilityにはOSプラットフォーム名(ios/macosx)と次の属性をコンマ区切りで指定できます。

  • introduced: メソッド宣言が取り入れられた最初のOSバージョン
  • deprecated: メソッド宣言が非推奨になった最初のOSバージョン
  • obsoleted: メソッド宣言が使われなくなった最初のOSバージョン
  • unavailable: 指定OSプラットフォームでメソッドが使えないことを示す
  • message: warningやerrorで表示されるメッセージを決められる

使用例を見て行きましょう。

Sample.h
#import <Foundation/Foundation.h>

@interface Sample : NSObject

- (void)firstRestrictedMethod:(NSInteger)integer __attribute__((availability(ios,introduced=4.0,deprecated=6.0,obsoleted=7.0)));

- (void)secondRestrictedMethod:(NSInteger)integer __attribute__((availability(ios,unavailable,message="This method has been deleted.")));

@end
Sample.m
#import "Sample.h"

@implementation Sample

- (void)firstRestrictedMethod:(NSInteger)integer{}

- (void)secondRestrictedMethod:(NSInteger)integer{}

@end

プロジェクトファイルをクリックしてTarget->Generalタブ->Deployment TargetからiOSのターゲットバージョンを指定して挙動を確認してみます

attribute4

Deployment Target = 6.0


attribute5

Deployment Target = 7.0


attribute6

まず、firstRestrictedMethodの挙動についてはiOS6ではdeprecatedされているというwarningを出し、iOS7ではobsoletedされているというerrorを出すことがわかります。

また、secondRestrictedMethodの挙動についても確認してみると、
attribute7
のように、iOSプラットフォームでは使えませんというerrorと、attributeに書かれたメッセージ:This method has been deletedを出すことが分かります。

ns_returns_retained/ns_returns_not_retained属性

メソッドの呼び出し側に強参照を期待するかどうかを示した属性です。
ARC配下ではalloc, copy, mutableCopy, new, initメソッドファミリに属していないメソッドはオブジェクトを返り値に持つ場合、特に明示しなければ

  • 弱参照に渡す場合はautoreleasepoolに登録されます
  • 強参照に渡す場合はautoreleasepoolに登録されず、強参照のスコープ終了まで保有されます


逆にこれらのメソッドファミリに属しているメソッドの場合、autoreleasepoolに登録されるような機構はそのメソッド単体では働かず、メソッド内部でretainedされたオブジェクトを自動的にreleaseするコードをARCがメソッド呼び出し側に挿入するため、

  • 弱参照に渡す場合は即時解放されて同じスコープ内でも参照がnilになってしまう可能性があります
  • 強参照に渡す場合はその参照に保有され、強参照のスコープ終了まで保有されます


これらのメソッドファミリに属していないようなメソッドに対して内部でretainedされたオブジェクトを強参照に保有されることを明示的にしたい場合に、ns_returns_retainedを使います。
逆にこれらのメソッドファミリに属しているようなメソッドに対して、メソッドファミリに属していない場合のデフォルトのautoreleaseの機構を働かせたいときにはns_returns_not_retainedを使います。

例を挙げて見てみましょう。

Sample.h
#import <Foundation/Foundation.h>

@interface Sample : NSObject

- (NSString*)stringRetained:(NSString*)str __attribute__((ns_returns_retained));

- (NSString*)newStringNotRetained:(NSString*)str __attribute__((ns_returns_not_retained));

@end
Sample.m
#import "Sample.h"

@implementation Sample

- (NSString*)stringRetained:(NSString *)str
{
    NSString *retVal = [[NSString alloc] initWithString:str];
    return retVal;
}

- (NSString*)newStringNotRetained:(NSString *)str
{
    NSString *retVal = [NSString stringWithFormat:@"%@", str];
    return retVal;
}

@end


実際に強参照が期待されているかどうかを確認してみます。
attribute8
retainedされたオブジェクトなのに弱参照にもたせてしまっているからコンパイラによってrelease用コードを挿入されてすぐに解放される危険性があるよ、というwarningが出ています。
__weakを取り除けばデフォルトの強参照に変更されるのでwarningは出なくなります。

newStringNotRetainedメソッドについても弱参照に代入して確かめてみて、このメソッドがnewメソッドファミリに属するような名前を持っているにもかかわらずwarningが出ないことを確認してみてください。ARC環境下のnot_retainedなメソッドでは返り値を弱参照に入れるとautoreleasepoolに登録されてから解放される為に、しばらくは値の存在が保証されるためです。
また、__attribute__((ns_returns_not_retained))をコメントアウトするとretainedされたオブジェクトに対するのと同様のwarningが出ることも確かめてみてください。newメソッドファミリに属するメソッドはretainedされたオブジェクトを返り値に持ち、弱参照に代入するとARC側が即時releaseしてしまう危険性があるためです。

尚、Foundation.framework内のNSObjCRuntime.hヘッダファイル内にあるように、NS_RETURNS_RETAINEDNS_RETURNS_NOT_RETAINEDマクロはそれぞれ__attribute__((ns_returns_retained))__attribute__((ns_returns_not_retained))と同等の役割を果たします。

メソッドファミリの概念に関しては詳解 Objective-C 2.0 第3版に詳しい解説が載っています。
また、ARC環境下でのautoreleaseの機構についてはエキスパートObjective-Cプログラミングに詳しいです。

objc_reguires_super属性

サブクラスの継承メソッドにスーパークラスのメソッドコールを要請します。例を見てみましょう。

Sample.h
#import <Foundation/Foundation.h>

@interface Sample : NSObject

- (void)requireSuperMethod __attribute__((objc_requires_super));

@end
Sample.m
#import "Sample.h"

@implementation Sample

- (void)requireSuperMethod {}

@end
SubSample.h
#import "Sample.h"

@interface SubSample : Sample

@end
SubSample.m
#import "SubSample.h"

@implementation SubSample

- (void)requireSuperMethod
{
    
}

@end


SubSample.mのrequireSuperMethodに以下のようなwarningが発生します。
attribute9
[super requireSuperMethod];を継承したメソッド内でも宣言してくださいという内容の警告です。サブクラスに必ずスーパクラスの元メソッドを呼んでほしい時には大変便利です。

例によってNSObjCRuntime.hヘッダファイル内にあるように、NS_REQUIRES_SUPERマクロは__attribute__((objc_requires_super))と同等の役割を果たします。

objc_method_family属性

この属性を使うことで命名規約に基づかないようなメソッドでも所定のメソッドファミリに属するように設定できます。例えばinit大文字〜以外の名前を持つメソッドでもinitのメソッドファミリに属するように変えることができます。

  • __attribute__((objc_method_family(alloc))): allocファミリへ変更(強参照を要請するポインタ型を返すメソッド)
  • __attribute__((objc_method_family(copy))): copyファミリへ変更(強参照を要請するポインタ型を返すメソッド)
  • __attribute__((objc_method_family(mutableCopy))): mutableCopyファミリへ変更(強参照を要請するポインタ型を返すメソッド)
  • __attribute__((objc_method_family(new))): newファミリへ変更(強参照を要請するポインタ型を返すメソッド)
  • __attribute__((objc_method_family(init))): initファミリへ変更(強参照を要請するid型もしくはスーパークラス、サブクラスのポインタ型を返すメソッド)
  • __attribute__((objc_method_family(none))): どのファミリにも属さない


例を見てみましょう。

Sample.h
#import <Foundation/Foundation.h>

@interface Sample : NSObject

- (instancetype)createSample __attribute__((objc_method_family(init)));

@end
Sample.m
#import "Sample.h"

@implementation Sample

- (instancetype)createSample
{
    if (self = [super init]) {
    }
    return self;
}

@end


ここで、ヘッダファイルのメソッドに付いている属性をnoneにしてみると次のようなerrorが現れます。
attribute10
init属性を適用することで抑えられていたエラーが出てきているというわけです。

このような属性変更を乱用することは所属メソッドファミリをメソッド名一つで把握できなくなる為にあまりオススメは出来ません。できるだけObjective-Cの命名規約に基づいたメソッドネーミングを心がけるようにしたいものです。

おまけ(C言語関数への属性)


C言語関数へもたせられる属性は膨大です。詳しくは参考サイトの最後のリンクを御覧ください。
ここでは代表的なものに絞って例を挙げるにとどめます。

const/pure属性

C言語の関数では

  • 何回実行しても、どのようなプログラムの状態で実行しても同じ引数を与えれば返り値が変わらない関数に対してはconst属性を
  • 何回実行してもプログラムの状態に影響を及ぼさない関数(=副作用を持たない関数)に対してはpure属性を


つけることでコンパイラがパフォーマンスの最適化を図ってくれます。
例えばconst属性の関数は引数にしか結果が依存しないので結果をキャッシュしておき、同じ引数で呼び出された時は再び計算を行わないようにコンパイラが最適化してくれます。
Objective-C内でもconst/pure属性をつけることは可能ですが、常にselfで自インスタンス、自クラスを参照できてしまうので最適化は行われません。ただし、多人数開発の際にメソッド宣言でconst/pure属性をつけることで他人が見た時にひと目で副作用を持たないメソッドとわかります。
尚、const/pureであることはプログラマが保証しなければならず、コンパイラが関数内を静的に解析して、大域状態に触れたコードがある時などにwarningを出すわけではないので注意が必要です。

例を見てみましょう。

Sample.h
#ifndef Sample_h
#define Sample_h

extern int squareSample(int i) __attribute__((const));

#endif
Sample.c
#include "Sample.h"

int squareSample(int i)
{
    return i * i;
}

他のファイルでこの関数を呼び出して、結果をどこにも格納しないと次のようなwarningを吐きます。 attribute3
const/pure属性を持つ関数は基本的に副作用を持たないのでvoid型のような使い方をコンパイラが許容しないということです。

overloadable属性

C言語関数にC++メソッドのようなオーバーロードを許容します。例を挙げます。

Sample.h
#import <Foundation/Foundation.h>

@interface Sample : NSObject

extern NSString * __attribute__((overloadable)) stringFromValue(float f);

extern NSString * __attribute__((overloadable)) stringFromValue(int i);

@end
Sample.m
#import "Sample.h"

@implementation Sample

NSString * __attribute__((overloadable)) stringFromValue(float f)
{
    return [NSString stringWithFormat:@"%lf", f];
}

NSString * __attribute__((overloadable)) stringFromValue(int i)
{
    return [NSString stringWithFormat:@"%d", i];
}

@end


実際にこの関数は中の引数型に応じて選択され、コンパイルエラーもなく動きます。

nonnull属性

インデックスでポインタ引数を指定することでその引数にNULLを入れるとwarningを発するようにします。nonnullのみを入れるとその関数の全ポインタ引数をnonnullにします。
例を見てみましょう

Sample.h
#import <Foundation/Foundation.h>

@interface Sample : NSObject

extern void NSLogCString(char *str) __attribute__((nonnull(1)));

@end
Sample.m
#import "Sample.h"

@implementation Sample

void NSLogCString(char *str)
{
    NSLog(@"%@", @(str));
}

@end


関数にNULLを入れると以下の様なwarningを出します。 attribute11
Sample.m中のchar*型からNSString*型への変更についてはObjective-C Literals - Clang 3.5 documentationのBoxed C Stringsの項目に詳しい説明が載っています。

終わりに

多人数開発にせよ、そうでないにせよ、このような属性を明示的に示しておくことで、メソッドの使用用途に反する使い方をされた際にはコンパイラ(clang)がwarning,errorを出すようになります。コンパイラによる静的解析が正しい実装への手助けをしてくれるわけです。意図しない使われ方を防ぐためにもこういった制約は明示的にしていきたいものです。

参考サイト