rvalue reference、move semanticsに挫折した人のための再入門解説
この記事は C++11 Advent Calendar 14日目の記事です。
何がわかりにくいのか
C++11から正式に導入される新機能である、move semanticsとrvalue referenceですが、なかなかこれを理解出来ないという人が多いようです。
ちょっと考えればなるほど、と思いますが私が思うにこのわかりにくい最大要因はやはりその名前のせいかと思います。
特にrvalue referenceですが、日本語に訳すと右辺値参照という呼ばれ方をしています。
そもそも右辺値や左辺値という名称はC言語時代の流れでそのまま引き継いでいるものであり、右辺値参照という名称は言わば後付けで名称されたものです。
実際に使用する際に右辺値なのか左辺値なのかを意識している人は殆どおらず、これが余計な混乱を招いているのではないかと思います。
ではコードを書いてみて理解しましょう。
lvalueとrvalue
int main() { int a = 0; a; // lvalue 0; // rvalue }
とても単純なコードです。
これが理解出来ない人がいるとなるとさすがに私でも教えられる気がしないので1度C言語入門からやり直すことをオススメします。
そしてこれをみてなんとなく理解出来た人は既にrvalueを殆ど理解していると言えます。
そう、lvalueとrvalueの最大の違いというのは"名前を持つオブジェクト"と"名前を持たない無名オブジェクト"ということです。
named valueとunnamed valueとでも呼んだ方がわかりやすいでしょう。
次回からは"lvalue -> named value"、"rvalue -> unnamed value"と置き換えて読んでみてください。
それでは別の例もみてみましょう。
struct A {}; int func() { return 0; } int main() { A a; a; // lvalue A(); // rvalue func(); // rvalue }
もう説明するまでもないでしょう。
つまり、rvalueとは生まれた次の瞬間には死ななくてはならないオブジェクトのことなのです。
逆に言えば生まれたあとも自分で寿命管理出来るオブジェクトはlvalueということです。
ではrvalue referenceというのはその名の通りrvalueへの参照というだけです。
rvalue reference
struct A {}; int main() { A a; // lvalue reference A& lr1 = a; // 問題ない A& lr2 = A(); // lvalue referneceへrvalueを渡そうとしているのでerror // rvalue reference A&& rr1 = a; // rvalue referenceへlvalueを渡そうとしているのでerror A&& rr2 = A(); // 問題ない }
"A&& rr"という部分が実際にrvalue referenceとして受け取っている部分です。
要はこれだけという話なのですが、ひとつ注意しなくてはならないのが、rvalue referenceとして受け取ったオブジェクトはrvalueではなくて、lvalueということです。
なぜなら最初にも言った通り"名前を持つオブジェクト"になるからです。
rvalue reference型の変数はrvalueではなくて、rvalueへの参照が受け取れるだけで通常の変数と変わらないということです。
move semantics
そして出てきましたmove semanticsさん。
言葉で説明するのはなかなか難しいのですが、もの凄く単純な話「値のセマンティクスを移動させる」というだけのことです。
そのまんまじゃねーかバカやろうというツッコミがあると思いますが、とりあえずセマンティクスの意味に誤解がないように説明リンクを。
セマンティクスとは
http://www.kab-studio.biz/Programing/JavaA2Z/Word/00000315.html
セマンティクスの意味という意味がわからない説明ですが、とりあえず理解は出来るでしょう。
つまりセマンティクスを移動させるということはプログラムとして意味のある値を移動させるということになります。
移動させるということは一般的に代入操作で行なわれるcopy semanticsとは別物になり、移動元となった値は完全に無効値となります。
内部的にポインタだけが移動したと考えるとわかりやすいかもしれません。
実際にコードで書くとこうなります。
template<typename T> void swap(T& a, T& b) { //T tmp = a; // これだとコピーが発生する T tmp = std::move(a); // Tのmove constructorが呼ばれる a = std::move(b); // Tのmove assignment(T::operator(T&& b))が呼ばれる b = std::move(tmp); // tmpの中身をbに移動させる。tmpの中身はなくなる }
move semantics仕様のswap()関数です。
std::move()関数はその中で引数のlvalueをrvalueにキャストします。
そしてキャストしたrvalueをそのままmove constructor、もしくはmove assignment operatorへと渡します。
move constructorや、move assignment operatorはcopy constructorやcopy assignment operatorなどと同じようにクラスに定義出来ます。
定義しなければコンパイラが自動で生成します。
参考
http://d.hatena.ne.jp/faith_and_brave/20100331/1270020213
ちなみにstd::move()関数の他にstd::forward()という関数もありますが、これは難しい話になるのでここでは解説しません。
std::forward()に関してはきっと id:gintenlabo さんが書いてくれるはず!!
結局何が嬉しいのか
以上でmove semanticsを使用したコードが書けたわけですが、結局何が嬉しいんでしょうか?
答えはひとつ。
copy semanticsだと発生するコピーのコストがmove semanticsでは一切ないということです。
小さいオブジェクトでなら無視出来るコピーコストでも大きいオブジェクトになると問題になりがちなコピーコストが一切ありません。
BigDataなクラスでも作ってSTLコンテナに突っ込もうものなら大変なコピーコストになってしまうということです。
いちいちポインタでnewしてそれを突っ込んで消す際にdeleteなんて無駄なことは必要ありません。
コピーが発生しないので気軽にコンテナに入れ放題ということです。
では最後にSTLを使ったcopy semanticsとmove semanticsのベンチマークを比較しましょう。
#include <vector> #include <iostream> #include <time.h> #include <set> #include <algorithm> static const int N = 3001; std::set<int> get_set(int) { std::set<int> s; for (int i = 0; i < N; ++i) { while (!s.insert(std::rand()).second) ; } return s; } std::vector<std::set<int> > generate() { std::vector<std::set<int> > v; for (int i = 0; i < N; ++i) { v.push_back(get_set(i)); } return v; } void output(const char* s, clock_t t1, clock_t t2) { std::cout << s << static_cast<float>(((t2 - t1) / static_cast<double>(CLOCKS_PER_SEC))) << std::endl; } float time_it() { clock_t t1, t2, t3; clock_t t0 = clock(); { std::vector<std::set<int> > v = generate(); t1 = clock(); output("construction ", t0, t1); std::sort(v.begin(), v.end()); t2 = clock(); output("sort ", t1, t2); } t3 = clock(); output("destruction ", t2, t3); return static_cast<float>(((t3 - t0) / static_cast<double>(CLOCKS_PER_SEC))); } int main() { float t = time_it(); std::cout << "Total time = " << t << '\n'; }
以下の結果はgcc 4.5.2で最適化なしでCore i7 920 2.67GHzで比較したものです。
move semanticsは-std=gnu++0xオプションをつけてビルドしています。
// copy semantics construction 9.182 sort 8.302 destruction 0.984 Total time = 18.468 // move semantics construction 5.228 sort 0.041 destruction 1.015 Total time = 6.284
トータル時間で三倍程度差がでました。
特にソートに関しては殆んど時間がかかっていません、まさしく爆速。
デストラクト時に若干時間を食っているのが気になりますが、それでも誤差程度。
これが単純な結果とは言いづらいですが、それでもこれほどの差がでます。
move semantics万歳。
総括
move semanticsは誰もが幸せになれる素晴しい機能。
STLの場合はC++11対応コンパイラさえ使用していれば勝手にmove semanticsを使用します。
意識していなくても速くなるというのは素晴しい。
もちろん理解して使えばこの上ない強力な武器です、ぜひ活用しましょう。
それでは明日のid:iorateさんにバトンタッチです。
追記
lvalueではない名前つきオブジェクトがあるとツッコミを受けたので以下にまとめておきます。
やはりrvalueを完全に意識して書くのはなかなか大変かもしれません。
int& f(int& x) { return x; } // f()の戻り値はlvalue *new int(0); // 名前はないけどlvalue enum { hoge }; // hogeは名前があるけどrvalue template<char const * ch> // chはrvalueだけど、*chはlvalue template <int I> // Iはrvalue
追記2
今回のセマンティクスに対する解説は人によって誤解を生むものかもしれません。
そして私も誤解しているかもしれないので、あまり真に受けすぎない程度に参考にしてください。
Modern Vim Life!
Vim Advent Calendar 2011の7日目です。
最近流行りのVimとは
Vimとは一般的には単なるテキストエディタのはずですが、ここ数年のVimの進化は単なるテキストエディタの枠を越えてVisual StudioのようなIDEのようなものからあらゆる情報を管理出来る統合ツールとなりつつあります。
これはVimと同様にEmacsでも同じ事が言えますが、ここ数年のVimは一部でEmacs以上になっていたりとその進化は凄まじいものがあります。
今日は少しでもVimに興味を持ってもらえるように最近のVimで出来る事を紹介したいと思います。
こんな事が出来るよ!
- あふ風の高機能なファイラ
- あらゆるものをソースにインクリメンタルに候補を絞り好きなように実行する
- Vimの中で完結出来るシェル実行環境
- あらゆるテキストを補完する事が出来る高度な補完環境
- Linuxのパッケージ管理システムのようにプラグインを管理する
- VimでTwitterを楽しむ
ではそれぞれを紹介していきましょう。
あふ風の高機能なファイラ
VimにはデフォルトでNetrwというファイルエクスプローラが付属されており、":Explore"と入力する事により使用する事が出来ます。
が、最近のVimでは vimfiler というVimプラグインが流行っており、専らファイルブラウザとして使用する分にはこちらの方が圧倒的に便利です。
見た目は2画面ファイラとしても有名な "あふ" というファイラに似ており、そちらを触った事があるUserなら自然と使用する事が出来ます。
特に後述するunite.vimとの連携機能や拡張リネーム機能は他のファイラには存在しないものであり、vimfilerを使う時だけの特権と言えるでしょう。
あらゆるものをソースにインクリメンタルに候補を絞り好きなように実行する
unite.vim は何らかの情報ソースを入力し、それを元にインクリメンタルに候補を絞って実行させる事の出来るプラグインです。
上記の画像では現在存在しているbufferを対象に"vim"というワードに対してインクリメンタルに候補を絞っています。
絞った候補に対しては自由なactionを指定する事が出来、それらを組み合せて素早く情報にアクセスし、仕事をするという事が可能になります。
unite.vimはVimのバッファ管理はもちろん、コマンドランチャやGrep対象の絞り込み、履歴情報の参照、辞書情報の検索やWeb上にある情報にまでアクセスし、候補の絞り込みを行う事が出来ます。
これらは全てunite.vimの"source"として入力する事が出来るものであればどんなものでも対象にする事が出来ます。
この力は絶大であらゆる作業効率を極限まで効率化する事が出来ます。
unite.vimはVimプラグインの一種ですが、それ自身のプラグインも沢山あり、プラグインプラグインとも言うべきでしょうか。
非常にプラグイン自体も作りやすくVim scriptに手を始めて出すような人にも手が出しやすいと思います。
とにかくこれを使っていないとVimを使う時に半分くらい色々と損をしていると思います。絶対オススメ!!
Vimの中で完結出来るシェル実行環境
プログラミングなどを行う際にはcmd.exe(これは少ないでしょうけど)、Windows PowerShell、bash、zshなど様々なシェル環境を使用する事となると思いますが、Vimにはpure Vim scriptで書かれた独自のシェル、vimshell があります。
わざわざVimの中でシェルを使う事はないだろうと思うかもしれませんが、これが結構便利です。
まず、コードを書きながらVimから出る必要が一切ありません。
そしてVimの中で作業が出来るという事はVimのフル機能を使用する事が出来ます。
Vimのフル機能を使用するという事は当然プラグインを使用する事も出来ますし、その気になればプログラムのデバッグも出来ます。
シェルの履歴は当然Vimの中で保持出来ますし、unite.vimのsourceから引っ張る事も出来ます。
もし、vimshellで実行出来ないような環境があった場合には Conque というVimを疑似端末として振る舞わせるプラグインもあります。
Conqueは疑似端末としてシェルを起動するので、よりVimを端末として使用する事が出来ます。
当然疑似端末は互換性の代わりにVimとの親和性が低くなってしまう欠点もありますので、ご利用は計画的に。
あらゆるテキストを補完する事が出来る高度な補完環境
Vimではコードやテキストを補完出来るプラグインとして有名なのが acp.vim と neocomplcache などがあります。
これらはいれてみて即便利さが実感出来るため、とてもすぐに恩恵を受ける事が出来ます。
特にneocomplcacheはあらゆるテキストを補完するだけではなく、キャッシュし(それも非同期に)快適な補完を提供してくれます。
更にC++のような解析の難しいような言語でもlibclangを使用したneocomplcache-clang を使用すると精度の高い補完を行います。
※上記画像はboost::fusion::vectorのメンバをneocomplcache-clangでコード補完したもの
当然補完対象が多くなればなるほど、時間もかかりますので、あまりに大きすぎるライブラリを使用する時はご注意を。
C++に限らず、様々な言語に対応しているのでとりあえず使ってみてその素晴らしさを実感してください。
Linuxのパッケージ管理システムのようにプラグインを管理する
Vimのプラグインは数が増えるとその管理が厄介で手がつけられなくなる事もあります。
Linuxにはパッケージを管理するような仕組みが色々とありますが、Vimにはプラグインを管理するプラグインがあります。
現在主流なのは pathogen.vim や Vundle、neobundle.vim の3つが有名です。
私はneobundle.vimを使用しています。
基本的にはvimrcに管理したいプラグインの名前を記述し、あとは更新コマンドを実行するだけで常にプラグインの最新版を得る事が出来ます。
プラグインは自分でも何を使用しているのかよくわからなくなる時がありますが、これがあれば自分で何をいれたのかすぐにわかります。
出来るだけ始めに導入しておいて特に楽が出来るプラグインだと思います。
VimでTwitterを楽しむ
VimではTwitterも手軽に楽しむ事が出来るプラグインとして、TwitVim があります。
当然文字だけの世界になってしまうのですが、気軽にTweetしたり、TLをチラ見する程度には十分です。
あくまでもエディタを触っているようにしか見えないので、仕事中にTwitterするのにもピッタリですね!!
他にもvimshellを使用する事により、Termtter やUserStream対応の earthqueke.gem といったものも使用出来ます。
WindowsだとRubyとの相性の悪さでなかなか大変かもしれませんが、よりVimでTwitterを楽しみたいという方にはオススメです。
Boost.勉強会#7 東京に参加してきた
冒頭のprogress_display追悼式にはみんな笑った。
各々の発表内容はまとめるほど記憶が残っていないので印象に残っているところだけ。
Boostライブラリ一周の旅 - id:faith_and_brave
最近のBoostにて追加されたライブラリを軽く紹介。
個人的に特に注目したいのが計算幾何ライブラリのBoost.Geometryと新しいラムダ式を提供するBoost.Phoenix、C++11のコンテナ相当の機能を提供するBoost.ContainerとC++03でmove semanticsをエミュレーションするBoost.Move。
Boost.GeometryはBoost.FusionやBoost.Graphなどと同じようにコンセプトベースに設計されたライブラリでSTL同様、アルゴリズムとデータ構造を使い分けて使用出来ます。
ゲームなどで使えますが、まだ実用例がないと思われるので1度ちゃんと使ってみたいところです。
Boost.Phoenixは既にあるBoost.Lambdaとは違う手法でラムダ式を提供してくれるようですが、まだ私がまともにBoost.Lambdaすら使っていないのでその違いがよくわかっていません。
Boost.ContainerとBoost.Moveはそれぞれ既に連動しており、Boost.Containerを使用するとそれだけでBoost.Moveの恩恵を受けられるはず。
Boost.MoveはC++11で使用出来るmove semanticsをエミュレーションしてくれる素晴らしいライブラリですが、その中はなかなかの黒魔術が使用されているようです。
詳しいことはそのうちにでも調べてみたいなー。
clangで入門 解析戦略ー - id:fjnl
clangを使用してC++のソースコード解析を行なう話。
普段Vimでclang_completeを使用してC++のソースコード補完をしている私としては非常に興味のある話でした。
ただ、LLVMやclangの中は何度か読んだ事があるのですが、なかなか私の手に負えるようなものではありませんでした。
うーん、ドキュメントがもっとしっかりとしていればやってみる気もするんだけどなー。
Introduction to Boost.B-tree - id:eldesh
B-treeのBoost実装らしいですが、いまひとつ実用性があるのかわかりません。
個人的に質問してみたんですが、通常のsetやmapにはあるアロケータ指定がないのはちょっと致命的。
そして他にも色々と謎が多い。パフォーマンス的にも速いとは言えない?うーん…
中3女子でもわかるconstexpr - id:boleros
間違いなく今回一番濃かった発表。
C++11で新たに使用出来るようになったconstexprをフルに使用した場合どこまで出来るか試してみた、といった感じ。
コンパイル時文字列解析やコンパイル時レイトレーシングとか普通に考えたらマジキチですね…
しかしこれを理解出来る人は魔界の人間らしいので、人間界にいる私には到底理解出来ませんでした。
他にもネタ満載の発表で終始笑えるネタもあったりで満足した内容でした。
PPPUC++読み終わった
- 作者: ビャーネ・ストラウストラップ,Bjarne Stroustrup,επιστημη,エピステーメー,遠藤美代子(株式会社クイープ)
- 出版社/メーカー: 翔泳社
- 発売日: 2011/08/10
- メディア: 大型本
- 購入: 10人 クリック: 283回
- この商品を含むブログ (52件) を見る
原著名は「Programming Principles and Practice Using C++」
実はVimテクニックバイブルと同時期に購入していたこの本ですが、なかなかゆっくりと読む時間がなくて全部読み終わるまでに時間がかかりました。
1176ページは伊達じゃないです。こんな本はそうポンポンと読めるもんじゃないです。
だからと言って読む価値がないわけではなくて、むしろこれだけは読んでおけと言わんばかりの内容です。
実際に読むべき読者層
あくまでも個人的な感想です。
- C++をある程度知っているが本気で勉強をした事がない人
- 仕事でC++を使っていて恥かしくないコードが書きたい人
- C++を普段から使っているけど、標準ライブラリとかよくわかっていない人
- C++使ってないけど、ちょっとC++を本気でやってみたくなった人
- モダンなプログラミングを実践してみたい人
- 組込み開発でC++を使っている人
- たった一冊しか本が買えなくて、どんなプログラミングの本を買うべきか迷っている人
他にも色々とありそうだがこんなところで…
これは入門書?
本書は入門書と呼ばれていますが、プログラミングのプの字も知らないような人が読む本ではなくて、上記にも書いている通りある程度のプログラミング経験者向けではあります。
かと言ってそこまで難しい内容が書いているわけではありません。
あくまでも基本的なアルゴリズムやデータ構造に基づいてC++を用いた場合、どうやって実装すべきか?という現代的な実装方法を示しています。
そしてC言語などと比べた場合、何が悪くて何が良いのかを明確に示してくれます。
なので本当に細かいC++に関しての説明はありません。
が、大事なところはしっかりと説明してくれるので問題ありません。
個人的に面白かったのはゼロからスクラッチでstd::vector風のコンテナを作っていくところ。
標準ライブラリと言えば複雑なものですが、これを読めば少しはそんなイメージが払拭出来るかもしれません。
割りと上級者な人
まぁC++上級者なんてどのレベルから区切ればいいのかわかりませんが、ある程度C++の深いところを知っている人にはそこまで重要な本ではないと思います。
そういう意味では初級中級者向け。
この本を読んでみて「ああ、大体知っているし、普段からそれを実践しているよ」という人は値段相応の価値はないかもしれません。
しかし、基本を抑えておきたいという人にはオススメ出来ると思います。
私もそれが理由で購入した人なので。
結局いい本なのか?
全体的にみて素晴しい本だと思いましたが、ネックなのはやはりその圧倒的なぶ厚さです。
持ち運ぶのはもちろん、家の中で読むのも躊躇う厚さです。
それがネックと感じない人には絶対オススメ出来る内容ではないかと。
Vim勉強会#10に参加してきた
Vim勉強会#10
題名の通りです。
主催者のujihisaさんから「ブログに記事を書くまでがVim勉強会です」というメールを頂いたので、早速書いています。
全体的に知っている事が多かったのは事実ですが、数少ない他のVimmerに会えるというチャンスで参加してみるとなかなかに新鮮でした。
特にujihisaさんが使うVimはよく訓練されており、vimrcも2000行近くあったような気がします。
「Vimmerって細かい事気にするよな!自分もだけど!」と思わざるえない。
あとなぜかVim勉強会なのにEmacsセッションがふたつもあったりと別の意味でも新鮮でした。
私自身はそこまで色々と出来るVimmerではありませんが、今後のVimのためにも色々やりたいなぁと思う事があるので近いうちに何かやってみたいと思います。
Vimのインデントについて
Vimはデフォルトでもとても優秀なインデント機能があります。
更にVimには4つのインデントスタイルが存在しており、helpによると以下のようになっています。
'autoindent' 一つ前の行に基づくインデント 'smartindent' 'autoindent'と同様だが幾つかのC構文を認識し、適切な箇所のインデントを増減させる。 'cindent' 他の2つの方法よりも賢く動作し、設定することで異なるインデントスタイルにも対応できる。 'indentexpr' この中で一番融通が利く: ある行のインデントを計算するのにVimスクリプトを実行する。 この方法が有効である(空でない)時にはその他 のインデントは抑制される。
特にCライクなプログラミング言語な場合は、非常に細かいインデント設定が出来るようになっており、'cindent'を設定すればより賢くインデントをしてくれるようになります。
必要に応じて"set cindent"を設定するようにしましょう。それぞれを同時に設定することは出来ません。
自分好みにインデントを決めたい場合
'cinoptions'または'cino'に対してオプション値を与える必要があります。
非常にきめ細かな設定が出来るようになっており"help cinoptions-values"という具合にヘルプを引けば詳細を知る事が出来ます。
例1
// インデント指定がない場合 int main() { int a = 0; switch (a) { case 0: a = 2; break; } } // インデント指定がある場合 int main() { int a = 0; switch (a) { case 0: a = 2; break; } }
上記はswitch-case文でインデントを指定した時と指定していない時の違いです。
"case 0:"以降のインデントが変わっています。
上記の設定はvimrcに
set cinoptions+=:0
というオプション設定に値を加えた場合が下の例になります。
例2
// インデント指定がない場合 class Hoge { public: Hoge(); private: int hoge; }; // インデント指定がある場合 class Hoge { public: Hoge(); private: int hoge; };
set cinoptions+=g0
上記のオプション設定を加えると例のようになります。
また、例1と例2を複合させたい場合
set cinoptions+=:0,g0
というように指定する事が出来ます。
とりあえずヘルプで'cinoptions-values'を検索してみよう
ビックリするほどに複雑な設定例が出てきます。
恐らく自分好みのインデントスタイルを作るには困らないと思います。
インデントも環境によって様々な場合がありますので、自分好みに変えられるようにしておきましょう。
eskk.vimを使ってみた
Vim使いでかつ、SKK使いというのは絶対数的にも少ないと思われますがそういう人達のためのVimプラグインとして、eskk.vimというものがあります。
もう既に公開されてからそれなりに時間が経っているはずなのですが、とにかく情報が少ない…
私も普段はWindowsでSKKIMEを使っているので、このたび一度しっかりと使ってみようと思い挑戦しました。
基本設定(vimrc)
let g:eskk#directory = "~/.eskk" let g:eskk#dictionary = { 'path': "~/.skk-jisyo", 'sorted': 0, 'encoding': 'utf-8', } let g:eskk#large_dictionary = { 'path': "~/.eskk/SKK-JISYO.L", 'sorted': 1, 'encoding': 'euc-jp', }
SKK-JISYO.L はここらへんから落としてきましょう。
とりあえず最低限これだけ書いておけばいいと思います。
Windowsで別のIMEを普段使用している人の場合はVimでeskk.vimを使用するために以下を追加しておくといいと思います。
set imdisable
これで他のIMEに邪魔されずにeskk.vimを使うことが出来ます。
neocomplcacheとの補完機能を連携させたい場合は以下も追加。
let g:eskk#enable_completion = 1
個人的にはこの補完機能が使えるところがeskk.vim最大の利点だと思います。
使用していて気になったところ
使用して色々気になっているところがあったので箇条書き。
- 変換をした後に
で変換を完了させると必ずeskk.vimが無効になる。 - 補完で選んだものを
で選択しようとすると、前の文字を破壊して消してしまう。 - 変換の取り消しに
が効かない。 eskk.vim有効時に改行をした時、1行ではなく2行改行される ※g:eskk#egg_like_newline を有効にすれば1行になる模様- ドキュメントにオプション設定に関する説明がないものが多く機能が理解出来ない。
とりあえずパっと思いつくのがこのくらいでしょうか…
特に私が普段使用しているSKKIMEでは、
SKKは自分の頭で考えながらリズミカルに変換出来るのが魅力なのでこのペースが落ちるのは結構辛いところ。次に
まだeskk.vimはバグも多いらしいので作者のtyruさんに声が届くようにみんなでもっと使いましょう!
※追記
eskk.vim有効時に改行をした時、1行ではなく2行改行される
上記の問題は特にオプションも関係なかった模様。他の設定が何か絡んでいたのかも…