この記事はIQが1Advent Calendarの10日目の記事になります.
昨日はMew_1406さんのIQ1と謝罪行脚と題された,怖いお話でしたね.
はじめに
ご存知の通り,僕はIQ1です.
IQ1には様々な困難が存在します.
例えば,物が覚えられない....
僕はプログラムを書くことがあるのですが,基本的なイディオムや標準ライブラリの関数の使い方等を覚えられず,このために苦戦することがあります.
そこで,この記事ではIQ1でも多少楽にプログラムを書く手法を紹介したいと思います.
特に,僕が書くことのあるC++に焦点を当てたいと思います.
基本的にほとんどのエディタには「スニペット展開」という機能が標準,あるいはプラグインという形で利用可能です.
スニペット展開とは何か,それは以下のGIF画像を見てもらうのが早いでしょう.
このように,事前に登録しておいたコードのテンプレートを挿入するのが,コードスニペットの展開というものになります.
これは単なるコーディング速度の向上だけでなく,僕のようなIQ1にとっては記憶補助にもなるわけです.
前提
僕は普段テキストエディタとしてVimを使っています.
ですので,この記事において,特に断りがない限り,Vimを使っていることを前提とします.
また,Vimでスニペット展開を行うプラグインとして,Shougo/neosnippet.vimを使うこととします
ただ,スニペット展開自体は,前述の通り,今頃のテキストエディタにもあるものなので,Vimはあくまで一例としてください.
neosnippetの導入
Vimとneosnippet.vimを前提とするので,最小限のインストール方法について記載します.
プラグインマネージャに,Shougo/dein.vimを使っているなら,.vimrcに以下のような記述を加えると,使用できるようになるはずです.
neosnippet.vimは補完プラグインとして,Shougo/deoplete.nvimやShougo/neocomplete.vimを導入しておくと,ベンリさが100倍になるので,一緒に入れておきましょう.
Neovim,Vim8(Windows除く)の場合はShougo/deoplete.nvim,Vim7やWindowsの場合はneocomplete.vimを導入する設定になっています.
(Windowsではdeopleteの補完が遅いと聞いたので除外)
set encoding=utf-8
if has('vim_starting')
let s:deindir = expand('~/.cache/dein')
let s:deinlocal = s:deindir . '/repos/github.com/Shougo/dein.vim'
let &runtimepath = s:deinlocal . ',' . &runtimepath
endif
if dein#load_state(s:deindir)
call dein#begin(s:deindir)
call dein#add('Shougo/dein.vim')
if has('nvim') || !has('win32') && v:version >= 704
call dein#add('Shougo/deoplete.nvim', {
\ 'on_event': 'InsertEnter',
\})
if !has('nvim')
call dein#add('roxma/nvim-yarp')
call dein#add('roxma/vim-hug-neovim-rpc')
endif
elseif v:version > 703 || (v:version == 703 && has('patch885'))
if has('lua')
call dein#add('Shougo/neocomplete.vim', {
\ 'on_event': 'InsertEnter',
\ 'on_cmd': [
\ 'NeoCompleteEnable',
\ 'NeoCompleteDisable',
\ 'NeoCompleteLock',
\ 'NeoCompleteUnlock',
\ 'NeoCompleteToggle',
\ 'NeoCompleteSetFileType',
\ 'NeoCompleteClean',
\ 'NeoCompleteBufferMakeCache',
\ 'NeoCompleteDictionaryMakeCache',
\ 'NeoCompleteSyntaxMakeCache',
\ 'NeoCompleteTagMakeCache'
\ ]
\})
else
call dein#add('Shougo/neocomplcache', {
\ 'on_event': 'InsertEnter',
\ 'on_cmd': [
\ 'NeoComplCacheEnable',
\ 'NeoComplCacheDisable',
\ 'NeoComplCacheLock',
\ 'NeoComplCacheUnlock',
\ 'NeoComplCacheToggle',
\ 'NeoComplCacheLockSource',
\ 'NeoComplCacheUnlockSource',
\ (v:version >= 703 ? 'NeoComplCacheSetFileType' : 'NeoComplCacheSetFileType'),
\ 'NeoComplCacheSetFileType',
\ 'NeoComplCacheClean',
\ ],
\ 'on_map': [['is', '<Plug>(neocomplcache_snippets_']]
\})
endif
endif
call dein#add('Shougo/neosnippet', {
\ 'on_event': 'InsertEnter',
\ 'on_cmd': [
\ 'NeoSnippetEdit',
\ 'NeoSnippetMakeCache',
\ 'NeoSnippetSource',
\ 'NeoSnippetClearMarkers'
\ ],
\ 'on_ft': 'neosnippet',
\ 'on_map': [['nisx', '<Plug>(neosnippet_']],
\})
call dein#add('Shougo/neosnippet-snippets')
call dein#end()
call dein#save_state()
endif
if dein#tap('deoplete.nvim')
let g:deoplete#enable_at_startup = 1
endif
if dein#tap('neocomplete.vim')
let g:neocomplete#enable_at_startup = 1
endif
if dein#tap('neocomplcache')
let g:neocomplcache_enable_at_startup = 1
endif
if dein#tap('neosnippet')
imap <C-k> <Plug>(neosnippet_expand_or_jump)
smap <C-k> <Plug>(neosnippet_expand_or_jump)
imap <expr><TAB> neosnippet#expandable() <Bar><Bar> neosnippet#jumpable() ?
\ "\<Plug>(neosnippet_expand_or_jump)" : pumvisible() ? "\<C-n>" : "\<TAB>"
smap <expr><TAB> neosnippet#expandable() <Bar><Bar> neosnippet#jumpable() ?
\ "\<Plug>(neosnippet_expand_or_jump)" : "\<TAB>"
let g:neosnippet#snippets_directory = '~/.vim/neosnippets'
let g:neosnippet#expand_word_boundary = 1
endif
filetype plugin indent on
syntax enable
動作確認
まず,
$ vim main.cpp
として,Vimを起動し,インサートモードに入り,main<C-k>
としてみましょう.
以下のgifアニメのようになれば問題ありません.
なお,このgifアニメでは,前述の最低限の .vimrc
を用いたときのスクリーンキャストを貼っていますが,これ以降は僕が普段使用している .vimrc
でのスクリーンキャストを貼り付けます.
さて,ここまでで,Vimにスニペットプラグインを導入し,デフォルトのスニペットを展開することをしました.
しかし,スニペットとは自分で追加していくもの...ここでは僕が実際に普段使っているC++のスニペットを紹介することにしましょう.
neosnippetのファイル配置
今回は ~/.vim/neosnippets/
にスニペットファイルを置きます.
前述の設定例でも,このパスを指定していますね.
C++のスニペットの場合は, cpp.snip
というファイル名にしなければなりません.
(つまり,フルパスは ~/.vim/neosnippets/cpp.snip
)
neosnippetのスニペット定義について
:h neosnippet-snippet-syntax
を見るのが早いですが,簡単に.
snippet [[スニペットのトリガー]]
abbr [[deoplet等の補完時に出てくる説明 (省略可)]]
alias [[別のトリガーを指定可能 (省略可)]]
regex [[ここに記述した正規表現にマッチしている場合にのみ展開可能 (省略可)]]
option [[説明が面倒なので,ヘルプを見て]]
[[1段階インデントした位置にスニペット本体を記述]]
つまり,最低限のスニペットの例は以下のようになります.
snippet rbf
for (auto&& ${1:e} : ${2:#:container}) {
${0}
}
さて,よくわからない記述が出てきましたね.
${数字}
は <C-k>
を押下する度にカーソルの移動する位置を表していて,そのときのデフォルトの展開結果を指定してたりします.
${0}
は最終的にカーソルが移動する位置,
${1:e}
は e
をデフォルトの展開結果(デフォルトで e
が挿入される)とした,1番目のカーソルの移動位置,
${2:#:container}
は2番目のカーソル移動位置で, container
と表示はするものの,${1:e}
と違って実際にテキスト挿入を行わないもの(コメント的なもの)となっています.
単に ${1}
と書いた場合,デフォルト値が無いカーソル移動位置となります.
まぁ,詳細は :h neosnippet-snippet-syntax
を見るなり,Shougo/neosnippet-snippetsの例を見てください.
main()関数
まずは main()
からだよね,ということで,スニペットを作ってみます.
まぁ,これはデフォルトのスニペットでも定義されているのですが,細かい部分で好みに合わないので,書き換えます.
C++のmain関数は
int
main(int argc, const char* argv[])
{
return EXIT_SUCCESS;
}
って感じで書くので,単純に以下のようにスニペット化します.
デフォルトで定義されているのを無効化するために,直前にdelete main
を書きます.
snippet main
int
main(${1:int argc, const char* argv[]})
{
${0};
return EXIT_SUCCESS;
}
すると,素早く以下のようにmain関数を書けるようになります.
競プロをやっている人は普段使ってるテンプレートをスニペット登録しておくと便利かもしれないですね.
インクルードガード
ヘッダファイルにはインルードガードを書きますよね.
コンパイラ拡張をなるべく嫌う派としては, #pragma
を使わず,
#ifndef HOGE_H
#define HOGE_H
#endif
と書くと思いますが...面倒!
特に HOGE_H
が2回登場しているので,これはスニペット化を考えるところです.
そこで以下のスニペットを用意しましょう.
(僕は #endif
の後ろにコメント入れる主義なので,それも加えています)
snippet inc_guard
#ifndef ${1:#:NAME}
#define $1
{0}
#endif // $1
すると, inc_guard<C-k>
の入力で以下のように素早くインクルードガードを記述できます.
std::array
や std::vector
等の合計値を出す
std::array
や std::vector
の要素の合計値を出したいときに困ることのひとつに std::sum()
みたいな単純な関数が無いことがあると思います.
<numeric>
の std::accumulate()
を使えばいいのですが,
std::vector<int> vct(10);
std::generate(std::begin(vct), std::end(vct), std::mt19937(std::random_device{}()));
auto sum = std::accumulate(std::cbegin(vct), std::cend(vct), 0);
このように,非常にタイプ数が多くて困りものです.
なので,IQ1の僕は無い知恵を絞り,以下のスニペットを登録しました.
snippet sum
std::accumulate(std::cbegin(${1}), std::cend($1), ${2:decltype($1)::value_type()})
第三引数を decltype(vct)::value_type()
のように展開できるようにしておくと,要素型の値が何であってもデフォルト値を利用できるようになるので便利です.
例えば,int
型なら 0
ですし, double
型なら 0.0
ですね.
(要素型が double
であるのに,第三引数に 0
を指定すると,恐らく望んだ結果は返ってこないので,そういう事故防止にも役立ちます)
C++11以降の <regex>
の正規表現マッチングによるループ
C++11になり <regex>
が追加されて,C++でも気軽に正規表現が利用できる時代となりました.
しかし,ここで問題が1つあります.
それは,<regex>
は利用に際し,やや複雑なコードが要求されることです.
例えば,正規表現のマッチングによるループの例を見てみましょう.
std::string text("2017-12-10 12:34:56");
std::regex ptn("\\d+");
for (std::sregex_iterator itr = std::sregex_iterator(std::cbegin(text), std::cend(text), ptn), end; itr != end; ++itr) {
std::cout << itr->str() << std::endl;
}
長い...こんなもの,とてもIQ1が気軽に利用できるものではありません!
しかし,こういうものはテンプレ,そして共通項があるというもの.
エイヤッと以下のスニペットを用意しましょう.
入力すべき項目は以下の4つです.
- イテレータの変数名
- 対象となる
std::string
の変数名
- マッチパターンとなる
std::regex
の変数名
- イテレータの終端を表す
std::sregex_iterator
のデフォルトでコンストラクトされたものを格納する変数名
4つ目は正直,決め打ちでもよいかな感はあるのですが,一応指定できるようにしておくと,同じブロックで既に変数名が使用されていた場合に対応できるというメリットがあります.
snippet regex_match_loop
for (std::sregex_iterator ${1:itr} = std::sregex_iterator(std::cbegin(${2:#:text}), std::cend($2), ${3:#:regex}), ${4:end}; $1 != $4; ++$1) {
${0}
}
スリープ
かつて,C++では,標準ライブラリでスリープを行う関数が提供されておらず,各環境に応じたスリープ系の関数を呼び出す必要がありました.
時は移り,C++11... <chrono>
や <thread>
が追加され,標準ライブラリの関数でスリープを行うことが可能になりました.
ただし,やや長ったらしい記述が必要となるため,IQ1にとっては書くのが苦痛です.
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
スリープに必要なのは以下の2つ.
- 時間分解能(ミリ秒とか)
- どの程度スリープするか
なので,以下のスニペットを用意してみましょう.
std::this_thread::sleep_for(std::chrono::${1:milliseconds}(${2:1000}));
<algorithm>
の関数にラムダを渡す
最もスニペットが有用なのは,ラムダ取る <algorithm>
の関数のスニペットでしょう.
例えば, std::array
の要素の内,3で割り切れるものがいくつあるか数えるコードを考えると,
std::array<int, 10> arr;
std::iota(std::begin(arr), std::end(arr), 0);
auto cnt = std::count_if(
std::cbegin(arr),
std::cend(arr),
[](const auto& e) {
e % 3 == 0;
});
と書くと思います.
しかし,これはかなり面倒.
特にラムダの辺りが嫌な感じですね.
これをスニペットにすると以下のような感じでしょうか.
snippet count_if
alias count_f
abbr std::count_if <algorithm>
std::count_if(
std::cbegin(${1}),
std::cend($1),
[](const auto& ${2:e}) {
return ${0};
});
記述量が減って,かなり快適に書けるようになりました!
ここで紹介している std:count_if
だけでなく, std::sort
や std::accumulate
のラムダを取る版のスニペットを用意しておくと,非常に便利になるでしょう.
スニペットファイルを用意しました!!!
さて,ここまで紹介してきたneosnippetのC++用のスニペット定義ファイルを用意しました.
C++11, C++14, C++17用と用意しています.
適当にコピペするなり,改変するなりして使ってください.
差異
C++11/C++14/C++17用のスニペットの差異は以下の通りです.
普段利用しているコンパイラに応じたものを使うといいでしょう.
例えば,競プロのジャッジサーバにC++11のコンパイラしか入っていないのであれば,C++11を使うのがよいでしょう.
std::sort()の述語やstd::accumulate()の集計関数等のラムダにジェネリックラムダを利用するように
decltype(vct)::value_type
は見た目的に長いので,短い記法を使ってスッキリさせようというやつです.
関数等で参照として受け取った std::vector
等に対して用いる場合でも,いちいち std::remove_reference
を狭まなくてよくなるので,楽ですね.
# before
std::sort(
std::begin(${1}),
std::end($1),
[](const decltype($1)::value_type& x, const decltype($1)::value_type& y) {
return ${0:x < y};
});
# after
std::sort(
std::begin(${1}),
std::end($1),
[](const auto& x, const auto& y) {
return ${0:x < y};
});
<algorithm>
の読取専用のイテレータ引数に対して,std::begin()/std::end()でなく.std::cbegin()/std::cend()を用いるように
生成されるコードは変わらないと思うんですが,const付けられるものに対してはconstの方がいいよねというやつです.
フリー関数の std::begin()
, std::end()
はC++11で追加されたんですが,何故か std::cbegin()
, std::cend()
はC++14になってから追加されたので,それに合わせた変更になります.
C++11でもメンバー関数版の cbegin()
, cend()
使えばいいじゃないという話になりそうですが, std::begin()
, std::end()
と釣り合いが取れなくなって気持ち悪いので....
# before (C++11)
snippet sum
std::accumulate(std::begin(${1}), std::end($1), ${2:decltype($1)::value_type()})
# after (C++14)
snippet sum
std::accumulate(std::cbegin(${1}), std::cend($1), ${2:decltype($1)::value_type()})
<type_traits>
の関数
C++11では型の取得のために ::type
にアクセスしていましたが,より簡潔に書けるようになったので,そちらを利用.
# before
snippet decay
std::decay<T>::type
# after
snippet decay
std::decay_t<T>
<type_traits>
の関数
C++14までは型の判定に使用できるメタ関数の真偽値は value
メンバーから取得していましたが,C++17ではもっと楽に取得できるようになったので,そちらを用いるようにしました.
まとめ
なお,neosnippet.vimの後続として,deoppet.nvimが開発されているとのことです.
Vimconf 2017でShougoさんは,スニペットファイルはneosnippetと同じ形式とおっしゃていたと思うので,今の内にneosnippet用のスニペットファイルを充実させても損にはならないと思います.
最後に
ちゃっくさんの金で肉が食べたい!
明日は,shrcyanさんの記事になります.楽しみですね.