koturnの日記

転職したい社会人2年生の技術系日記

IQ1を支えるコーディング術

この記事はIQが1Advent Calendarの10日目の記事になります. 昨日はMew_1406さんのIQ1と謝罪行脚と題された,怖いお話でしたね.

はじめに

ご存知の通り,僕はIQ1です. IQ1には様々な困難が存在します.

例えば,物が覚えられない.... 僕はプログラムを書くことがあるのですが,基本的なイディオムや標準ライブラリの関数の使い方等を覚えられず,このために苦戦することがあります.

そこで,この記事ではIQ1でも多少楽にプログラムを書く手法を紹介したいと思います. 特に,僕が書くことのあるC++に焦点を当てたいと思います.

スニペット展開

基本的にほとんどのエディタには「スニペット展開」という機能が標準,あるいはプラグインという形で利用可能です. スニペット展開とは何か,それは以下のGIF画像を見てもらうのが早いでしょう.

f:id:koturn:20171210023943g:plain

このように,事前に登録しておいたコードのテンプレートを挿入するのが,コードスニペットの展開というものになります. これは単なるコーディング速度の向上だけでなく,僕のようなIQ1にとっては記憶補助にもなるわけです.

前提

僕は普段テキストエディタとしてVimを使っています. ですので,この記事において,特に断りがない限り,Vimを使っていることを前提とします. また,Vimスニペット展開を行うプラグインとして,Shougo/neosnippet.vimを使うこととします

ただ,スニペット展開自体は,前述の通り,今頃のテキストエディタにもあるものなので,Vimはあくまで一例としてください.

neosnippetの導入

Vimneosnippet.vimを前提とするので,最小限のインストール方法について記載します.

プラグインマネージャに,Shougo/dein.vimを使っているなら,.vimrcに以下のような記述を加えると,使用できるようになるはずです. neosnippet.vimは補完プラグインとして,Shougo/deoplete.nvimShougo/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アニメのようになれば問題ありません.

f:id:koturn:20171210023827g:plain

なお,この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関数を書けるようになります.

f:id:koturn:20171210023837g:plain

競プロをやっている人は普段使ってるテンプレートをスニペット登録しておくと便利かもしれないですね.

インクルードガード

ヘッダファイルにはインルードガードを書きますよね. コンパイラ拡張をなるべく嫌う派としては, #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> の入力で以下のように素早くインクルードガードを記述できます.

f:id:koturn:20171210023841g:plain

std::arraystd::vector 等の合計値を出す

std::arraystd::vector の要素の合計値を出したいときに困ることのひとつに std::sum() みたいな単純な関数が無いことがあると思います. <numeric>std::accumulate() を使えばいいのですが,

// #include <algorithm>
// #include <vector>
// #include <numeric>
// #include <random>

// 以下の2行は乱数入れてるだけなので,無視していいです
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 を指定すると,恐らく望んだ結果は返ってこないので,そういう事故防止にも役立ちます)

f:id:koturn:20171210023943g:plain

C++11以降の <regex>正規表現マッチングによるループ

C++11になり <regex> が追加されて,C++でも気軽に正規表現が利用できる時代となりました. しかし,ここで問題が1つあります. それは,<regex> は利用に際し,やや複雑なコードが要求されることです.

例えば,正規表現のマッチングによるループの例を見てみましょう.

// #incluse <iostream>
// #incluse <regex>
// #incluse <string>

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つです.

  1. イテレータの変数名
  2. 対象となる std::string の変数名
  3. マッチパターンとなる std::regex の変数名
  4. イテレータの終端を表す 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}
  }

f:id:koturn:20171210023947g:plain

スリープ

かつて,C++では,標準ライブラリでスリープを行う関数が提供されておらず,各環境に応じたスリープ系の関数を呼び出す必要がありました. 時は移り,C++11... <chrono><thread> が追加され,標準ライブラリの関数でスリープを行うことが可能になりました. ただし,やや長ったらしい記述が必要となるため,IQ1にとっては書くのが苦痛です.

// #include <chrono>
// #include <thread>
std::this_thread::sleep_for(std::chrono::milliseconds(1000));

スリープに必要なのは以下の2つ.

  1. 時間分解能(ミリ秒とか)
  2. どの程度スリープするか

なので,以下のスニペットを用意してみましょう.

std::this_thread::sleep_for(std::chrono::${1:milliseconds}(${2:1000}));

f:id:koturn:20171210023950g:plain

<algorithm> の関数にラムダを渡す

最もスニペットが有用なのは,ラムダ取る <algorithm> の関数のスニペットでしょう. 例えば, std::array の要素の内,3で割り切れるものがいくつあるか数えるコードを考えると,

// #include <algorithm>
// #include <array>
// #include <numeric>
// #include <iostream>

// 要素数とか入れる値は適当で
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};
    });

f:id:koturn:20171210023954g:plain

記述量が減って,かなり快適に書けるようになりました! ここで紹介している std:count_if だけでなく, std::sortstd::accumulate のラムダを取る版のスニペットを用意しておくと,非常に便利になるでしょう.

スニペットファイルを用意しました!!!

さて,ここまで紹介してきたneosnippetのC++用のスニペット定義ファイルを用意しましたC++11, C++14, C++17用と用意しています. 適当にコピペするなり,改変するなりして使ってください.

差異

C++11/C++14/C++17用のスニペットの差異は以下の通りです. 普段利用しているコンパイラに応じたものを使うといいでしょう. 例えば,競プロのジャッジサーバにC++11のコンパイラしか入っていないのであれば,C++11を使うのがよいでしょう.

C++11 -> C++14

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>

C++14 -> C++17

<type_traits> の関数

C++14までは型の判定に使用できるメタ関数の真偽値は value メンバーから取得していましたが,C++17ではもっと楽に取得できるようになったので,そちらを用いるようにしました.

まとめ

なお,neosnippet.vimの後続として,deoppet.nvimが開発されているとのことです. Vimconf 2017でShougoさんは,スニペットファイルはneosnippetと同じ形式とおっしゃていたと思うので,今の内にneosnippet用のスニペットファイルを充実させても損にはならないと思います.

最後に

ちゃっくさんの金で肉が食べたい!

明日は,shrcyanさんの記事になります.楽しみですね.