「プログラミング言語C++第四版」について気が付いたことなど (11)

仮想関数にだって何らかの用途はあります …


20.4 抽象クラス

…(略)… Shapeなどの一部のクラスは、そもそもオブジェクトが存在することのない抽象概念を表現する。Shapeは、そこから派生したクラスの基底となることに存在価値がある。このことは、何らかの用途をもったものとして仮想関数を定義できないという事実が示している


原文:

Some classes, such as a class Shape, represent abstract concepts for which objects cannot exist. A Shape makes sense only as the base of some class derived from it. This can be seen from the fact that it is not possible to provide sensible definitions for its virtual functions:


試訳:

Shapeなどの一部のクラスは、対象物が存在することのない抽象概念を表現する。Shapeはそこから派生したクラスの基底となることだけに意味がある。このことはShapeクラスの仮想関数には意義のある定義を与えることが出来ないという事実からも見て取れる:


考察:

“sensible” の意味をどう捉えるか、上では「意義のある定義」と訳してみたが、「実用的な定義」とか「具体的な定義」とか、そんな感じに訳してもいいかも。いずれにしても仮想関数には使い道が無いなどということはない。

「プログラミング言語C++第四版」について気が付いたことなど (10)

コンストラクタだって「継承」したりはしない …


20.3.6 返却型緩和

…(略)…

しかも、コンストラクタは、通常の関数とはまったく異質なものだ。特に、通常の関数はメモリ管理ルーチンを継承しないが、コンストラクタは継承する。


原文:

Furthermore, a constructor is not quite an ordinary function. In particular, it interacts with memory management routines in ways ordinary member functions don’t.


試訳:

さらに、コンストラクタは通常の関数とは全く異質なものだ。特に、それ(コンストラクタ)は通常のメンバ関数とは異なるやり方でメモリ管理ルーチンとやり取りする。


考察:

“interacts with” を”継承”と訳すのはおかしい。”interact” を “inherit” と取り違えているような気がする。

「プログラミング言語C++第四版」について気が付いたことなど (9)

いや、大変だよ …


19. 2. 5  メモリ確保と解放

…(略)…

広域のoperator new()とoperator delete()を置きかえるのは、それほど大変ではないが、お勧めはしない。というのも、デフォルトの振舞いを想定しているプログラマがいるかもしれないし、広域のoperatornew()とoperatordelete()を置きかえているかもしれないからだ。


原文:

Replacing the global operator new() and operator delete() is not for the fainthearted and
not recommended. After all, someone else might rely on some aspect of the default behavior or might even have supplied other versions of these functions.


考察:

“is not for the fainthearted” は「気の弱い人向きではない」という意味なので、要するに「大変だよ」ということ。意味が逆になってる。訳すなら「広域のoperator new()とoperator delete()を置きかえるのは、気の弱い人向きではないし、お勧めでもない」か。

“or might even have supplied other versions of these functions.” は、こちらは意味はあってるのだけれども、「ひょっとしたらこれらの関数の別バージョンを提供しているかもしれない」と素直に訳した方が分かり易くないだろうか。

 

「プログラミング言語C++第四版」について気が付いたことなど (8)

特に、~なので、ということではなく


18.2.5  名前空間内の演算子

…(略)…

標準のiostreamライブラリは、組込み型出力用のメンバ関数<<を定義しているので、ユーザはostreamクラスを変更することなく、ユーザ定義型出力用の<<を定義できる ( § 38.4.2)。

18.3 複素数型


原文:

In Particular, the standard iostream library defines << member functions to output built-in types, and a user can define << to output user-defined types without modifying class ostream (§38.4.2)。


試訳:

とりわけ、標準iostreamライブラリは組み込みタイプ向けの出力用に<<メンバ関数群を定義しているが、ユーザはユーザ定義型の出力を行なう<<を、ostream クラスを変更することなく定義することが出来る。


考察:

どうも In particular の据わりが悪いが、要するに、iostreamクラスには沢山の組み込み型用のoperator<<()があるけれども、そんなの気にせずにiostreamクラスの外でユーザー定義型向けのoperator<<()は幾らでも定義できるよ、ということ。

「プログラミング言語C++第四版」について気が付いたことなど (7)

動かしてみると …


15. 4. 2   初期化 と 並行 処理

次 の 例 を 考えよ う:

int x = 3;
int y = sqrt(++ x);

x と y の 値 は いくつ に なる だろ う か?   その 答え は 明らか に “3 と 2 !” だ。


そうかなあ? と思って main()を書いて動かしてみると …

#include <iostream>
#include <math.h>

using namespace std;

int x = 3;
int y = sqrt(++x);

int main(int,char**)
{
  cout << "x=" << x << ", y=" << y << endl;
  return 0;
}

