忘れたときに備えた記録

トップ «前の日記(2008-01-21(Monday)) 最新 次の日記(2008-01-25(Friday))» 編集
2005|02|03|04|05|06|07|08|09|10|11|12|
2006|01|02|03|04|05|06|07|08|09|10|11|12|
2007|01|02|03|04|05|06|07|08|09|10|11|12|
2008|01|02|03|04|05|06|07|08|09|11|12|
2009|01|02|03|04|05|06|10|12|
2010|06|07|08|12|
2011|07|09|
2012|09|11|
2013|02|03|09|
2015|10|11|
2016|01|08|11|
2017|02|08|10|
2018|11|

2008-01-22(Tuesday)

うっかり

C++のテンプレートクラスのフレンド関数について少し調べて書いていたのですが、結論が間違っていたので全部キャンセルしてしまいました。(´・ω・`)ショボーン

やっぱり残しておきます

途中の考察を後で使うような気がしてきたので、やっぱり書いてしまうことにします(汗

というわけで、改めて…

templateクラスのfriend関数

昨日、C++入門セミナーにTAっぽい役割で参加したのですが、その時に次のような例が話題になりました。

テンプレートクラスのプライベート変数にアクセスできるフレンド関数は、どのように宣言するか?

例えば

/// test1.cpp
#include <iostream>

template <class C> class Test
{
    C v;
public:
    Test(const C &a){
        v = a;
    }
    friend std::ostream& operator<<(std::ostream &os, const Test<C> &t);
};

template<class C> std::ostream& operator<<(std::ostream &os, const Test<C> &t)
{
    os << t.v;
    return os;
}

int main()
{

    Test<int> o(1);

    std::cout << o << std::endl;

    return 0;
}

というソースをコンパイルするにはどうした良いか?という話です。

ざっとググったところ、

あたりが引っかかります。

Tags: C++

test1.cppのコンパイル

まず、上の例(test1.cpp)をそのままコンパイルしようとすると、

test1.cpp:10: 警告: friend declaration ‘std::ostream& operator<<(std::ostream&, const Test<C>&)’ declares a non-template function
test1.cpp:10: 警告: (if this is not what you intended, make sure the function template has already been declared and add <> after the function name here) -Wno-non-template-friend disables this warning
/tmp/ccOP5KR2.o: In function `main':
test1.cpp:(.text+0xab): undefined reference to `operator<<(std::basic_ostream<char, std::char_traits<char> >&, Test<int> const&)'
collect2: ld はステータス 1 で終了しました

というコンパイル時の警告とリンク時のエラーが出ます。

警告の方は、friend宣言した関数がテンプレート関数ではないと言ってきており、テンプレート関数にするには関数テンプレートをあらかじめ宣言し、ここ(friend宣言)の関数名の直後に<>を追加せよとアドバイスしてきています。

また、リンク時のエラーによると、

std::cout << o

の部分を実行するのに必要な、Test<int>を引数にとるoperator<<関数がないと言ってきています。

アドバイスに従って書き換えてみる

そこで警告メッセージに従って、冒頭にテンプレート関数の宣言を追加します。関数でテンプレートクラスの名前が必要なので、テンプレートクラスの宣言も追加します。

できたソースはこんな感じ(main関数は同じなので省略)。

/// test2.cpp
#include <iostream>

template <class C> class Test;
template <class C> std::ostream& operator<<(std::ostream &os, const Test<C> &t);

template <class C> class Test
{
    C v;
public:
    Test(const C &a){
        v = a;
    }
    friend std::ostream& operator<<<>(std::ostream &os, const Test<C> &t);
};

template<class C> std::ostream& operator<<(std::ostream &os, const Test<C> &t)
{
    os << t.v;
    return os;
}

これでコンパイル時の警告が消えて、リンクも無事行われ、問題なく動作します。

でも

template <class C> class Test;
template <class C> std::ostream& operator<<(std::ostream &os, const Test<C> &t);

これは美しくないでしょう!?クラス定義の前に、まずクラス名だけ宣言して、friendする関数は2回も宣言するなんて。あんまりです。(しかしやっぱり、この方法しかないみたいです。残念無念)

もちろん、関数宣言の代わりに、そこに直接、関数operator<<の実装を記述すれば関数宣言はいらなくなります。それでも、最初のクラス宣言は残ってしまって、美しくないことに違いはありません。

と言うわけで、このクラス定義の前の宣言がいらない書き方がないものか、調べてみました。

宣言だけ省いてみる

まずは、コンパイルが成功したtest2.cppから、美しくない宣言だけ削ってコンパイルしてみます。削ったのは先頭のこの2行です。

template <class C> class Test;
template <class C> std::ostream& operator<<(std::ostream &os, const Test<C> &t);

するとこんなエラーが出てきました。

test2.cpp:10: error: declaration of ‘operator<<’ as non-function
test2.cpp:10: error: expected ‘;’ before ‘<’ token
test2.cpp: In function ‘std::ostream& operator<<(std::ostream&, const Test<C>&) [with C = int]’:
test2.cpp:24:   instantiated from here
test2.cpp:5: error: ‘int Test<int>::v’ is private
test2.cpp:15: error: within this context

つまり

friend std::ostream& operator<<<>(std::ostream &os, const Test<C> &t);

この宣言が関数ではないと見なされて、そのせいでoperator<<関数の実相の中でTest<C>.vを参照しようとしてもprivateだから参照できませんよと言うわけです。

test1.cppのリンクが成功するようにしてみる

そもそも、なぜtest1.cppのリンクが失敗するのかを調べてみました。色々試した挙句、次の関数をtest1.cppに追加すると、コンパイル時の警告は残ったままですがリンクにも成功するようになりました。

std::ostream& operator<<(std::ostream &os, const Test<int> &t)
{
    os << t.v+1;
    return os;
}

つまり、

friend std::ostream& operator<<(std::ostream &os, const Test<C> &t);

の宣言は、テンプレート関数をfriend登録するのではなく、普通の関数をfriend登録する千件として解釈されているわけです。

察するに、このソースのコンパイルは次のような手順で行われているのでしょう

  1. テンプレートクラスTestの実装を読み込んで解析する(この時点でfriend行の警告が出る)
  2. Test<int> o(1); の変数宣言によって<int>版のTestクラスが実体化され、それと同時にTest<int>クラスのfriendとしてoperator<<(... Test<int> &t)が登録される
    1. テンプレートクラスoperator<<は、実体化されないためにエラーにならない
    2. std::cout << o(1) では、普通の関数版のoperator<<が呼び出される(ので、最初のtest1.cppではリンクエラーになる)

template関数をfriend宣言する

そんなわけで、関数のフレンド宣言で直接テンプレート関数が指定できればいいのだろうと考え、あれこれ試した挙句

#include <iostream>

template <class C> class Test
{
   C v;
public:
   Test(const C &a){
      v = a;
   }
   template<class D> friend std::ostream& operator<<(std::ostream &os, const Test<D> &t);
};

template<class C> std::ostream& operator<<(std::ostream &os, const Test<C> &t)
{
   os << t.v;
   return os;
}


int main()
{

   Test<int> o(1);
   std::cout << o << std::endl;

   return 0;
}

というソースを書くに至りました。

template<class D> friend std::ostream& operator<<(std::ostream &os, const Test<D> &t);

がポイントで、こう書くことで、test2.cppのような美しくない宣言が不要になります。

やったぜ!えうれか!!




・・・・・と思っていたのですが。

改めてよくよく参考リンク先を再読してみたら、[cppll:10716] Re: instantiate されない friend-function-templateですでに言及されている上に、 Test<int>のインスタンスで呼び出された関数からTest<double>のプライベート変数にアクセスできるという深刻な問題まで指摘されていました。

結論: もっとよく落ち着いて調べましょうorz

テンプレート関数と普通の関数の優先順位

せっかくなので、もうちょっと考察を書き留めてみましょう。

#include <iostream>

template <class C> class Test
{
    C v;
public:
    Test(const C &a){
        v = a;
    }
    template<class D> friend std::ostream& operator<<(std::ostream &os, const Test<D> &t);
    friend std::ostream& operator<<(std::ostream &os, const Test<int> &t);
};
Test<double> gd(1000);

template<class C> std::ostream& operator<<(std::ostream &os, const Test<C> &t)
{
    os << t.v;
    return os;
}
template<> std::ostream& operator<<(std::ostream &os, const Test<int> &t)
{
    os << t.v+gd.v;
    return os;
}

std::ostream& operator<<(std::ostream &os, const Test<int> &t)
{
    os << t.v*100+gd.v;
}

int main()
{

    Test<int> o(1);
    std::cout << o << std::endl;

    return 0;
}

このソースで、さっきの「Test<int>からTest<double>のプライベート変数にアクセスできる」問題が再現できます(確認するには、普通の関数の方のfriend宣言と実装をコメントアウトする必要があります)。

もう一つ。このソースをコンパイルして実行すると、

1100

と出てきます。つまり、テンプレート関数(およびその特別版)と、(引数にテンプレートクラスの特別版をとる)普通の関数とを用意しておくと、後者が優先して使われることが分かります。

宣言を減らす方向で書き直してみる

せっかくだから、俺はこの赤の扉を選ぶぜ!もう少し色々試してみます。

まず、宣言を記述する場所を変えてみました。

#include <iostream>

template <class C> class Test
{
    C v;
public:
    Test(const C &a){
        v = a;
    }
};

template<class C> std::ostream& operator<<(std::ostream &os, const Test<C> &t)
{
    os << t.v;
    return os;
}

template <class C> class Test
{
    friend std::ostream& operator<<<>(std::ostream &os, const Test<C> &t);
};

関数の定義を書いてから、その関数のfriend宣言をするわけです。rubyのprivateの書き方に似ててちょっと良いかなあと。

で、コンパイル。

test3.cpp:19: error: redefinition of ‘class Test<C>’
test3.cpp:5: error: previous definition of ‘class Test<C>’

はい。Rubyじゃないんだから、クラスを複数箇所に分散して書けはしないのでした。

friend宣言したら負けかなと思っている

そもそもfriend関数が必要な構造が間違っているんだ!というわけで、次のように書いてみました。

// test4.cpp
#include <iostream>

template <class C> class Test
{
    C v;
public:
    Test(const C &a){
        v = a;
    }
    void to_stream(std::ostream &os) const
    {
        os << v;
    }
};

template<class C> std::ostream& operator<<(std::ostream &os, const Test<C> &t)
{
    t.to_stream(os);
    return os;
}

int main()
{

    Test<int> o(1);

    std::cout << o << std::endl;

    return 0;
}

出力に使うためのメンバ関数を用意して、operator<<関数でそれを呼ぶわけです。RubyのObject#to_strみたいなノリで。

少なくともoperator<<については、これが一番良いんじゃないかなという気がしてきましたが、どうでしょうか?

本日のツッコミ(全3件) [ツッコミを入れる]
_ tueda (2009-06-08(Monday) 11:41)

私も<br> cout << 何か<br>は、別途メンバー関数として<br> std::ostream& print (std::ostream& out);<br>を用意して operator<< () の中で呼び出しています。<br><br>もっとエレガントかつ根本的な解決方法として、<br>「privateは使わず全部publicで宣言する」<br>というのがあります。個人的にはこれがベストアンサーです。<br>friendなんて要りません。

_ ひらく (2009-06-10(Wednesday) 19:43)

>「privateは使わず全部publicで宣言する」<br><br>これもまた思い切った手ですねぇ。メンバーの隠蔽とかは一切なしの方向ですか?

_ お幸 (2018-11-10(Saturday) 13:33)

素直に getter関数定義したほうが(ry <br> <br>public: <br> const C& v() const { <br> return v; <br> }

[]