利用者定義演算子

利用者定義演算子 (りようしゃていぎえんざんし[1][2] : user-defined operators[3][4]) とはプログラミング言語において、言語の利用者が演算子に対し組み込みの演算子とは異なる動作を定義できる機能である。

概要

古くはFORTRANから導入された機能である。当初は可読性と記述性の観点から各種言語に取り入れられた機能であるが、SmalltalkC++によるオブジェクト指向の発達とともに多態性を実現するための機能としての側面も持つようになった。利用者定義演算子の定義はどのような言語でも関数またはメンバー関数あるいはメソッドのいずれかで定義するようになっている。

演算子定義の例

Smalltalkによる例:

Object
    subclass:               #Value
    instanceVariableNames:  'value'
    classVariableNames:     ''
    poolDictionaries:       ''
    category:               'Example'.

Value
    createGetMethod: 'value' default: 0;
    createSetMethod: 'value'.

Value methodsFor: 'accessing'
!
species
    " 演算子の戻り値型としてValue自身を使う。 "
    ^ Value.
!!

" '+' 演算子と '-' 演算子の定義。 "
Value methodsFor: 'operator'
!
+ aNumber
    ^ self species with: self value + aNumber.
!
- aNumber
    ^ self species with: self value - aNumber.
!!

Value methodsFor: 'instance creation'
!
with: aNumber
    ^ super new value: aNumber.
!!

| value0 value1 value2 |

value1 := Value with: 10.
value2 := Value with: 5.

" 利用者定義演算子を使用したため各変数には整数オブジェクトではなくValueクラスのオブジェクトが代入されている。 "
value0 := value1 + value2.
value0 := value1 - value2.

Smalltalkにおいてはメソッドに記号だけで構成されるセレクター[5]をつけることで利用者定義演算子を定義することができる。Smalltalkでは演算子を2項セレクターとよび#with:など英数でできたセレクターとほぼ同様に扱う。Smalltalkにおいて演算子はメッセージの一種という扱いであり引数が必ず1個で優先順位が異なる点以外は特別扱いはしない。このため演算子として定義できる記号には殆ど制限がない(ただし区切り用の記号や代入記号は指定できない)。また、演算子として定義する記号は2文字でもよく->~=といった演算子がよく定義されている。Smalltalkは多重定義ができないため一つのクラスに同じ演算子を複数定義することはできない。ただし、インスタンスオブジェクトとクラスオブジェクトは同じクラスに紐づくものの別のオブジェクトであるためインスタンスメソッドとクラスメソッドで同じ演算子を定義することが可能になっている。

C++による例:

class Value
{
    int value;
public:
    Value( int value ):
        value( value )
    {
    }

    // '+' 演算子の定義。
    Value operator + ( Value const &source ) const
    {
        return Value( source.value + value );
    }

    // '-' 演算子の定義。
    Value operator - ( Value const &source ) const
    {
        return Value( source.value - value );
    }

    // 単項演算子版の'-'演算子の定義。
    Value operator - (void) const
    {
        return Value( -value );
    }
};

// 大域関数版演算子の定義。

Value operator + ( int left, Value const &right )
{
    return Value( left ) + right;
}

Value operator - ( int left, Value const &right )
{
    return Value( left ) - right;
}

int main(void)
{
    Value
        value0( 0 ),
        value1( 10 ),
        value2( 5 );
    
    // 利用者定義演算子を使用しているためどの変数も数値型ではなくValue型になっている。
    value0 = value1 + value2;
    value0 = value1 - value2;
    
    // メンバー関数では1項目はValue型でなければならないが大域関数を定義しているため1項目に数値を指定できる。
    value0 = 10 + value2;
    value0 = 10 - value2;
    
    return EXIT_SUCCESS;
}

C++においてはoperatorキーワードの後に記号をつけた形の特殊な名前を持つ関数を定義することで演算子を定義できる。C++では言語機能として用意されている演算子しか定義できず独自の記号を用いた演算子を定義することはできない。また、演算子の引数や戻り値の型は演算子の種類によって制限される。例えば型のメンバーを指定する->演算子を単項演算子として独自に定義したり、数値型や->演算子を定義していない型を戻り値の型として指定することはできない。多くの制限があるなか下記のような通常の関数と異なる演算子独自の振る舞いをする演算子があり通常の関数では不可能な構文を記述することができるようになっている。Smalltalkでは2項演算子しか定義することはできないが+x -xといった単項演算子は勿論x[i]といった添字演算子やx( a, b, c )といった関数呼出演算子なども定義することができる。

C++は多重定義が可能な言語であり、利用者定義演算子は多重定義の枠組みに入っている。このためSmalltalkと異なり、引数が異なる場合に限って同じ名前空間で同じ名前の演算子を複数定義することが可能となっている。ただし、既存の演算子を上書きすることになってしまうため数値型だけを引数とする演算子の定義はできない。