案の定、

x = 4, y = 2

と表示されるのであります。(Visual C++ 2013で確認)


考察:

int y = sqrt(x+1);

なら疑問の余地無く“3 と 2 !” なんですけどね。

xが最初に初期化された時点(注)では 3 なので、だから x は3だと言えなくもない気はしますけど、普通は main に飛んでくる前の初期化の細かなタイミングや順序なんて誰も気にしませんし、外部変数の初期値というのはmainに制御が渡るまでに確定している値と考えるのが妥当だと思うので、y=sqrt(++x)でxが「幾つになるだろうか」と聞かれれば、4と答えるのが正解だと私は思います。

注: 「静的に割り当てられる定数式によってオブジェクトの初期化が行なわれるのはリンク時だから、x は 3 になる」とありますが、そうとは限りません。プログラムコードを不揮発性メモリ(ROMとか)に置く必要がある環境ではリテラルや変数の初期値も一緒にROMに置く必要があります。しかしながら、ROMに置いたままでは変数の値を変更できませんから、初期値はROM上に置いておき、それをmainの前の準備処理で実際の変数の場所(RAM)にコピーするというやり方が一般的だと思います。この場合、外部結合の変数 x の初期化が行なわれるのはリンク時ではなく、プログラムの実行時、つまり main の前です。

でもそもそも、extern 変数の初期化で他のextern変数の値を変更するようなコード書いちゃ駄目ですよね。

この節ではマルチスレッド環境ではもっと色々なことが起こり得るって書いてありますが、静的か動的かには関わらず、外部結合の変数が初期化されるタイミングでは通常はメインスレッド以外は動いていませんし、もし外部結合の変数のコンストラクタで幾つかスレッドを起動していたとしても(=mainの前に走っているスレッドがあったとしても)、int y=sqrt(++x) の初期化のためのコードがそれらのスレッドの起動の都度実行されるわけではありません。外部結合の変数の初期化は一回だけです。なので、「yがsqrt(4)にもsqrt(5)にも」とはならないのではないかと思います。

むしろそれよりは、mainの前に走り出すようなスレッドでは少なくとも動的に初期化される外部結合の変数の初期化がスレッドが走り始めたタイミングで全部は終わっていない可能性があり、それはそれで危ない話ではないかと思われ、なのでコンストラクタでスレッドを起動するのはやめておいた方が無難でしょう。

 

「プログラミング言語C++第四版」について気が付いたことなど (6)

分かりにくい訳


15.3.2.1 電卓プログラムの残りのモジュール

電卓プログラムの残りのモジュールも、パーサと同様に構成できる。ただし、いずれも小さいので、専用の _impl.h は不要だ。専用の_impl.hが必要になるのは、論理モジュールの実装が、共有コンテキスト(とユーザーに提供する機能)を必要とする関数が複数個になる場合だけである。


原文:

The remaining calculator modules can be organized similarly to the parser. However, those modules are so small that they don’t require their own _impl.h files. Such files are needed only where the implementation of a logical module consists of many functions that need a shared context (in addition to what is provided to users).


試訳:

電卓プログラムの残りのモジュールも、パーサと同様に構成できる。ただし、いずれも小さいので、専用の _impl.h のようなファイルは不要だ。このようなファイルは論理モジュールの実装が(ユーザに提供されるものとは別の)共有コンテキストを必要とする多数の関数により構成される場合にのみ必要となる。


考察:

邦訳では in addition to が暗に意味することがはっきり読み取れない。関数が幾つあっても、ユーザに提供するコンテキストと同じファイルで共有コンテキストを提供してよいなら 個別の _impl.h のようなファイルは必要無いし、また、単に「複数個」では 個別の _impl.h を使う根拠としては弱い。privateにしか共有されない情報が沢山の関数に跨っている時に限り、個別の _impl.h のようなprivateな情報共有の手段が必要になるというのが、たぶん著者の言いたいことだと思う。

「プログラミング言語C++第四版」について気が付いたことなど (5)

意味不明な訳


13.6.4.3 push_back()

…(略)…

あいにく、理にかなった一般的な値は存在しない。vectorを一度push_back()することがあれば、ほぼ間違いなくpush_back()処理が行なわれると想定できる。増加係数の2は、平均的なメモリ利用を最小限に抑える、数学的に最適な係数(1.618)よりも大きいため、メモリが乏しくないシステムであれば、よりよい実行時性能を期待できる。


原文:

As it happens, these are not unreasonable or uncommon values. The assumption is that once we have seen one push_back() for a vector, we will almost certainly see many more. The factor two is larger than the mathematically optimal factor to minimize average memory use (1.618), so as to give beter run-time performance for systems where memories are not tiny.


試訳:

