読者です 読者をやめる 読者になる 読者になる

GotW #4: クラスのメカニクス(勝手訳)

GotW

第 4 回めの GotW の翻訳です。

例によって原文著者(Herb Sutter 氏)の許可は得ていませんし、私の英訳がヒドいクオリティである(用語の統一がとれていないとか、誤訳が含まれているとか)かもしれませんのでそこのところはご理解いただければと思います。


原文:http://herbsutter.com/2013/05/20/gotw-4-class-mechanics/


How good are you at the details of writing classes? This item focuses not only on blatant errors, but even more so on professional style. Understanding these principles will help you to design classes that are easier to use and easier to maintain.

あなたはクラスを書くことにどれくらい詳しいですか?この項目では明らかなエラーにフォーカスするだけでなくプロフェッショナルなスタイルについてもフォーカスします。これらの原理を理解することが使いやすく保守しやすいクラスのデザインを助けるでしょう。

Problem 問題

JG Question 見習いグルへの質問

1. What makes interfaces “easy to use correctly, hard to use incorrectly”? Explain.

1. インターフェースを "正しく使うのは容易で、間違って使うのが難しい" ものにするにはどうすればよいですか?説明してください。

Guru Question グルへの質問

2. You are doing a code review. A programmer has written the following class, which shows some poor style and has some real errors. How many can you find, and how would you fix them?

2. あなたはコードレビューをしています。あるプログラマが以下の様なクラスを書きました。いくつか問題のあるスタイルといくつか実際のエラーがあるようです。いくつ見つけられますか?また、どうやって直しますか?

class complex {
public:
    complex( double r, double i = 0 )
        : real(r), imag(i)
    { }

    void operator+ ( complex other ) {
        real = real + other.real;
        imag = imag + other.imag;
    }

    void operator<<( ostream os ) {
        os << "(" << real << "," << imag << ")";
    }

    complex operator++() {
        ++real;
        return *this;
    }

    complex operator++( int ) {
        auto temp = *this;
        ++real;
        return temp;
    }

    // ... 上記を補足するより多くの関数 ...

private:
    double real, imag;
};

Note: This is not intended to be a complete class. For example, if you provide operator++ you would normally also provide operator–. Rather, this is an instructive example to focus on the mechanics of writing correctly the kinds of functions this class is trying to support.

注意:これは完全なクラスであることを意図して書かれていません。たとえば、operator++を提供するなら、普通はoperator-も提供するはずです。それよりも、これはこのクラスがサポートしようとしている機能のようなものを、正しく書くための方式にフォーカスするための手本となるような例です。

Solution 解答

1. What makes interfaces “easy to use correctly, hard to use incorrectly”? Explain.

1. インターフェースを "正しく使うのは容易で、間違って使うのが難しい" ものにするにはどうすればよいですか?説明してください。

We want to enable a “pit of success” where users of our type just naturally fall into good practices—they just naturally write code that is valid, correct, and efficient.

私たちのようなタイプのユーザが書くコードが、有効で、正しくて、効率的なものに自然になるような、"成功のための落とし穴" http://blogs.msdn.com/b/brada/archive/2003/10/02/50420.aspx を可能にしたいと思います。

On the other hand, we want to make it hard for our users to get into trouble—we want code that would be incorrect or inefficient to be invalid (a compile time error if possible) or at least inconvenient and hard to write silently so that we can protect the user from unwelcome surprises.

一方で、私たちのユーザがトラブルに巻き込まれにくくもしたいです。正しくない、または効率的でないようなコードが有効でない(可能であればコンパイル時エラーになる)ように、もしくは、少なくとも黙って書くには不便だったり難しいようにしたいです。そうすることで、ユーザをうれしくないサプライズから守ることができます。


Scott Meyers popularized this guidance. See his concise writeup for further examples.

スコットメイヤーズはこのガイダンスをポピュラーにしました http://programmer.97things.oreilly.com/wiki/index.php/Make_Interfaces_Easy_to_Use_Correctly_and_Hard_to_Use_Incorrectly。彼の簡潔な記事を更なる例として参照してください。

2. You are doing a code review. A programmer has written the following class, which shows some poor style and has some real errors. How many can you find, and how would you fix them?