演算子宣言 名称 振る舞い 応用例
operator Type() 型変換(キャスト)演算子 Type 型を返す。Type 型への暗黙的な変換が可能となる。呼び出しにはstatic_castなどのキャスト構文による明示的な型変換も使える。C++11以降では、暗黙的な型変換を抑止して、明示的な型変換を強制するexplicit修飾子を指定することもできる[6]

応用例のようにif文while文の条件式などで用いるbool型変換演算子の定義として用いられるほか、アトミック変数のクラステンプレートの内部型への暗黙変換などにも用いられる[7]

std::unique_ptr<std::string> pointer( new std::string( "hoge" ) );
// 以下では std::unique_ptr::operator bool() が呼ばれる。
if( pointer ) { /* pointer 内部で管理されるポインターが NULL でない場合 */ }
Type operator->() アロー演算子 Type 型を返す。通例、ポインターType*を返し、Typeのメンバーを使用できるように実装することで、疑似的なポインター型を定義することができる。

応用例のようなスマートポインターや排他[要説明]などのためによく用いられる。

std::shared_ptr< std::vector<int> > pointer( new std::vector<int>() );
// 以下では std::shared_ptr::pointer operator->() が呼ばれる。
pointer->push_back(0);
// push_back() は std::vector のメンバーだが、std::shared_ptr のメンバーではない。

使用可能な言語

独自の演算子定義 種類の制限 多重定義
FORTRAN 可能 なし あり
ALGOL 可能 なし あり
Smalltalk 可能 なし なし
C++ 不可 あり あり
Prolog 可能 なし なし
Haskell 可能 あり なし
OCaml 可能 あり あり
Scala 可能 あり あり
Ruby 不可 あり なし
Python 不可 あり なし
C# 不可 あり あり
Visual Basic .NET 不可 あり あり
PL/SQL 不可 なし あり
PL/pgSQL 不可 なし あり
Nim 可能 なし あり
Swift 可能 あり あり
Kotlin 不可 あり なし

表内の注記

独自の演算子定義 組み込みの演算子として存在しない識別子を演算子として定義できる。
種類の制限 標準の演算子の中に再定義できない演算子が存在する。ただし代入式についてはC++以外で定義できる言語は殆どないため制限としていない。
多重定義 演算子として多重定義が可能であること。関数の多重定義ができても演算子は多重定義できない場合もある。

オブジェクト指向と利用者定義演算子

いくつかのオブジェクト指向言語ではFORTRANと同じ可読性と記述性の観点で導入されているが、多くのオブジェクト指向言語においては数値型とオブジェクトを同一の関数(あるいはメソッド)で処理する観点で導入されておりそれらの言語では必須の機能となっている。

純粋なオブジェクト指向言語(特にSmalltalk)においてオブジェクトに対する操作は全てメソッドで受信可能なメッセージとして表されるべきであり、セレクターが記号になっているだけのメッセージである演算子は特別扱いすべきではない。このため優先度の違いはあるものの演算子は単なるセレクターの一種として位置づけられている。

利用者定義演算子による多態性の例

Smalltalkによる例:

| center result |

"中央位置を返すブロックオブジェクト"
center :=
[ :edge0 :edge1 |
    ( edge0 + edge1 ) / 2.
].

"スカラー値同士の中央位置を算出"
result :=
    center
        value: 10
        value:  5.

"座標同士の中央位置を算出"
result :=
    center
        value: 10 @ 10
        value:  5 @  5.

C++による例:

// 中央位置を返す関数
template< class Type > auto Center( Type const &edge0, Type const &edge1 )
{
    return ( edge0 + edge1 ) / 2; // 除算演算子の実体はCenterの引数によって変わる
}

int main(void)
{
    int value;
    
    // スカラー値同士の中央位置を算出
    int result0 = Center( 10, 5 );

    // アドレス同士の中央位置を算出
    off_t result1 = Center( &value + 10, &value + 5 );
    
    // 複素数同士の中央値を算出
    std::complex< double > result2 = Center( std::complex< double >( 10, 10 ), std::complex< double >( 5, 5 ) );

    return EXIT_SUCCESS;
}

脚注

  1. ^ https://web.kudpc.kyoto-u.ac.jp/Archives/PDF/NewsLetter/kouhou_f90_3.pdf
  2. ^ http://www.rs.kagu.sut.ac.jp/yama/f90/INTERFACE_.html
  3. ^ https://msdn.microsoft.com/en-us/library/ds533389.aspx
  4. ^ https://docs.oracle.com/database/121/SQLRF/operators007.htm#SQLRF51172
  5. ^ メッセージとメソッドを紐づける名前。Smalltalkでは一つのメソッドに複数のセレクターをつけることができる。
  6. ^ 明示的な型変換演算子のオーバーロード - cpprefjp C++日本語リファレンス
  7. ^ atomic::operator T - cpprefjp C++日本語リファレンス

関連項目

Strategi Solo vs Squad di Free Fire: Cara Menang Mudah!