Class::Accessor::Fastが破壊的だったと初めて知ったあの日

事の発端はぽけーっとはてブのお気に入りを見ていたらClass::Accessor::Liteの記事が目に付いた事でした。

お、新しいモジュールか?と思いさっそく実装を拝見させてもらったわけです。

なるほどなるほど、超が付くほどの超シンプル。超々シンプル。実際問題Class::Accessor::Fast使うよりも、Liteのように自分で超軽量のアクセサ定義することの方が多かったりします。

しかし一点気になったところがありました。

@_のリファレンスを保存している部分があったのです。

# Class::Accessor::Lite-0.02

sub __m {
    my $n = shift;
    sub {
        return $_[0]->{$n} if @_ == 1;
        return $_[0]->{$n} = $_[1] if @_ == 2;
        shift->{$n} = \@_; ### ← この部分
    };
}

何故リファレンスで保存してるのかなぁと思い、大元のClass::Accessor::Fastではどうなってるのか実装を見てみたところ、同じく@_のリファレンスを保存していました。

# Class::Accessor::Fast-0.34

sub make_wo_accessor {
    my($class, $field) = @_;

    return sub {
        if (@_ == 1) {
            my $caller = caller;
            $_[0]->_croak("'$caller' cannot access the value of '$field' on objects of class '$class'");
        }
        else {
            return $_[0]->{$field} = $_[1] if @_ == 2;
            return (shift)->{$field} = \@_; ### ← この部分
        }
    };
}

言わずもがな@_というのは関数呼び出し時の引数として渡される変数のエイリアスとなるため、@_のリファレンスをメンバに保存して使いまわしてしまうと、色々と問題が出る可能性があります。

以下のコードは、大元の引数を破壊してしまう例です。

package Foo;
use base qw/Class::Accessor::Fast/;
Foo->mk_accessors('foo');

my $foo = Foo->new;
my @data = (10,20,30);

$foo->foo(@data); # @_は@dataのエイリアスになる

$foo->foo->[0] = 99; # メンバの方を99に変更する

print $data[0]; # 「99」と表示される!

ぶっちゃけこれは意図した仕様なのかどうかちょっとわかりかねますが、本家本元のClass::Accessorの実装ではリファレンスを保存していませんでした。

# Class::Accessor-0.34

sub set {
    my($self, $key) = splice(@_, 0, 2);

    if(@_ == 1) {
        $self->{$key} = $_[0];
    }
    elsif(@_ > 1) {
        $self->{$key} = [@_]; ### ← 無名配列に展開して保存してる
    }
    else {
        $self->_croak("Wrong number of arguments received");
    }
}

ということで恐らくはFast(及び、Fastを参考にしたLite)特有のバグの可能性は濃厚な感じがするっぽいノリです。

ただまぁそもそもアクセサに複数の値を渡すと配列のリファレンスで保持されるって言う仕様を、今初めて知ったんですけどねッ!





と、ここまで書いてる途中でようやくLiteの記事が2年前の記事だったことに気付いたわけで、今更2年前の記事に反応かよ的な冷たい視線も感じつつも、折角ここまで調べて文章化したのでネタもないしお蔵入りもったいないからあっぷあっぷ。