GotW #3: 標準ライブラリを使う(もしくは一時オブジェクト再訪)(勝手訳)
第 3 回めの GotW の翻訳です。
例によって原文著者(Herb Sutter 氏)の許可は得ていませんし、私の英訳がヒドいクオリティである(用語の統一がとれていないとか、誤訳が含まれているとか)かもしれませんのでそこのところはご理解いただければと思います。
Effective reuse is an important part of good software engineering. To demonstrate how much better off you can be by using standard library algorithms instead of handcrafting your own, let’s reconsider the previous question to demonstrate how many of the problems could have been avoided by simply reusing what’s already available in the standard library.
効率的な再利用は、よいソフトウェア工学の重要な一部です。自作のものを使う代わりに標準ライブラリのアルゴリズムを使うことによってどれだけのメリットがもたらされるかを示すために、前回の質問をもう一度考えてみましょう。標準ライブラリにすでに存在しているものを単に再利用するだけで、そこで起こった問題がどれだけ回避できるかがわかるはずです。
Problem 問題
Guru Question グルへの質問
2. How many of the pitfalls in GotW #2 could have been avoided in the first place, if only the programmer had replaced the explicit iterator-based for loop with:
2. GotW #2 の落とし穴のうち、明示的なイテレータベースの for ループを以下の方法で書き換えただけで、いくつの問題が回避されるか?
(a) a range-based for loop?
(b) a standard library algorithm call?
(a) 範囲ベースの for ループ
(b) 標準ライブラリアルゴリズムの呼び出し
Demonstrate. (Note: As with GotW #2, don’t change the semantics of the function, even though they could be improved.)
解答してください。(注意:GotW #2 と同様、関数のセマンティクスは変更しないでください。たとえ改善につながるとしても)
To recap, here is the mostly-fixed function:
以下は、前回の解答においてほぼ修正済みの関数です:
string find_addr( const list<employee>& emps, const string& name ) { for( auto i = begin(emps); i != end(emps); ++i ) { if( i->name() == name ) { return i->addr; } } return ""; }
Solution 解答
1. What is the most widely used C++ library?
1. 最も広く使われている C++ のライブラリは何か?
The C++ standard library, with its implementations on every platform.
C++ 標準ライブラリ。すべてのプラットフォームにおいてその実装についてくるもの。
2. (a) How many of the pitfalls in GotW #2 could have been avoided with a range-based for loop?
2. GotW #2 の落とし穴のうち、明示的なイテレータベースの for ループを以下の方法で書き換えただけで、いくつの問題が回避されるか?
Astute readers of GotW #2 will have been champing at the bit to say: “Why aren’t you using a range-based for loop?” Indeed, why not? That would solve several of the temporaries, never mind be easier to write.
鋭い読者なら、GotW #2 の時から "範囲ベースの for loop を使ったらどうだい?" と言いたくてうずうずしていたことでしょう。確かにそうですね。そうすることでいくつかの一時オブジェクトの問題は解決しますし、むしろ書くのも簡単になるくらいです。
Compare the original unimproved explicit iterator loop:
比較してみましょう。
オリジナルの改善前の明示的イテレータループ:
for( auto i = begin(emps); i != end(emps); i++ ) { if( *i == name ) { return i->addr; } }
with the range-based for loop (bonus points if you remembered to write the const auto&):
範囲ベースの for ループ(const auto& と書くことを覚えていたならボーナスポイントを差し上げます):
for( const auto& e : emps ) { if( e == name ) { return e.addr; } }
The expressions e == name and return e.addr; are unchanged in terms of their possible or actual temporaries. But the questions in the naked loop code about whether or not the = causes a temporary (recall: it doesn’t), whether or not end() recalculation matters and should be hoisted (recall: probably not, but maybe), and whether or not i++ should be rewritten ++i (recall: it should) all simply don’t arise in the range-for code. Such is the power of clear code, and using a higher level of abstraction.
式 e == name と return e.addr; は一時オブジェクトができる可能性があることや実際にできてしまうという意味では変わっていません。しかし質問のループバージョンにあった、= が一時オブジェクトを生成するかどうか(実際にはしません)、end() の再計算が問題になるか、またループの外にくくり出されるべきか(おそらく問題にはなりませんが、なるかもしれません)、i++ は ++i と書きなおされるべきか(書きなおされるべきです)といったすべてのことが、範囲 for では単に問題になりません。これこそ、高度な抽象化を用いたクリアなコードのパワーです。
A key advantage is that using the range-based for loop has increased our level of abstraction, the information density in our code. Consider: What can you say about the following two pieces of code without reading what comes next?
範囲ベースの for ループを使うことの主なアドバンテージは、抽象化のレベルを上げ、コードの情報の精度を上げることです。次の2つのコード片について、次に何がくるかを読まずに何を言うことができるか考えてみてください。
for( auto i = begin(emps); i != end(emps); i++ ) { // A for( const auto& e : emps ) { // B
At first it might seem that lines A and B convey the same information, but they don’t. When you see A, all you know is that there’s a loop of some sort that uses an iterator over emps. Granted, we’re so used to A that our eye’s peripheral vision tends to “autocomplete” it in our heads into “a loop that visits the elements of emps in order” and our autocomplete is often correct—except when it isn’t: was that a ++, or a s+= 2 in a strided loop? is the index modified inside the body? Our peripheral vision might be wrong.
まず、行 A も B も同じ情報を含んでいるようだと思われますが、実際には違います。A を見てわかるのは、emps に対してイテレータを使って何らかのループをしているということです。確かに、私たちは A の形式に慣れていますし、私たちの目の周辺視野は頭のなかで "これは emps の要素を順に処理するループだ” と "オートコンプリート" しようとします。そしてそのオートコンプリートはたいてい正しいものです -- 間違っているとき以外は。もし ++ が +=2 だったら?そのインデックスはループ本体の中で変更されていない?私たちの周辺視野は間違えるかもしれません。
On the other hand, B conveys more information to the reader. When you see B, you know for certain without inspecting the body of the loop that it is a loop that visits the element of emps in order. What’s more, you’ve simplified the loop control because there’s no need for an iterator indirection. Both of these are raising the level of abstraction of our code, and that’s a good thing.
一方、B は読者に対してより多くの情報を与えます。B を見て、ループの本体を見なくても確実にわかるのは、これは emps の要素を順に処理するループであるということです。さらに、ループ制御がシンプルになります。イテレータによる間接参照が不要になるからです。これらのことはコードの抽象化のレベルを上げます。これは良いことです。
Note that, as discussed in GotW #2, the naked for loop didn’t naturally allow consolidating to a single return statement without resorting to making the code more complex by adding an additional variable and performing extra computation (a default construction followed by an assignment, instead of just a construction). That’s still true of the range-based for loop form, because it still has the two return statements in different scopes.
GotW #2 で議論したように、生の for ループを書くと必然的に return 文を一つだけにまとめることができません。一つにするには、コードを複雑にするような、変数の追加や余計な計算が必要になります(デフォルトコンストラクタによる構築と、代入が必要になります。ただ構築すれば良いだけではなくなります)。範囲ベースの for ループ形式でもそれは同じで、return 文をそれぞれ異なるスコープで2つ書かないといけないことには変わりありません。
2. (b) … with a standard library algorithm call?
2. (b) … 標準ライブラリアルゴリズムの呼び出しでは?
With no other changes, simply using the standard find algorithm could do everything the range-based for loop did to avoid needless temporaries (and questions about them):
他の変更なしで、単に標準の find アルゴリズムを使うことで、範囲ベースの for でできることは全てできて、かつ不要な一時オブジェクト(とそれにまつわる質問)を回避することができます。
// ベター (内部にフォーカス) // string find_addr( /*...*/ ) { const auto i = find( begin(emps), end(emps), name ); // はい、直してあげましたよ return i != end(emps) ? i->addr : ""; }
This naturally eliminates the same temporaries as the range-for version, and it further increases our level of abstraction. As with the range-based for loop, we can see at a glance and for certain that the loop will visit the elements of emps in order, but on top of that we also know we’re trying to find something and will get back an iterator to the first matching element if one exists. We do still have an iterator indirection, but only a single-use iterator object and no iterator arithmetic as in the original naked iterator for loop.
これは範囲ベースの for のバージョンと同じ一時オブジェクトを取り除きます。そして抽象化のレベルを更に引き上げます。範囲ベースの for ループのように、emps の要素に順に処理をするループであることがぱっと見で確定できます。その上、何かを見つけ(find)ようとしていて、もし見つかったら最初にマッチした要素へのイテレータが返されるということがわかります。イテレータによる間接参照はまだ残っていますが、イテレータオブジェクトは一度きりしか現れず、生のイテレータ for ループにあったようなイテレータの算術演算はありません。
Further, we have eliminated a loop nested scope entirely and flattened out the function to a single scope which can simplify this calling function in ways even the range-for couldn’t. To demonstrate still more just how fundamental this point is, note that what else the flattening out of the body buys us: Now, because the return statements are in the same scope (possible only because we eliminated the loop scope), we have the option of naturally combining them. You could still write if( i != end(emps) ) return i->addr; else return “”; here, on one or two or four lines, but there’s no need to. To be clear, the point here is not that reducing return statements should be a goal in itself—it shouldn’t be, and “single exit” thinking has always been flawed as we already saw in GotW #2. Rather, the point is that using an algorithm often simplifies our code more than an explicit loop, even a range-for loop, can do—not only directly by removing extra indirections and extra variables and a loop nested scope, but often also by permitting additional simplifications in nearby code.
さらに、ループのネストしたスコープをなくして関数を一つのスコープにフラットにして関数を単純に呼ぶだけにしました。これは範囲ベースの for でもできなかったことです。単にこの点が大変基本的なことというだけでないことを示すために、関数本体をフラットにすることが私たちにもたらしてくれるもう一つのことを強調しておきます:今、return 文が同じスコープにあるので(これはループのスコープを取り除いたことによってのみ可能なことです)、私たちはそれらを組み合わせることができます。if( i != end(emps) ) return i->addr; else return ""; と、1行もしくは2行、もしくは4行で書くこともできますが、そうする必要はありません。明確にしておきたいのですが、ここでのポイントは return 文の数を減らすことが自己目的化したゴールではないということです。またそうなるべきではありません。そして "一つの出口" という考えは、GotW #2 で見たように、常に間違っていました。ここでのポイントは、アルゴリズムを使うことは明示的なループを書くよりも私達のコードをよりシンプルにするものだ、ということです。余計な間接参照や余計な変数を取り除くことは範囲 for ループを使ってもできることではありますが、そのように直接的にシンプルにするだけではなく、周辺のコードのさらなるシンプル化を可能とします。
The above code might still cause a temporary when comparing an employee with a string, and we can eliminate even that temporary if we go one small step further and use find_if with a custom comparison that compares e.name() == name to avoid a possible conversion, assuming something like a suitable employee::name() is available as we did in GotW #2. Combining this with the other fixes to pass parameters by reference, we get:
上記のコードは employee を string と比較するときにまだ一時オブジェクトを生成します。その一時オブジェクトさえも取り除く事が可能です。もう一歩推し進めて、find_if をカスタム比較関数と合わせて使って e.name() == name で比較を行えば、発生しうる変換を回避できます。もちろんそれには GotW #2 でのように適切な employee::name() のようなものが使えるという前提です。これと引数を参照で渡すという修正を組み合わせると、こうなります:
// さらにベター (完成) // string find_addr( const list<employee>& emps, const string& name ) { const auto i = find_if( begin(emps), end(emps), [&](const auto& e) { return e.name() == name; } ); return i != end(emps) ? i->addr : ""; }
Summary まとめ
Prefer algorithm calls over explicit loops, when you have or can write a suitable algorithm that does what you want. They raise the level of abstraction and the clarity of our code. Scott Meyers’ advice in Effective STL is still true, and more applicable than even now that lambdas make algorithms much more usable than before:
あなたのしたい事をするようなアルゴリズムがすでにあるかあなたが書くことができるならば、明示的なループよりもアルゴリズム呼び出しを優先しましょう。それにより抽象化のレベルとコードの明快さが引き上げられます。Effective STL でのスコット・メイヤーズのアドバイスは今でも有効であり、以前よりもラムダがアルゴリズムをより使いでのあるものにした今となっては、そのアドバイスは一層当てはまります:
Guideline: Prefer algorithm calls to explicit loops. Algorithm calls are often clearer and reduce complexity. If no suitable algorithm exists, why not write it? You’ll use it again.
ガイドライン:明示的なループよりもアルゴリズム呼び出しを優先せよ。アルゴリズム呼び出しはより明快で、複雑さを低減させるものだ。適切なアルゴリズムが存在しなければ、それを書いてみよう。それを再利用することになるだろう。
Prefer reusing existing library code to handcrafting your own. The more widely used the library, the more likely it is to come well-designed, pre-debugged, and pre-optimized for many common requirements. And what library is more widely used than the standard library? In your C++ program, your standard library implementation is the most widely used library code you’re likely to use. This helps you both in the library’s design and its implementation: It’s full of code that’s intended to be used and reused, and to that end a lot of thought and care has gone into the design of its features, including its standard algorithms like find and sort. Implementers have also spent hours sweating over efficiency details, and usability details, and all sorts of other considerations so that you don’t have to—including performing optimizations you should almost never resort to in application-level code, such as using nonportable OS- and CPU-target specific optimizations.
自分で手作りするよりも既存のライブラリコードを再利用することを優先しましょう。ライブラリがより広く使われるようになると、そのライブラリは多くの一般的な要件に対してより良くデザインされ、事前にデバッグや最適化が済んでいるものになるでしょう。そして標準ライブラリよりも広く使われているライブラリはあるでしょうか?あなたの C++ プログラムの中で、あなたの標準ライブラリ実装が最も広く使われているライブラリコードで、あなたが使うべきものです。これはあなたをライブラリのデザインとその実装の両面で手助けします。標準ライブラリはすべて、利用され、再利用されることを意図して作られたコードです。find や sort のような標準的なアルゴリズムも含め、その機能のデザインにとてもたくさんの考えや配慮の集大成になっています。実装者は効率性の詳細や使いやすさの詳細、その他ありとあらゆる考慮しなければならないことに対して多大な時間をつぎ込み汗を流して来ました。あなたがそうしなくても良いように。あなたが決して使うことのなさそうな、移植性のない OS や CPU を対象にした固有の最適化といったような、最適化の実施もその中に含まれています。
So, always prefer to reuse code, especially algorithms and especially the standard library, and escape the trap of “I’ll-write-my-own-just-’cause-I-can.”
ですので、つねにコードの再利用を優先しましょう。アルゴリズムと特に標準ライブラリを。そして "自分で書けるから自分で書く" という罠を避けるようにしましょう。
Guideline: Reuse code—especially standard library code—instead of handcrafting your own. It’s faster, easier, and safer.
ガイドライン:自作するのではなく、コードを再利用せよ。特に標準ライブラリのコードを。それはより速いし、より簡単だし、より安全だ。
Acknowledgments 謝辞
Thanks in particular to the following for their feedback to improve this article: Olaf ven der Spek, Sam Kramer.
この記事をより良くするための以下の各位からのフィードバックに対し、特に感謝する:Olaf ven der Spek, Sam Kramer.