ありがちだが、これらの値は理にかなっていないわけでも一般的でないわけでもない。vectorで一回でもpush_back()が使われれば、もっと沢山使われるのはほぼ確実であろう。増加係数の2は、平均的なメモリ利用を最小限に抑える数学的に最適な係数(1.618)よりも大きいため、メモリが乏しくないシステムであれば、よりよい実行時性能を期待できる。


考察:

not unreasonable or uncommonと二重否定になっているのを見落としていて、マジックナンバーのようだけど実は根拠はあるんだよと言っているのを逆の意味に捉えているように思う。The assumption is の文はどういうわけか many moreを訳していない。

「プログラミング言語C++第四版」について気が付いたことなど (4)

(原書の)コード例の間違い


13.6.4.1 reserve()

template < typename T, typename A >
void vector < T, A >:: reserve( size_ type newalloc) // 欠陥のある最初の版
{
  if (newalloc <= capacity()) return; // 確保分を減らすことはない
  vector < T, A > v( newalloc); // 新しい要素数でvectorを作る
  copy( vb. elem, vb. elem + size(), v. begin());// 全要素をコピー
  vb. space = size();
  swap(∗ this, v); // 新しい値を入れる
} // 古い値を暗黙裏に解放

原書のコード

template < typename T, typename A >
void vector < T, A >:: reserve( size_ type newalloc) // flawed first attempt
{
  if (newalloc <= capacity()) return; // never decrease allocation
  vector < T, A > v( newalloc); // make a vector with the new capacity
  copy( elem, elem + size(), v. begin());// copy elements
  vb. space = size();
  swap(∗ this, v); // install new value
} // implicitly release old value

邦訳でも原書でもvbはvectorの(vector_base型の)メンバ変数として (大分前の §13.6.2 の中ほどのコード例で)宣言されている。vectorがvector_baseから派生していのるであれば原著のようにvb.は要らないが、そうではないのでこれは邦訳の方が正しい

「プログラミング言語C++第四版」について気が付いたことなど (3)

どっちなの?


13. 5. 2   例外 の 捕捉

…(中略)…

原則として、例外は送出時にコピーされる(§13.5)。そのため、例外の保持と転送に対して、処理系はさまざまな方針を適用できる。とはいえ、newが、標準のメモリ不足例外bad_allocを送出するのに必要なメモリがあることは保証されている(§11.2.3)。


と書いてあるので、あれ? と思って §11.2.3 を見てみると、


11.2.3 メモリ領域の割り当て

…(中略)…

どれだけ多くのメモリがあっても、最終的にはbad_allocハンドラが実行される。ここで注意してほしいのは、物理メモリを使い果たすと、new演算子が例外を送出する保証がない、ということだ。


となっている。(太字強調は私)

§13.5.2の原文は

It is guaranteed, howerver, that there is sufficient memory to allow new to throw the standard out-of-memory exception, bad_alloc(§11.2.3)

§11.2.3の原文は

However much memory we have available, this will eventually invoke the bad_alloc handler. Please be careful: the new operator is not guaranteed to throw when you run out of physical main memory.

となっており、訳は間違っていない。

現実的にはbad_alloc例外がthrowされるような事態になることは稀で、なったらなったで適当に落ちてくれれば多くの場合さしたる不都合は無いと思うし、プログラム側で例外処理を書いたとしてもどのみちまともに動かない可能性の方が高いとも思うのだけれども、例外がthrowされる保証があるのか無いのかすっきりしないのは気持ちが悪い。

§13.5.2では「例外をthrowするために必要最低限のメモリはnewの処理として常に残してある」と言っているだけで、だからと言って実際に例外をthrowできるとは限らない、ということなのだろうか?

 

「プログラミング言語C++第四版」について気が付いたことなど (2)

非推奨って言われてもなあ (笑)


13.5.1.3 例外指定

古いC++コードでは、例外指定(exception specification)が使われている。たとえば:

void f( int) throw( Bad, Worse); // 送出する例外は Bad と Worse に限られる
void g( int) throw(); // 例外を送出しない

空の例外指定 throw() は、noexcept(§ 13. 5. 1. 1)と等価である。…(中略)… 空でないthrow 指定子は、使いこなすのが難しい上に、指定された例外が送出されたかどうかの判断のための実行時チェックが高コストになる可能性がある。この機能は、うまくいかなかったので、現在では非推奨とされている。利用し てはいけない。


感想:

これは翻訳の問題ではなくてただの感想ですが、noexcept以外は役に立たないということのようですけれども、標準規格に取り入れられた機能が「うまくいかなかったので非推奨」って言われても困るかも知れません。でも、そもそもユーザー例外って、大域エラー処理以外ではあまり使わないものですし、私はthrow()機能は殆ど使ってないので個人的には実は困りません。