14.4 コピーがコントロール可能なハンドルの改良 if (cp) cp->regrade (final, thesis) ; else throw run—time—error("regrade Of unknown ; 265 14.4 コピーがコントロール可能なハンドルの改良 コピーをコントロールできる ptr ハンドルは便利なものですが、すべての望ましい機能があるわけではあり ません。たとえば、第 12 章で考えた str クラスを ptr を使って定義し直してみましよう。 12.3.4 では、 2 つ の str オプジェクトをつなげるときには、たくさんの文字のコピーを非明示的にしていることを見ました。参 typedef Vec く char> : : size—type size—type; / / インタフェ - スは以前と同し return *this ; std: :back-inserter(*data) ) ; std: :copy(s . data->begin() , s . data¯>end() , data. make-unique ( ) ; Str& operator + =(const Str& s) { public : friend std: : istream& operator>> (std: : istream&, class Str { / / これはうまくいくでしようか ? 照カウンタ付きの str クラスを使うことで、いくつかのコピーは避けられると考えるかもしれません。 Str&) ; size—type size() const { return data¯>size ( ) ; } const char& operator ロ (size—type i) const { return (*data) return (*data) [i] ; data. make—unique ( ) ; char& operator ロ (size-type i) { / / 必要なときには make-unique を使う std: :copy(), j, std: :back-inserter(*data)) ; template く class 工Ⅱ > str()n i, ln j) : data(new Vec く char>) { Str(size—type Ⅱ , char c) : data(new Vec く char>(ll' c)) { } std: :back-inserter(*data) ) ; std: :copy(cp, cp + std: :strlen(cp) , Str(const char* (p) : data(new Vec く char>) { str() : data(new Vec く char>) { } / / コンストラクタの定義は変えて pt てを生成するようにした private : / / vector を指す ptr オプジェクト Ptr く Vec く char> > data; / / 12.3.2 、 12.3 . 3 にあるように定義 std: :ostream& operator くく (std: :ostream&, Str operator 十 (const str&, const Str&) ; const Str&) ;
268 第 14 章メモリを ( ほとんど ) 自動で管理する ・ ptr く t>:make-unique を使い、しかも ( たぶん、それがないなどの理由で ) T::c10ne を使いたくないな ら、 cl 。 ne く T > を特化して好きなように定義すればよい。 間接度の度合いを上げて ptr の振る舞いを細かくコントロールすることができるようになりました。最後に残 るのはもっと難しい部分です。それは、そもそも何をしたいかはっきり決めるということです。 14.4.2 コピーが必要なのはいつ ? この例の最後の部分は詳細に調べ直す価値があります。 operator ロの 2 つのバージョンを見直してみましょ う。片方は data. make-unique を呼び出し、片方は呼び出していません。この違いは何でしよう。 この違いは、その関数が const メンバかどうかに関連しています。あとの方の。 perator ロは co Ⅱ st なメン バ関数です。これはオプジェクトの内容を変えないという約束です。そして、 const char & を呼び出し側に戻 すことで、この約束を守っています。そのため、データオプジェクトである vec く char> オプジェクトを複数の str で共有していても問題が無いのです。いずれにしても、ここで得た値を使ってもとの str を書き換えるこ とはできないからです。 一方、もう 1 つの operatorC] は char & を戻します。これを使えば、ユーザは str の内容を変更することも できるわけです。その場合、たまたま同じデータオプジェクトを共有している str にまで変更の影響が行かない ようにしなければなりません。そのため、 make ー unique を呼び出してデータオプジェクトを今の str オプジェ クト専用のものとし、これを変更しても他の str オプジェクトに影響が無いようにしているのです。そして、そ れからその ve c 内の文字への参照を戻しているわけです。 14.5 詳細 テンプレート特化 : 特別なテンプレートの定義のように見えるが、 1 つ以上の型パラメータを省略し、変わり に特定の型を使う。いろいろなテンプレート特化の使い方はこの本の範囲を超えてしまう。しかし、そのような ものがあるということ、またコンパイル時の型の決定に有用であることは知っておくべきである。 課題 14 ー 0 この章のプログラムをコンパイルし、実行し、試してみてください。 14 ー 1 ptr く c 。 re > の比較関数の定義を書いてください。 14-2 ptr く core> オプジェクトを使って学生の成績処理プログラムを書き、試してみてください。 14 ー 3 最終バージョンの ptr を使って Student ー i Ⅱ fo クラスの定義を書き直し、それを使って 13.5 の成績処 理プログラムを書き直してください。 14-4 最終バージョンの ptr を使って str クラスの定義を書き直してください。 14 ー 5 上の問の str クラスに関して、 vec く str > を使った split や文字の絵の関数を考え、それらを使ったプロ グラムをコンパイルし、試してください。 14 ー 6 ptr クラスは実際に 2 つの問題を解決しています。それは参照カウンタの保持とオプジェクトの生成・破 棄の問題です。参照カウンタだけを考え、他には何もしないクラスを定義してください。それからそのクラ スを使って str の定義を書き換えてください。
266 第 14 章メモリを ( ほとんど ) 自動で管理する str のインタフェースは変えていませんが、実装は根本的に変えました。 vec オプジェクトを直接保持する代 わりに、 vec オプジェクトを指す ptr オプジェクトを保持するようにしたのです。これより、複数の str オプ ジェクトが同じデータオプジェクトを共有できるようになりました。コンストラクタは vec オプジェクトを適 当に初期化して生成し、 ptr オプジェクトをこれに結び付けます。読み込み専用でデータを変えない関数の定義 は前と同じです。もちろん、これらの関数は ptr に対して実行されるので、 ptr 内のポインタを通じて文字デー タを取り出さなければなりません。おもしろいのは、入力演算子、結合演算子、 const でない口演算子といっ た、 Str を変更する演算子関数です。 たとえば、 str : : operator + = を見てください。これはすでにある vec オプジェクトにデータを追加する演算 子なので、 data ・ make-unique() を呼び出しているのです。この関数呼び出しの後なら、 str オプジェクトの ptr は、自由に変更してよいオプジェクトのコピーを指すようになるわけです。 14.4 ュコードを変更できない型のコピー しかし残念ながら、 make-unique の定義には重大な問題があるのです。 template く class T> void Ptr く T> : : make_unique ( ) if (*refptr ! = —*refptr; refptr = new size—t ( 1 ) ; P = P? p->clone() : 0 ; / / に門題あり p ー > clone の呼び出しを見てください。今は ptr く Vec く char> > を使っているので、これは Vec く char> のメン バ関数 cl 。 ne を呼び出すことになります。しかし、 vec にそんなメンバ関数はないのです。 こまでの仕掛けでは、 clone は ptr が指すオプジェクトのメンバ関数でなけばなりません。そうすること で仮想関数の仕掛けが使えるからです。言い換えれば、 ptr がすべての派生クラスに対してきちんと働くために は、 clone という関数がそのクラスに定義されていることが非常に重要なのです。しかし、また、 vec の定義を 変えることはできません。 vec は vect 。 r の機能を部分的に実現するクラスとして書いたものです。 vector に は cl 。Ⅱ e などという関数はありません。これを vec に付け加えると、 vect 。 r の機能の一部を実現するという 前提も壊れてしまいます。どうすればよいのでしよう。 このような困った問題への解答は、しばしば、冗談めかして私たちがいう、ソフトウェア工ンジニアリング の基本定理にあります。それは、すべての問題は間接度の度合いを上げれば解決できる、というものです。問題 は、存在しないメンバ関数を呼び出そうとしていること、そしてそのメンバ関数を新たに書き加えることができ ないということでした。解答は、直接メンバ関数を呼び出すのではなく、オプジェクト生成のために呼び出せる グローバル関数を定義し、それを間に使うというものです。この関数の名前も clone とします。 template く class T> T* clone(const T* tp) return tp—>clone() ;
14.4 コピーがコントロール可能なハンドルの改良 そして、 make-unique メンバの定義を変えます。 template く class T> void ptr く T> : :make—unique() if (*refptr ! = 1 ) { —*refptr; refptr = new size—t(l) ; p = p? clone(p) : 0 ; / / メンバではなくグロ - バルな cl 。 ne の呼び出し 267 このような関数を定義し、 make ー u Ⅱ ique の定義に挟み込んでも、その動作が変わらないことはあきらかだと思 います。グローバルな clone の呼び出しは、コピーされるオプジェクトのメンバ関数である clone の呼び出 しになるからです。 make-unique は、メンバでない clone を呼び出し、その clone が P の指すオプジェクト についてそのメンバ関数を呼び出す、というように、間接度の度合いを上げたのです。 student-info のよう に C10 Ⅱ e をメンバに持っているクラスについては、これは何の得にもなりません。しかし、 str で見たように clone をメンバに持たないオプジェクトを ptr が指すような場合には、これが問題解決につながるのです。 こで次のような関数を定義します。 / / ptr く vec く char> > を有効にするための関数 template く > Vec く char>* c10ne (const Vec く char>* vp) return new Vec く char>(*vp) ; template く > を最初に書くことで、この関数はテンプレート特化 (template specialization) であることを示しま す。このようにすることで、特定の引数型に対して特定バージョンのテンプレート関数を定義できるのです。 れで、 vec く char> のポインタを引数にしたときは、他のポインタを引数にしたときと異なる動作をするように定 義できたことになります。 clone に vec く char > * 型引数を渡すと、コンパイラは上記の特別バージョンの cl 。Ⅱ e を呼び出すのです。他のポインタを引数に渡すと、コンパイラはテンプレートの clone から、「引数のポインタ に対して clone メンバ関数を呼び出す」関数を具体化します。特化したバージョンでは、引数から Vec く char> のコピーコンストラクタを使って、新しい vec く char> オプジェクトを生成します。このような clone の特化で は仮想関数の仕組みが使えませんが、 vec の派生関数は考えないのでこれは必要ありません。 こでしたことは、メンバ関数 clone への依存を弱めたということです。それはこの関数がメンバ関数とし て存在しない可能性もあるからです。間接度を上げることで、特定のクラスのコピーの仕方を clone の定義で 指示できるようにしました。メンバ関数、コピーコンストラクタ、あるいはまったく違う方法を使うように決め られるのです。テンプレート特化がなければ、 ptr クラスはデータオプジェクトの clone メンバ関数を使いま ・ ptr く t>:make-unique を使い、しかも T: :clone が定義されているなら、 make-unique は T: :clone を ない。 ・ Ptr く T > は使っても ptr く T>::make-unique を使わないなら、 T::c10ne は定義されていなくてもかまわ しかし、それは make ー u Ⅱ ique が使われるときだけです。これは次のようにまとめることもできます。 す。
12.3 Str の演算子 219 内部のデータ保持に vec を使っているので、 + = の定義は簡単です。 c 。 py 関数を使って右オペランドのコピーを 左オペランドである vec オプジェクトに付け加えているのです。また、普通の代入演算子と同様に、左オペラン ドのオプジェクトを戻すことにしました。 すると operator + の定義は。 perat 。 r + = を使って簡単に書けます。 Str operator 十 (const Str& s , const Str& t) Str r return て ; str をつなぐのはメンバでない関数であり、それは新しい str を生成します。新しい str は、 r というローカル な変数で s をコピーして生成します。この初期化では、 str のコピーコンストラクタが使われます。次に、 + = 演 算子を使って r に t をつなぎ、 r を ( また非明示的ですが、コピーコンストラクタを呼んで ) 戻しています。 12.3.4 複合工クスプレッション const str& 型のオペランドを取る文字列結合演算子を定義しました。工クスプレッションに文字へのポイン タが含まれる場合はどうなるでしようか。たとえば、 1.2 のプログラムで str を使ったらどうなるでしよう。 プログラムには次のようなコードがありました。 " He110 , 十 name 十 " ー " ・ const std: : string greeting ここで name は string オプジェクトです。同様に次のようなコードを書きたいと思います。 const Str greeting " He110 , ここでⅡ e は str とします。 + 演算子は左結合なので、 " 十 name " He110 , この工クスプレッションは、まず、 を評価し、その結果と「 ! 」に + 演算子を適用するのと同じです。これは次のようにも表現できます。 ( " He110 , 十 name) 十 " ー " 12.2 で見たように const char * 型の引数を取るコンストラクタを定義することで、 const char* から str 12.3.3 では引数の型が str のものを定義しましたが、 const char * のものは定義しませんでした。しかし、 右が逆になっても、どちらも const char* と str に対して、 + 演算子を適用していると言えます。 合した結果である str オプジェクトが左ペランドになって、文字列リテラルが右オペランドになるものです。左 ラルが左オペランドになり str オプジェクトが右オペランドになるというものです。もう 1 つは、文字列を結 ェクスプレッションをこのように分解することで、 2 つの異なる + があることがわかります。 1 つは文字列リテ コンパイラが const char * 型の引数を str に変換し、それから + 演算子 への変換が定義されたことになります。あきらかに、 str クラスはこのような工クスプレッションを処理できる このような型変換の意味を理解することは重要です。たとえば、 を呼び出すことになります。 のです。 + 演算子のどちらの場合でも、
218 第 12 章値のように振る舞うクラスオブジェクト 別な理由は見当たりません。さらに、いくつかの文字列を続けてつなげていけるようにしたいのです。これは sl 、 s2 、 s3 を str オプジェクトとすると、次のような工クスプレッションを可能にしたいということです。 sl + s2 + s3 このように使うためには、この演算子の戻すものは str であるとわかります。 以上の考察から、この演算子は次のようなメンバでない関数として実装することになります。 Str operator + (const Str&, const Str&) ; この定義の前に少し考えてみると、。 pert 。 r + を定義するなら。 perat 。 r + = も定義したほうがよいと思うかもし れません。たとえば、 str クラスのユーザが、 s と s 1 をつないだものをあらためて s にしたいと考えるかもし れません。これは次のような形にしたいものです。 S 十 S 1 ; S S 十 = S 1 ; 実は、後で、。 perat 。 r + の定義を書くには、。 perat 。 r + = を先に書いたほうが簡単だとわかります。単純な + 演算 子と違って、複合型演算子である + = は左辺のオペランドを変えてしまいます。そのため、これは str クラスのメ ンバ関数にしましよう。文字列をつなぐ新しい関数の定義を付け加えて、 str の最終版は次のようになります。 class Str { / / 12 . 3 . 2 で定義した人力演算子 friend std: : istream& operator>>(std: : istream&, str&) ; public : Str& operator + =(const Str& s) { std::copy(s . data. begin(), s . data. end(), std: :back-inserter(data) ) ; return *this ; / / 前と同し typedef Vec く char> : : size_type size_type; Str(size-type Ⅱ , char c) : data(), c) { } Str(const char* (p) { std: :copy(cp, cp + std: :strlen(cp) , std: :back-inserter(data)) ; size—type size() const { return data. size() ; } const char& operator [ ] (size—type i) const { return data[i] char& operator ロ (size-type i) { return data[i] ; } std: :copy(), j, std: :back-inserter(data)) ; template く class ln> Str()n i, ln j) { private : Vec く char> data; / / 12 . 3 . 2 で定義したもの std: : ostream& operator くく (std: : ostream&, Str operator 十 (const Str&, const Str&) ; const Str&) ;
12.3 str の演算子 215 す。入力演算子は、すでにあるオプジェクトに新しい値を読み込むために使うものだからです。したがって、入 カ演算子は str クラスのメンバ関数がよいと考えるかもしれません。しかし、そうすると期待通りの動作をし なくなるのです。 この理由を考えるのに、工クスプレッションのオペランドがオーバーロードされた演算子関数にどのように結 び付けられるかを思い出してみましよう 11.2.4 ) 。 2 項演算子の場合、それがメンバ関数でなければ、左オペ これは S ; 初のパラメータ ( 左オペランド ) はいつもメンバ関数に非明示的に渡されるものです。そのため、 ランドは常に始めの引数と解釈され、右オペランドは 2 番目の引数とされます。演算子がメンバ関数の場合、最 とあれば、 Cin > > / / 以前と同様 size—type size() const { return data. size() ; } public : class Str { ただ、上のように演算子を定義するには、 str に新しくサイズを戻す関数 size を付け加える必要が出てきます。 return OS ; OS くく s[i] ; for (Str : : size—type i ostream& operator くく (ostream& OS , const Str& s) 出力演算子の定義は簡単に書けます。これは str のデータである文字を 1 っすっ書き出すだけです。 std: :ostream& operator くく (std: :ostream&, const Str&) ; / / 追加した std: :istream& operator>>(std: :istream&, Str&) ; / / 追加した そこで入出力演算子の宣言を加えて str クラスの定義があるファイル、 str . h などを更新しましよう。 もメンバ関数にできないと結論できるのです。 と同じです。これは標準ライプラリの使用方式と違ってしまいます。そのため、入力、そして同様に出力演算子 S > > cin; あるいは、これは S. operator>>(cin) ; し、 operator>> を str のメンバ関数にすれば、次のようにそれが使えます。 もちろん、 i stream の定義を変えることはできませんので、この演算子を付け加えることもできません。も クラスのメンバでなければならないとわかります。 のように c in のオーバーロードされた演算子 > > を使うのと同じ意味になります。これから、 > > 演算子は istream Ci11. operator>>(s) ;
第 12 章値のように振る舞うクラスオブジェクト Str greeting " He110 , は、次のように書いた場合の greetings と同じ値を持っことになるのです。 220 Str temp1("He110, Str temp2 templ + name ; Str temp3(" ! " ) Str greeting = temp2 + temp3; / / Str : : Str(const char*) / / operator + (const Str&, const str&) / / Str : : Str(const char*) / / operator + (const Str& , const Str&) このようにたくさんの一時オプジェクトを使うところを見ると、この方法は効率が悪いと考えるかもしれませ ん。実際、このような一時オプジェクトを生成するコストを考えて、商用の開発環境で使われる string は、し ばしば、コンストラクタによる自動の型変換を利用せず、個別の型に対するバージョンを定義するという手間の かかる方法をとっています。 12.3.5 2 項演算子をデザインする 2 項演算子の定義における型変換の役割は重要です。もしクラスが型変換を定義しているなら、普通は 2 項演 算子をメンバ関数でないように定義するのがよいでしよう。そうすることで、オペランドの型を左右入れ替えて も使えるからです。 もし演算子がクラスのメンバなら、左オペランドには、自動の型変換の結果で生成されるオプジェクトを使う ことはできません。その理由は、プログラマが書いた x + y のような工クスプレッションに対し、コンパイラは operator + をメンバに持つなんらかの型に x を変換できるかどうかのチェックをしないからです。コンパイラ ( とプログラマ ) は非メンバである operator + 関数と x のクラスのメンバである operator + 関数のみをチェッ クするのです。 メンバでない演算子の左オペランドと、いずれの演算子でもその右オペランドは、普通の関数の引数と同じ ルールに従います。これらのオペランドは、パラメータの型に変換できる型ならなんでもよいのです。もし 2 項 演算子をメンバ関数として定義すると、その左右オペランドに許される型が異なってしまうのです。つまり右オ ペランドは型変換でパラメータの型になればよいのですが、左オペランドはそうはできないわけです。これは + = のように最初から非対称な演算子では問題になりません。しかし、対称性が予想される演算子に対しては、混乱 とエラーを招いてしまいます。そのような演算子は、たいていの場合、左右オペランドに対して対称に定義する ことが望ましいのです。そして、それはその演算子をメンバでない関数として定義することによって実現できる のです。 ただし、 2 項演算子の中でも代入演算子については、左オペランドをそのクラスのオプジェクトに限定するべ きでしよう。そうでなければ何が起こるでしようか。左オペランドに型変換を許すと、オペランドをクラスオプ ジェクトに変換し、新しい値をその一時オプジェクトに代入することになります。しかし、それは一時オプジェ クトですから、代入が完了すると、今代入したばかりのオプジェクトにアクセスする方法がなくなるのです。し たがって、代入演算子自身を含めて、 + = などの複合代入演算子はクラスのメンバであるべきなのです。 12.4 変換が危険な場合 11.2.2 でサイズを引数にとるコンストラクタに explicit 宣言をしたことを思い出してください。今、引数 を 1 つしか取らないコンストラクタは型変換を表すということを学んだので、コンストラクタを explicit に
12.3 str の演算子 217 に付け加えていきたいのです。これは次の d 。 while ループ 7.4.4 ) で行っています。前のループで読み込ん だ最後の文字も data に付け加え、それからファイルの終わりか空白が出るまで文字を読み続けます。空白でな い文字を読み込むたびに、 push ー back 関数を使って data に文字を付け加えています。 その後、 is から読み込みができなくなるか、空白を読み込むと d0 while ループは終了します。後者の場合、 すでに 1 文字 ( 空白文字 ) を読み込みすぎたことになるので、これを is . unget ( ) でストリームに戻します。 u Ⅱ get 関数は get で最後に読み込んだ文字を入力ストリームに戻してしまう関数です。 unget を呼び出した後 は、ストリームはその前の get がまったく実行されなかったのと同じ状態に戻ります。 このコードはコンパイルできません。問題は operator>> が Str クラスの 実は、コメントしてあるように メンバでないため、 s のデータメンバである data にアクセスできないのです。 9.3.1 でも、 comapare 関数が student ー i Ⅱ f 。オプジェクトのメンバである n e にアクセスする必要があるという、同じような問題に直面し ました。そのときはアクセス関数を付け加えることで問題を解決しました。しかし、今は data に読み込み専用 のアクセス関数を付け加えるだけでは足りません。入力演算子は data を読み込むのではなく、 data にデータ を書き込むからです。入力演算子は str という抽象化の一部であるので、 data に書き込みアクセスさせること は問題ありません。しかし、すべてのユーザが data に書き込みできるようにはしたくありません。そのため、 operator>> に ( そして他のユーザに ) 利用可能な、 data にデータを入れる public メンバを付け加えることは できないのです。 そこで (public な ) アクセス関数を付け加えるのではなく、入力演算子を str クラスの frie Ⅱ d ( フレンド ) というものにすればよいのです。 friend に指定されたものは、そのクラスのメンバと同様に他のメンバにア クセスできるようになるのです。つまり、入力演算子を friend に宣言し、メンバ関数のように str クラスの private メンバにアクセス可能にすることができるのです。 class Str { friend std: : istream& operator>> (std: : istream&' Str&) ; / / 以前と同様 これは str クラスの定義に friend 宣言を付け加えたものです。この宣言は、 istream& と str & を引数に取る バージョンの operator > > は、 Str の private メンバにもアクセスできるという意味です。この宣言を str に 付け加えることで、入力演算子のコンパイルが可能になります。 frie d 宣言はクラスの定義のどこに書いてもかまいません。これが private や public のラベルの後にあっ ても何も変わりません。 friend 関数は特別なアクセスができるので、クラスのインタフェース部分と考えるこ とができます。そこで、 friend 宣言は、クラス定義の始めの方で、 public インタフェースのそばにまとめて 置くのがよいでしよう。 12.3.3 他の 2 項演算子 str クラスでまだ残っているのは + 演算子の実装です。しかし、その前に、いくつか決めるべきことがありま す。この演算子はメンバであるべきでしようか。そのオペランドの型は何でしようか。戻り値は何にしましよう か。後で説明しますが、これらの問には微妙な意味があるのです。 これらの問の答を推量してみます。やりたいことは、 2 つの str オプジェクトをつなげられるようにすること です。また、つなげるときに両方のオペランドの内容は変えません。これらから、この演算子をメンバにする特
12.2 自動の変換 213 てよいのです。たとえば、これがどのように働くのか、デストラクタを例に考えてみてください。もし、デスト ラクタを付けてもする仕事はないはすです。一般に、デストラクタが必要でないクラスでは、コピーコンストラ str オプジェクトは値のようになります。つまり、 str オプジェクトをコピーすると、 こまででコンストラクタを定義し、コピー、代入、破棄もどうするか決めました。これらの関数のおかげで 12.2 自動の変換 クタと代入演算子もわざわざ書く必要はないのです 11.3.6 ) 。 オリジナルとコピーが独 型の変数に代入することもできます。 れます。たとえば、 i Ⅱ t 型の値で d 。 uble 型の変数を初期化することができます。また、 int 型の値を double 立したものになるわけです。すると、次の問題は型の変換です。組み込み型では、しばしば自動で型変換が行わ double d = 10 ; double d2; 10 ; d2 / / 10 を d 。 uble に変換し、それで d を初期化 / / 10 を d 。 uble に変換し、それを d に代人 今考えている str クラスでは、 const char* から str を生成するには、 / / 新しい値を s に代人 / / t を初期化 こでは const char* を引数に取るコンストラクタを明示的に使って s を生成してい / / s の生成 S " he110 " Str t " he110 " ます。また、次のように書くこともできます。 と書くことができます。 Str s("hello"); const char* の st てへの代入ではまさにこれが行われています。 s " he110 " ; とあるとき、実際にはコンパ タです。 str が必要な場所で const char * が使われていると、コンパイラがこのコンストラクタを使うのです。 str にはすでにそのようなコンストラクタがあったのです。それが、 const char* を引数に取るコンストラク です。これは引数が 1 つだけのコンストラクタを定義することで定義されるのです。 型への変換です。後者の変換は 12.5 で説明しましよう。より一般的なのは、今考えている他の型からの変換 クラスの型変換には 2 方向があります。 1 つは他の型からその型への変換であり、もう 1 つはその型から他の 換に基づいて変換するのです。 変換するかを示すものです。組み込み型のときと同様に、コンパイラは必要なときに値の型をユーザ定義の型変 変換 (userdefinedconversion) になるのです。ユーザ定義の型変換とは、クラスオプジェクトをどのように型 const char* を引数に取るコンストラクタが定義されているからです。このコンストラクタがユーザ定義の型 考えるべきだと思うかもしれません。しかし、幸い、何もする必要はないのです。それは、 Str にはすでに str に新たに const char* を引数に取る代入演算子を定義し、コピーコンストラクタのオーバーロードも 上の 2 つの例は、どちらも const str& の来るべきところに、文字列リテラルを使っているのです。 算子を意味します。その代入演算子はコンパイラが生成するものです。この引数も const Str& です。つまり、 に取るコピーコンストラクタを使います。 2 番目のステートメントは定義ではありません。 こでは = は代入演 最初のステートメントは t の定義であり、 = は初期化を意味します。この形の初期化は常に c 。Ⅱ st str& を引数 11.3.3 で説明しましたが、 = という記号には上のように異なる 2 つの意味があることを思い出してください。