9.4 フレンド/ADL

クラスは設定したアクセス領域によって厳密にアクセスレベルを指定できます。 特に、privateprotectedはアクセス領域を明確に制御します。

フレンド関数は、そのようなアクセス領域を完全に無視し、privateだろうがprotectedだろうがアクセスできてしまう関数です。その効果の通り、とても強力な機能であり、privateprotectedの機能を無駄にしてしまう側面もあるため、乱用は禁物です。

しかし、有効的に使える場合がいくつかあります。その殆どが、まだ説明していないこれから紹介する演算子のオーバーロードという項目で挙げる概念にあります。よって、これから説明する、特に必要もない通常の関数に対するフレンド指定は、正直なところあまりお勧めできるものではありません。ただ、簡潔的に機能を紹介するため、特に意味もない関数をこの項ではフレンド指定しています。

9.4.1 フレンド関数を定義する

フレンド関数はfriendキーワードによって指定します。関数をグローバルに呼び出したい場合、friendキーワードを用いてクラス内にフレンド関数を宣言し、クラスの外側に定義します。

#include<iostream>

struct X{
    X(int a=42):a_(std::move(a)){}
    constexpr int get()const noexcept{return a_;}
private:
    int a_;

    friend void assign(X&,int);
};

void assign(X& x,int a)
{
    x.a_=a; // プライベートメンバにアクセス
}

int main()
{
    X x;
    assign(x,42);
    std::cout<<x.get()<<std::endl;
}

実行結果は以下の通りです。

42

ご覧の通り、friend指定された関数はプライベートメンバにアクセスできている事が分かると思います。 フレンド関数を内部で定義した場合、ADLという仕組みを利用した場合にのみ、呼び出す事が可能となります。 この仕組みを用いた呼び出しは演算子のオーバーロードで多様しますが、ここで一度ADLについて学んでおきましょう。

9.4.2 ADL(Argument Dependent Lookup)

Argument Dependent Lookupは、直訳すると実引数依存の名前探索という意味です。まずは、以下のコードを見てみましょう。

#include<iostream>

namespace ns{
    struct X{};
    void f(X&&){std::cout<<__func__<<std::endl;}
}

int main()
{
    f(ns::X()); // fの呼び出しに名前解決を用いていない
}

実行結果は以下の通りです。

f

このコードに違和感を持ったのであれば、その違和感は正しいものです。まずXのインスタンス化のために、Xの名前解決のためns::というように記述しています。これについては問題がないように思えます。

問題は、関数fの呼び出しです。関数fの呼び出しに、名前解決を行っていないのにも関わらず、正しくコンパイルされ正常に関数を呼び出せています。とても不思議ですね。

これは、ある型のオブジェクトが関数呼出の際に実引数として用いられた時、関連するnamespaceからも、その関数が探索されるという仕様があるためです。この仕様をADLと言います。

関連するnamespaceは以下のような場合です。

  • 引数(テンプレート引数の型を含む)が、そのクラス自身、そのクラスの基底クラス(基底クラスについては後の「継承」の章で説明しています)、あるいはクラスを囲む、同一namespaceのメンバーである場合
  • 引数(テンプレート引数の型を含む)がnamespaceのメンバーである場合 ※テンプレート引数については後の「テンプレート」の章で解説しています。

このような仕様は何度も述べている通り、主に演算子のオーバーロードで有効なのです。有効なのですが、このような仕組みが邪魔になることもあります。例えば、以下のコードを見てください。

#include<iostream>

namespace ns {
    struct X{};

    void f(X&&){std::cout<<"ns::"<<__func__<<std::endl;}
}

void f(ns::X&&){std::cout<<"::"<<__func__<<std::endl;}


int main()
{
    f(ns::X());
}

このコードはコンパイルエラーとなります。ADLの機能によって、ns::fが関数呼び出しの候補に上がってしまい、グローバルな関数と全く同じシグネチャであるため、オーバーロード解決が曖昧となってしまうためです。 このようなコードでは、大抵その名前空間内の関数を呼び出したつもりなどないでしょう。これに対しては、以下のように、名前空間をネストする事でADLを回避できます。

#include<iostream>

namespace ns {
    namespace adl_firewall{ // 名前空間で囲む
        struct X{};
    }

    using namespace adl_firewall; // 方法1:using namespaceしておく
    using adl_firewall::X; // 方法2:using宣言しておく
    using X=adl_firewall::X; // 方法3:using/typedefで名前解決済みの型名を宣言しておく

    void f(X&&){std::cout<<"ns::"<<__func__<<std::endl;}
}

void f(ns::X&&){std::cout<<"::"<<__func__<<std::endl;}


int main()
{
    f(ns::X());
}

9.4.3 フレンド関数とADL

さて、フレンド関数の話に戻りましょう。フレンド関数をクラス内部に定義した場合、前述した通り、ADLのみで呼び出しが可能です。つまり、あるクラス内部に定義されたフレンド関数はそのクラス型を引数に受け付けなければ呼び出すことはできないという事になります。

#include<iostream>

struct X{
    friend void f(X&&) // 引数にXを受け付ける事でADLが働く
    {
        std::cout<<__func__<<std::endl;
    }
};

int main()
{
    f(X());
}

実行結果は以下の通りです。

f

9.4.4 フレンドクラス

フレンド指定は、クラスにも指定する事ができます。

#include<iostream>

struct X;

class Y{
    friend struct X;
    int a=42;
};

struct X{
    X(const Y& y){std::cout<<y.a<<std::endl;} // Yのプライベートメンバへアクセス
};

int main()
{
    Y y;
    X x(y);
}

実行結果は以下の通りです。

42

フレンド指定は、上記のように、friend struct Xというように行います。フレンド指定する際、そのクラスがclassキーワードを使って宣言されたクラスであればclassを、structキーワードを使って宣言されたクラスであればstructキーワードを用いる事が推奨されます。