2. あなたはコードレビューをしています。あるプログラマが以下の様なクラスを書きました。いくつか問題のあるスタイルといくつか実際のエラーがあるようです。いくつ見つけられますか?また、どうやって直しますか?

This class has a lot of problems—even more than I will show explicitly here. The point of this puzzle was primarily to highlight class mechanics (issues like “what is the canonical form of operator<

このクラスはたくさんの問題を抱えています--私がここで明示的に示すよりも多くの問題があります。この問題のポイントは、第一義的にはクラスのメカニクス(operator<<の正しい書き方は?とかoperator+はメンバにするべき?とかいったような問題)を浮き彫りにすることであって、インターフェイスが単にまずくデザインされていることを指摘することではありませんが、まずは二つのおそらくもっとも有用な観察から始めることにしましょう。


First, this is a code review but the developer doesn’t seem to have tried to even unit-test his code, else he would have found some glaring problems.

1つめは、これはコードレビューですが、その開発者はどうやら彼のコードに対してユニットテストさえしていなかったようです。もししていたら、いくつかの明確な問題が見つかっていたはずです。


Second, why write a complex class when one already exists in the standard library? And, what’s more, when the standard one isn’t plagued with any of the following problems and has been crafted based on years of practice by the best people in our industry? Humble thyself and reuse.

2つめに、なぜすでに標準ライブラリに存在している complex クラスを新たに書いたのでしょう?しかも、標準のものは以下の問題には全く悩まされることがなく、業界の最高レベルの人々によって何年分ものプラクティスに基づいて作られているものなのに?謙虚たれ。再利用せよ。


Guideline: Reuse code—especially standard library code—instead of handcrafting your own. It’s faster, easier, and safer.

ガイドライン:コードを再利用せよ -- 特に標準ライブラリのコードを -- 自分で手作りするのではなく。標準ライブラリのコードはより速く、より簡単で、より安全である。


Perhaps the best way to fix the problems in the complex code is to avoid using the class at all, and use the std::complex template instead.

おそらくこの complex コードにおける問題を修正するのにもっとも良い方法は、このクラスを全く使わずに、代わりに std::complex テンプレートを使うことです。


Having said that, it’s an instructive example, so let’s go through the class as written and fix the problems as we go. First, the constructor:

とは言うものの、これは教育的な例なので、このクラスで書かれていることを読んで問題を修正していきましょう。まずはコンストラクタです:


1. The default constructor is missing.

1. デフォルトコンストラクタがない

    complex( double r, double i = 0 )
        : real(r), imag(i)
    { }

Once we supply a user-written constructor, we suppress the implicit generation of the default constructor. Beyond “easy to use correctly,” not having a default constructor makes the class annoying to use at all. In this case, we could either default both parameters, or provide a complex() = default; and declare the data members with initializers such as double real = 0, imag = 0; , or just delegate with complex() : complex(0) { } . Just defaulting the parameter is the simplest here.

ユーザ定義のコンストラクタを提供すると、デフォルトコンストラクタの暗黙的な生成が抑制されます。"正しく使うのが容易" である以前に、デフォルトコンストラクタを持たないことは、そのクラスをまったく使い物にならなくします。このケースでは、両方のパラメータにデフォルトを指定するか、complex() = default; と書いてデータメンバを初期値付きで double real = 0; imag = 0; のように宣言します。もしくは、単に complex(): complex(0) {} とデリゲートしてもよいでしょう。ここでは単にそのパラメータにデフォルトを指定するのが最もシンプルです。

Also, as explained in GotW #1, prefer to use { } consistently for initialization rather than ( ) just as a good modern habit. The two mean exactly the same thing in this case, but { } lets us be more consistent, and could catch a few errors during maintenance, such as typos that would invoke double-to-float narrowing conversions.

また、GotW #1 で説明したように、初期化には () ではなく一貫して {} を使うようにしましょう。これは近代的な良い習慣です。このケースではその2つはまったく同じ意味になりますが、{} のほうがより一貫していますし、保守の時に、double から float へのナローイング変換が起動されてしまうようなタイポなどいくつかの間違いを防ぐことができます。

2. operator+ passes by value.

2. operator+ は値渡し

    void operator+ ( complex other ) {
        real = real + other.real;
        imag = imag + other.imag;
    }

Although we’re about make other changes to this function in a moment, as written this parameter should be passed by const& because all we do is read from it.

この関数にはすぐに別の変更も必要になりますが、この引数からは読み出ししか行わないので const& で渡すべきです。

Guideline: Prefer passing a read-only parameter by const& if you are only going to read from it (not make a copy of it).

ガイドライン:読み取り専用の引数は const& で渡すこと。(コピーが作られない)

3. operator+ modifies this object’s value.

3. operator+ は this オブジェクトの値を変更する

Instead of returning void, operator+ should return a complex containing the sum and not modify this object’s value. Users who write val1 + val2 and see val1 changed are unlikely to be impressed by these gratuitously weird semantics. As Scott Meyers is wont to say, when writing a value type, “do as the ints do” and follow the conventions of the built-in types.

void を返す代わりに、operator+ は合計値を持った complex を返すべきで、this オブジェクトの値を変更するべきではありません。ユーザが val1 + val2 と書いた時に val1 が変更されてしまったとしたら、その不当で奇妙なセマンティクスに感激するということはなさそうです。スコット・メイヤーズがよく言うように、値型を書くときは、"int がするようにする"、そして組込み型の変換に従うことです。

4. operator+ is not written in terms of operator+= (which is missing).

4. operator+ は operator+= を使って書かれていない(そして operator+= は存在するべきなのに欠けている)

Really, this operator+ is trying to be operator+=. It should be split into an actual operator+ and operator+=, with the former calling the latter.

実際のところ、この operator+ は operator+= になろうとしています。これは operator+ と operator+= に分けられるべきで、前者が後者を呼び出すようにすべきです。

Guideline: If you supply a standalone version of an operator (e.g., operator+), always supply an assignment version of the same operator (e.g., operator+=) and prefer implementing the former in terms of the latter. Also, always preserve the natural relationship between op and op= (where op stands for any operator).

ガイドライン:ある演算子の単独のバージョン(たとえば operator+)を提供するなら、常にその演算子の代入バージョン(たとえば operator+=)も提供し、前者を実装するのに後者を使用するようにする。また、op と op= の間の自然な関係を常に維持する(op は任意の演算子)。


Having += is good, because users should prefer using it. Even in the above code, real = real + other.real; should be real += other.real; and similarly for the second line.

+= を持つことはいいことです。ユーザがそれを優先的に使用することができるからです。上記のコードでも、real = real + other.real; は real += other.real; となるべきです。2行目も同様です。


Guideline: Prefer writing a op= b instead of a = a op b (where op stands for any operator). It’s clearer, and it’s often more efficient.

ガイドライン:a = a op b ではなく、a op= b を優先すること(ここで op は任意の演算子を表す)。より明快で、しばしばより効率的である。


The reason why operator+= is more efficient is that it operates on the left-hand object directly and returns only a reference, not a temporary object. On the other hand, operator+ must return a temporary object. To see why, consider the following canonical forms for how operator+= and operator+ should normally be implemented for some type T.

operato+= がより効率的である理由は、それが左辺のオブジェクトに直接作用し、一時オブジェクトではなく参照を返すからです。かたや operator+ は一時オブジェクトを返さなければなりません。理由を見るために、以下の正規化された形式の operator+= と operator+ がある型 T に対して通常どのように実装されるべきかを考えてみましょう。

T& T::operator+=( const T& other ) {
    //...
    return *this;
}

T operator+( T a, const T& b ) {
    a += b;
    return a;
}


Did you notice that one parameter is passed by value, and one by reference? That’s because if you’re going to copy from a parameter anyway, it’s often better to pass it by value, which will naturally enable a move operation if the caller passes a temporary object such as in expressions like (val1 * val2) + val3. This is a good habit to follow even in cases like complex where a move is the same cost as a copy, since it doesn’t cost any efficiency when move and copy are the same, and arguably makes for cleaner code than passing by reference and adding an extra named local object. We’ll see more on parameter passing in a future GotW.

ひとつの引数が値で渡され、もう一つが参照で渡されていることに気づきましたか?どのみち引数からコピーするのであれば最初から値で渡してしまったほうがよいですし、値で渡されると呼び出し元が一時オブジェクトを渡した時にムーブ操作が可能となります。たとえば式 (val1 * val2) + val3 のときのように。これは complex のようにムーブがコピーと同じコストであるようなケースでも従うべき良い習慣です。なぜならムーブとコピーが同じだったとしても効率性を犠牲にすることはありませんし、参照で渡して余計なローカルな名前付きオブジェクトを追加するよりもほぼ間違いなくクリーンなコードになるからです。将来の GotW でパラメータ渡しについてより詳しく見ていきます。


Guideline: Prefer passing a read-only parameter by value if you’re going to make a copy of the parameter anyway, because it enables move from rvalue arguments.

ガイドライン:どのみちパラメータのコピーを作るのであれば、読み取り専用パラメータを値で渡すようにする。rvalue 引数からムーブが可能となるので。


Implementing + in terms of += both makes the code simpler and guarantees consistent semantics as the two functions are less likely to diverge during maintenance.

+ を += を使って実装することは、コードをよりシンプルにしますし、保守の間に2つの関数が枝分かれしにくく一貫したセマンティクスを保証します。

5. operator+ should not be a member function.

5. operator+ はメンバ関数であるべきではない

If operator+ is made a member function, as it is here, then it won’t work as naturally as your users may expect when you do decide to allow implicit conversions from other types. Here, an implicit conversion from double to complex makes sense, but with the original class users have an asymmetry: Specifically, when adding complex objects to numeric values, you can write a = b + 1.0 but not a = 1.0 + b because a member operator+ requires a complex (and not a double) as its left-hand argument.

ここに書かれたように operator+ がメンバ関数であるならば、他の型からの暗黙の変換を可能にしようと決めた時にユーザが期待するとおりに自然には動かなくなります。ここでは、double から complex への暗黙の変換は意味がありますが、オリジナルのクラスのユーザは非対称性を持つことになります:特に、complex オブジェクトを数値に足すとき、 a = b + 1.0 とは書けるのに、a = 1.0 + b とは書けません。メンバ operator+ は左側の引数に complex (double ではない)を必要とするからです。


Finally, the other reason to prefer non-members is because they provide better encapsulation, as pointed out by Scott Meyers.

最後に、非メンバを使うべきもう一つの理由は、スコット・メイヤーズが指摘(http://www.drdobbs.com/cpp/how-non-member-functions-improve-encapsu/184401197)するように、それがよりよいカプセル化を提供するからです。


Guideline: Prefer these guidelines for making an operator a member vs. nonmember function: unary operators are members; = () [] and -> must be members; the assignment operators (+= –= /= *= etc.) must be members; all other binary operators are nonmembers.

ガイドライン:演算子をメンバ関数にするか非メンバ関数にするかという問題に対し、以下のガイドラインを用いる:単項演算子はメンバにする。= () [] -> はメンバでなければならない。代入演算子(+= -= /= *= など)はメンバでなければならない。他のすべての二項演算子は非メンバ。

6. operator<< should not be a member function.

6. operator<< はメンバ関数であるべきではない

The author of this code didn’t really mean to enable the syntax my_complex << cout, did they?

このコードを書いた人は、シンタックス my_complex << cout を可能にしたかったわけではないですよね?

    void operator<<( ostream os ) {
        os << "(" << real << "," << imag << ")";
    }


The same reasons already given to show why operator+ should be a nonmember apply also to operator<<, only more so because a member the first parameter has to be a stream, not a complex. Further, the parameters should be references: (ostream&, const complex &).

operator+ が非メンバであるべきであることを示すのにすでに与えられたのと同じ理由が operator<< にも適用されます。それだけではなく、メンバなのであれば最初のパラメータは complex ではなく stream であるべきです。さらに、パラメータは参照であるべきです:(ostream&, const complex&)


Note also that the nonmember operator<< should normally be implemented in terms of a(n often virtual) const member function that does the work, usually named something like print.

非メンバ operator<< は通常、実際の仕事をするconst メンバ関数(virtual にすることが多く、たいてい print のような名前)を使って実装されるべきです。

7. operator<< should return ostream&.

7. operator<< は ostream& を返すべき

Further, operator<< should have a return type of ostream& and should return a reference to the stream in order to permit chaining. That way, users can use your operator<< naturally in code like cout << a << b;.

さらに、operator<< は返り値の型として ostream& を持つべきで、チェイン化を可能とするために引数として渡された stream への参照を返すべきです。こうすることで、ユーザは cout << a << b; のようなコードの中であなたの operator<< を自然に使うことができます。


Guideline: Always return stream references from operator<< and operator>>.

ガイドライン:operator<< と operator>> からは常に stream の参照を返す。

8. The preincrement operator’s return type is incorrect.

8. 前置インクリメント演算子の返り値の型が正しくない

    complex operator++() {
        ++real;
        return *this;
    }


Ignoring for the sake of argument whether preincrement is meaningful for complex numbers, if the function exists it should return a reference. This lets client code operate more intuitively and avoids needless inefficiency.

前置インクリメントが complex (複素数)に対して意味のあるものかどうかという議論はさておき、前置インクリメントの関数が存在するのであればそれは参照を返すべきです。これはクライアントコードをより直感的に作用させ、無意味な非効率性を回避させます。


Guideline: When you return *this, the return type should usually be a reference.

ガイドライン:return *this と書くときは返り値の型は通常は参照です。

9. Postincrement should be implemented in terms of preincrement.

9. 後置インクリメントは前置インクリメントを使って実装されるべき

    complex operator++( int ) {
        auto temp = *this;
        ++real;
        return temp;
    }


Instead of repeating the work, prefer to call ++*this. See GotW #2 for the full canonical form for postincrement.

作業を繰り返すのではなくて、++*this を呼び出すようにしましょう。後置インクリメントの完全な正規形は GotW #2 を見てください。


Guideline: For consistency, always implement postincrement in terms of preincrement, otherwise your users will get surprising (and often unpleasant) results.

ガイドライン:一貫性のために、後置インクリメントは常に前置インクリメントを使って実装します。さもなければユーザは驚くような(そしてそれはたいてい嬉しくない)結果を得ることになります。

Summary まとめ

That’s it. There are other modern C++ features we could apply here, but they would be arguably gratuitous and not appropriate for general recommendations. For example, this is a value type not designed to be inherited from, so we could prevent inheritance by making the class final, but that would be protecting against Machiavelli, not Murphy, and there’s no need for a general guideline that tells everyone they should now write final on every value type; that would just be tedious and unnecessary.

以上です。他にも適用可能なモダンな C++ の機能がありますが、それはほぼ間違いなく余計なもので、一般的な推奨としては適切ではないでしょう。たとえば、これが値型であって継承されるようにデザインされていないので、クラスを final にすることで継承を防ぐことができます。しかしそれはマキアヴェッリから守るかもしれませんがマーフィーからは守りません(訳注:マキアヴェッリは目的のために手段を選ばない様から、重箱の隅をつつくような、ごく希な間違い・失敗のことを表していると考えられ、マーフィーはマーフィーの法則から、人々が起こしがちなよくある間違いのことを表していると考えられます)。そして全員に今すぐすべての値型に対して final と書けと命じるような一般的なガイドラインの必要性もありません;単につまらない不必要なものになりかねません。


Here’s a corrected version of the class, ignoring design and style issues not explicitly noted above:

最後に修正されたバージョンのクラスを示します。ここでは上記で明示的に示された設計やスタイル上の問題は無視しています:

class complex {
public:
    complex( double r = 0, double i = 0 )
        : real{r}, imag{i}
    { }

    complex& operator+=( const complex& other ) {
        real += other.real;
        imag += other.imag;
        return *this;
    }

    complex& operator++() {
        ++real;
        return *this;
    }

    complex operator++( int ) {
        auto temp = *this;
        ++*this;
        return temp;
    }

    ostream& print( ostream& os ) const {
        return os << "(" << real << "," << imag << ")";
    }

private:
    double real, imag;
};

complex operator+( complex lhs, const complex& rhs ) {
    lhs += rhs;
    return lhs;
}

ostream& operator<<( ostream& os, const complex& c ) {
    return c.print(os);
}

Acknowledgments 謝辞

Thanks in particular to the following for their feedback to improve this article: Mikhail Belyaev, jlehrer, Olaf van der Spek, Marshall, litb1, hm, Dave Harris, nosenseetal.

この記事をより良くするための以下の各位からのフィードバックに対し、特に感謝する: Mikhail Belyaev, jlehrer, Olaf van der Spek, Marshall, litb1, hm, Dave Harris, nosenseetal.