koturnの日記

普通の人です.ブログ上のコードはコピペ自由です.

C++のラムダで再帰する

はじめに

C++において,ラムダで再帰したいと考えることはたまにある. この記事ではラムダで再帰する手法をいくつか紹介する. 例として扱う再帰関数はフィボナッチ数列の関数(もっとも単純な実装)とする.

int
fib(int n) noexcept
{
  return n < 2 ? n : (fib(n - 1) + fib(n - 2));
}

生成コード

ラムダで再帰を行いたいと考えるのは変数のキャプチャをしつつ再帰をしたい場面であると思う. 例えば,メモ化再帰を行うと考えた場合,

  1. グローバル変数としてメモ化配列を用意する
  2. メモ化再帰用のクラスを用意する(メンバにメモ化配列を用意する)
  3. 再帰関数の引数にメモ化配列を与える

といった方法が考えられるが,グローバル変数や専用のクラス定義が必要であり,あまりやりたくない. 引数にメモ化配列を与えるとなると,全ての再帰呼び出し箇所の記述が冗長になってしまう. ラムダで再帰ができれば,ローカル変数にメモ化用の配列を用意してもキャプチャできるようになるため,スッキリすると思われる.

3つの手法

この記事ではラムダで再帰を実現する手法を3つ紹介する.

1. std::function による無名再帰

C++11でない限り,絶対に使うべきではない

実装としては以下の通り.

#include <functional>
#include <iostream>
#include <utility>


int
main()
{
  std::function<int(int)> fib = [&fib](int n) -> int {
    return n < 2 ? n : (fib(n - 1) + fib(n - 2));
  };
  auto result = fib(10);
  std::cout << result << std::endl;
}

生成コード

利点と欠点は以下の通り.

利点

  1. C++11でも使用可能

欠点

  1. std::function を用いるため,実行コストが大きい(速度)
  2. std::function を用いるため,再帰可能深度が小さい
  3. 無名にできない
  4. std::function とラムダそのものに引数等の返り値を書く必要があり冗長

なぜかC++関連の記事を見ていると,この手法を紹介している記事が目につくのだが,言うまでもなく std::function を用いているのがとにかく良くない. よく知られている通り,std::function は型消去を行うため,与えた関数をインライン展開できない,実行コストが大きいといった欠点を持つ. std::functionoperator() の実装,あるいは生成されるコードを見ればわかるように( std::function 自体の operator() はインライン展開されるので,g++で -S --verbose-asm のようなオプションを付けてアセンブリを吐かせ,呼び出し部分のコードを確認すれば見てとれる),

  1. 保持している関数が有効かどうか(関数を保持しているかどうか)をチェックするコードが operator() に含まれる
  2. 関数が有効でない場合の, std::bad_function_call 例外を投げるためのコードが含まれる

といった欠点がある.これらの理由により,1回の関数呼び出しに時間がかかるだけでなく,消費スタック量が大きく,再帰可能深度が小さくなる. そのため,C++11でない限りは使うべきではない. 強いて利点を挙げるならば,下準備に必要な関数およびクラスがないため,ゼロからコードを書く場合には楽という利点はあるかもしれない.

本記事の「ラムダで再帰」に限った話ではなく,std::function を使用するべきかどうかは慎重にならなければならない. autoやテンプレートの型パラメータを用いることによって,いかにラムダをラムダのまま保持するかを考えるべきだ.

2. ジェネリックラムダを用いる方法

実装としては以下の通り. 可変引数はとりあえず完全転送すればよい.

なお, nodiscard 属性を付けているのは,返り値を利用しない場面は考えられないためである.

#include <iostream>
#include <utility>


namespace
{
template <typename F>
#if defined(__has_cpp_attribute) && __has_cpp_attribute(nodiscard)
[[nodiscard]]
#elif defined(__GNUC__) && (__GNUC__ > 3 || __GNUC__ == 3 && __GNUC_MINOR__ >= 4)
__attribute__((warn_unused_result))
#elif defined(_MSC_VER) && _MSC_VER >= 1700 && defined(_Check_return_)
_Check_return_
#endif  // defined(__has_cpp_attribute) && __has_cpp_attribute(nodiscard)
inline constexpr decltype(auto)
fix(F&& f) noexcept
{
  return [f = std::forward<F>(f)](auto&&... args) {
    return f(f, std::forward<decltype(args)>(args)...);
  };
}
}  // namespace


int
main()
{
  auto result = fix([](auto f, int n) -> int {
    return n < 2 ? n : (f(f, n - 1) + f(f, n - 2));
  })(10);
  std::cout << result << std::endl;
}

生成コード

利点と欠点は以下の通り.

利点

  1. 実行コストは小さい
  2. 無名再帰可能
  3. C++17以降ではコンパイル時計算可能なラムダによる再帰関数を記述可能

欠点

  1. C++14以降
  2. 再帰関数の引数にその再帰関数自身を与える必要があるため,関数本体の記述が冗長.

この手法はこの記事のものである. ジェネリックラムダを用いる必要があるため,C++14以降でなければならない. 2018年の今となっては困ることはないと思うが,例えば競プロのジャッジサーバがC++11までしか対応していない場面では利用できない.

std::function を用いる方法と違い,ラムダをラムダのまま保持しているので,関数呼び出しのコストは小さい. C++17以降では,ラムダが constexpr に対応するため,コンパイル時計算が可能となる. これは上記コードの auto resultconstexpr auto result に書き換えて,C++17対応のコンパイラコンパイルすれば確認できる.

3. ジェネリックラムダと operator() を用いる方法

実装としては以下の通り.

#include <iostream>
#include <utility>


template <typename F>
class
#if defined(__has_cpp_attribute) && __has_cpp_attribute(nodiscard)
[[nodiscard]]
#endif  // defined(__has_cpp_attribute) && __has_cpp_attribute(nodiscard)
FixPoint final : private F
{
public:
  template <typename G>
  explicit constexpr FixPoint(G&& g) noexcept
    : F{std::forward<G>(g)}
  {}

  template <typename... Args>
  constexpr decltype(auto)
  operator()(Args&&... args) const
#if !defined(__GNUC__) || defined(__clang__) || __GNUC__ >= 9
    noexcept(noexcept(F::operator()(std::declval<FixPoint>(), std::declval<Args>()...)))
#endif  // !defined(__GNUC__) || defined(__clang__) || __GNUC__ >= 9
  {
    return F::operator()(*this, std::forward<Args>(args)...);
  }
};  // class FixPoint


#if defined(__cpp_deduction_guides)
template <typename F>
FixPoint(F&&)
  -> FixPoint<std::decay_t<F>>;
#endif  // defined(__cpp_deduction_guides)


namespace
{
template <typename F>
#if !defined(__has_cpp_attribute) || !__has_cpp_attribute(nodiscard)
#  if defined(__GNUC__) && (__GNUC__ > 3 || __GNUC__ == 3 && __GNUC_MINOR__ >= 4)
__attribute__((warn_unused_result))
#  elif defined(_MSC_VER) && _MSC_VER >= 1700 && defined(_Check_return_)
_Check_return_
#  endif  // defined(__GNUC__) && (__GNUC__ > 3 || __GNUC__ == 3 && __GNUC_MINOR__ >= 4)
#endif  // !defined(__has_cpp_attribute) || !__has_cpp_attribute(nodiscard)
inline constexpr decltype(auto)
makeFixPoint(F&& f) noexcept
{
  return FixPoint<std::decay_t<F>>{std::forward<std::decay_t<F>>(f)};
}
}  // namespace


int
main()
{
  auto result = makeFixPoint([](auto f, int n) -> int {
    return n < 2 ? n : (f(n - 1) + f(n - 2));
  })(10);
  std::cout << result << std::endl;
}

生成コード

利点

  1. 実行コストは小さい
  2. 無名再帰可能
  3. C++17以降ではコンパイル時計算可能なラムダによる再帰関数を記述可能

欠点

  1. C++14以降

手法としてはこれがベストである. 2つ目の手法異なり,再帰関数の引数にその再帰関数自身を与える必要もない.

類似する実装として,こういうものがあるが,これでは2つ目の手法と変わりない. ラムダの第一引数に FixPoint クラスのオブジェクトを与える(すなわち, *this を与える)ように, FixPoint::operator() を定義するのがポイントである.

ちなみに,実装をもっと簡略化し,コアとなる部分だけ抜き出すと以下のようになる.

#include <iostream>
#include <utility>


template <typename F>
class
FixPoint : private F
{
public:
  explicit constexpr FixPoint(F&& f) noexcept
    : F{std::forward<F>(f)}
  {}

  template <typename... Args>
  constexpr decltype(auto)
  operator()(Args&&... args) const
  {
    return F::operator()(*this, std::forward<Args>(args)...);
  }
};  // class FixPoint


namespace
{
template <typename F>
inline constexpr decltype(auto)
makeFixPoint(F&& f) noexcept
{
  return FixPoint<F>{std::forward<F>(f)};
}
}  // namespace


int
main()
{
  auto result = makeFixPoint([](auto f, int n) -> int {
    return n < 2 ? n : (f(n - 1) + f(n - 2));
  })(10);
  std::cout << result << std::endl;
}

ただし,この実装では

  • makeFixPointFixPoint のコンストラクタで左辺値は受け取れない
  • 戻り値を無視してもワーニングが出ない

という欠点がある. 左辺値が受け取れないため,例えば次のコードがエラーになる

int
main()
{
  auto body = [](auto f, int n) -> int {
    return n < 2 ? n : (f(n - 1) + f(n - 2));
  };
  auto result = makeFixPoint(body)(10);
  std::cout << result << std::endl;
}

ちゃんと std::decayかますことによって,ムーブすべきときにムーブし,コピーすべきときにコピーできるようにしておくべきである.

C++17以降では constexpr なラムダでコンパイル時計算可能であるが,C++14でも constexpr な関数オブジェクトを用意すれば一応コンパイル時計算は可能である. ラムダではないので,本記事の趣旨とは外れるが....

#include <iostream>
#include <utility>


template <typename F>
class
#if defined(__has_cpp_attribute) && __has_cpp_attribute(nodiscard)
[[nodiscard]]
#endif  // defined(__has_cpp_attribute) && __has_cpp_attribute(nodiscard)
FixPoint final : private F
{
public:
  template <typename G>
  explicit constexpr FixPoint(G&& g) noexcept
    : F{std::forward<G>(g)}
  {}

  template <typename... Args>
  constexpr decltype(auto)
  operator()(Args&&... args) const
#if !defined(__GNUC__) || defined(__clang__) || __GNUC__ >= 9
    noexcept(noexcept(F::operator()(std::declval<FixPoint>(), std::declval<Args>()...)))
#endif  // !defined(__GNUC__) || defined(__clang__) || __GNUC__ >= 9
  {
    return F::operator()(*this, std::forward<Args>(args)...);
  }
};  // class FixPoint


#if defined(__cpp_deduction_guides)
template <typename F>
FixPoint(F&&)
  -> FixPoint<std::decay_t<F>>;
#endif  // defined(__cpp_deduction_guides)


namespace
{
template <typename F>
#if !defined(__has_cpp_attribute) || !__has_cpp_attribute(nodiscard)
#  if defined(__GNUC__) && (__GNUC__ > 3 || __GNUC__ == 3 && __GNUC_MINOR__ >= 4)
__attribute__((warn_unused_result))
#  elif defined(_MSC_VER) && _MSC_VER >= 1700 && defined(_Check_return_)
_Check_return_
#  endif  // defined(__GNUC__) && (__GNUC__ > 3 || __GNUC__ == 3 && __GNUC_MINOR__ >= 4)
#endif  // !defined(__has_cpp_attribute) || !__has_cpp_attribute(nodiscard)
inline constexpr decltype(auto)
makeFixPoint(F&& f) noexcept
{
  return FixPoint<std::decay_t<F>>{std::forward<std::decay_t<F>>(f)};
}
}  // namespace


class Fib
{
public:
  template <typename F>
  constexpr int
  operator()(F&& f, int n) const noexcept
  {
    return n < 2 ? n : (f(n - 1) + f(n - 2));
  }
};  // class Fib


int
main()
{
  constexpr auto result = makeFixPoint(Fib{})(10);
  std::cout << result << std::endl;
}

生成コード

C++17以降ではクラステンプレートのテンプレートパラメータ推論が可能になったため,推論のためのmake関数も不要になる.

#include <iostream>
#include <utility>


template <typename F>
class
#if defined(__has_cpp_attribute) && __has_cpp_attribute(nodiscard)
[[nodiscard]]
#endif  // defined(__has_cpp_attribute) && __has_cpp_attribute(nodiscard)
FixPoint final : private F
{
public:
  template <typename G>
  explicit constexpr FixPoint(G&& g) noexcept
    : F{std::forward<G>(g)}
  {}

  template <typename... Args>
  constexpr decltype(auto)
  operator()(Args&&... args) const
#if !defined(__GNUC__) || defined(__clang__) || __GNUC__ >= 9
    noexcept(noexcept(F::operator()(std::declval<FixPoint>(), std::declval<Args>()...)))
#endif  // !defined(__GNUC__) || defined(__clang__) || __GNUC__ >= 9
  {
    return F::operator()(*this, std::forward<Args>(args)...);
  }
};  // class FixPoint


#if defined(__cpp_deduction_guides)
template <typename F>
FixPoint(F&&)
  -> FixPoint<std::decay_t<F>>;
#endif  // defined(__cpp_deduction_guides)


int
main()
{
  auto result = FixPoint{[](auto f, int n) -> int {
    return n < 2 ? n : (f(n - 1) + f(n - 2));
  }}(10);
  std::cout << result << std::endl;
}

おまけの話題

FixPoint の実装

3つ目の手法ではラムダを継承して FixPoint を実装したが,別にメンバーにラムダを持つ実装でもよい. 僕個人としては,ラムダを継承する方がスッキリしていると感じたのと,次の章のオーバーロード実装を考えると継承の方を紹介するのが自然と考えた.

実は先に紹介した継承を用いる方法はいなむ神に教えていただいた

#include <iostream>
#include <utility>


template <typename F>
class
#if defined(__has_cpp_attribute) && __has_cpp_attribute(nodiscard)
[[nodiscard]]
#endif  // defined(__has_cpp_attribute) && __has_cpp_attribute(nodiscard)
FixPoint
{
private:
  const F m_f;

public:
  template <typename G>
  explicit constexpr FixPoint(G&& g) noexcept
    : m_f{std::forward<G>(g)}
  {}

  template <typename... Args>
  constexpr decltype(auto)
  operator()(Args&&... args) const
#if !defined(__GNUC__) || defined(__clang__) || __GNUC__ >= 9
    noexcept(noexcept(m_f(std::declval<FixPoint>(), std::declval<Args>()...)))
#endif  // !defined(__GNUC__) || defined(__clang__) || __GNUC__ >= 9
  {
    return m_f(*this, std::forward<Args>(args)...);
  }
};  // class FixPoint


#if defined(__cpp_deduction_guides)
template <typename F>
FixPoint(F&&)
  -> FixPoint<std::decay_t<F>>;
#endif  // defined(__cpp_deduction_guides)


int
main()
{
  auto result = FixPoint{[](auto f, int n) -> int {
    return n < 2 ? n : (f(n - 1) + f(n - 2));
  }}(10);
  std::cout << result << std::endl;
}

再帰ラムダのオーバーロード

これもいなむ神に教えていただいたものである. C++17のusing宣言のパック展開を用いると,オーバーロードが可能となる.

#include <iostream>
#include <utility>


template <typename... Fs>
class
#if defined(__has_cpp_attribute) && __has_cpp_attribute(nodiscard)
[[nodiscard]]
#endif  // defined(__has_cpp_attribute) && __has_cpp_attribute(nodiscard)
FixPoint final : private Fs...
{
  using Fs::operator()...;

public:
  template <
    typename... Gs,
    std::enable_if_t<sizeof...(Fs) == sizeof...(Gs), std::nullptr_t> = nullptr
  >
  explicit constexpr FixPoint(Gs&&... gs) noexcept
    : Fs{std::forward<Gs>(gs)}...
  {}

  template <typename... Args>
  constexpr decltype(auto)
  operator()(Args&&... args) const
#if !defined(__GNUC__) || defined(__clang__) || __GNUC__ >= 9
    noexcept(noexcept(operator()(std::declval<FixPoint>(), std::declval<Args>()...)))
#endif  // !defined(__GNUC__) || defined(__clang__) || __GNUC__ >= 9
  {
    return operator()(*this, std::forward<Args>(args)...);
  }
};  // class FixPoint


#if defined(__cpp_deduction_guides)
template <
  typename... Fs,
  std::enable_if_t<sizeof...(Fs) != 0, std::nullptr_t> = nullptr
>
FixPoint(Fs&&...)
  -> FixPoint<std::decay_t<Fs>...>;
#endif  // defined(__cpp_deduction_guides)


int
main()
{
  auto fib = FixPoint{
    [](auto f, int n) -> int {
      return n < 2 ? n : (f(n - 1) + f(n - 2));
    },
    [](auto f, double n) -> double {
      return n < 2 ? n : (f(n - 1) + f(n - 2));
    }};
  std::cout << fib(10) << std::endl;
  std::cout << fib(10.0) << std::endl;
}

上記の例では double 型のオーバーロードを用意しているが,単にオーバーロード可能ということを示すためのものにすぎない.

なお,Deducation Guides,およびコンストラクタにおいてSFINAEしているのは,g++の場合,これが無ければコンパイルエラーになるためである. clangおよびMSVCでは無くてもコンパイルが通るのだが....

Boost.Hanaによる実装(追記(2020/05/23))

実はBoostにも FixPoint クラスと同様のものの実装がある.

#include <iostream>
#include <boost/hana/functional/fix.hpp>


int
main()
{
  auto result = boost::hana::fix([](auto f, int n) -> int {
    return n < 2 ? n : (f(n - 1) + f(n - 2));
  })(10);
  std::cout << result << std::endl;
}

生成コード

実装としては以下の形となっている.

#include <iostream>
#include <type_traits>
#include <utility>


namespace detail
{

template <template <typename ...> class T>
struct create
{
  template <typename ...X>
  constexpr T<typename std::decay<X>::type...>
  operator()(X&& ...x) const {
    return T<typename std::decay<X>::type...>{
      static_cast<X&&>(x)...
    };
  }
};  // struct create

}  // namespace detail


template <typename F>
struct fix_t;

constexpr detail::create<fix_t> fix{};

template <typename F>
struct fix_t
{
  F f;

  template <typename ...X>
  constexpr decltype(auto) operator()(X&& ...x) const&
  { return f(fix(f), static_cast<X&&>(x)...); }

  template <typename ...X>
  constexpr decltype(auto) operator()(X&& ...x) &
  { return f(fix(f), static_cast<X&&>(x)...); }

  template <typename ...X>
  constexpr decltype(auto) operator()(X&& ...x) &&
  { return std::move(f)(fix(f), static_cast<X&&>(x)...); }
};


int
main()
{
  auto result = fix([](auto f, int n) -> int {
    return n < 2 ? n : (f(n - 1) + f(n - 2));
  })(10);
  std::cout << result << std::endl;
}

生成コード

追記 (2018/06/10)

いなむ神にラムダのみで再帰する方法を提示していただいた. ラムダのみでYコンビネータとZコンビネータを実現しており,非常に面白い. この記事で散々取り扱っているフィボナッチ数列の関数を,上記記事のYコンビネータとZコンビネータを用いて実装すると,それぞれ以下のようになる.

ラムダオンリーのYコンビネータによるフィボナッチ数列の関数の実装

#include <iostream>
#include <utility>


int
main()
{
  auto result = [g=[](auto f, int n) -> int {
    return n < 2 ? n : (f(f, n - 1) + f(f, n - 2));
  }](auto&&... args) {
    return g(g, std::forward<decltype(args)>(args)...);
  }(10);
  std::cout << result << std::endl;
}

生成コード

なお,MSVCでコンパイルしたとき,上記の実装だとMSVCがポンコツなためICEとなってコンパイルが通らないので,以下の実装を用いる方が安全かもしれない. MSVCでは初期化キャプチャでラムダをキャプチャすると,ICEとなるようだ.

#include <iostream>
#include <utility>


int
main()
{
  auto ret = [](auto f) {
    return [=](auto&&... args) {
      return f(f, std::forward<decltype(args)>(args)...);
    };
  }([](auto f, int n) -> int {
    return n < 2 ? n : f(f, n - 1) + f(f, n - 2);
  })(10);
  std::cout << ret << std::endl;
}

f:id:koturn:20180621043652p:plain

ラムダオンリーのZコンビネータによるフィボナッチ数列の関数の実装

#include <iostream>
#include <utility>


int
main()
{
  auto result = [](auto f) {
    return [=](auto g) {
      return [=](auto&&... args) {
        return f(g(g), std::forward<decltype(args)>(args)...);
      };
    }([=](auto g) {
      return [=](auto&&... args) {
        return f(g(g), std::forward<decltype(args)>(args)...);
      };
    });
  }([](auto f, int n) -> int {
    return n < 2 ? n : (f(n - 1) + f(n - 2));
  })(10);
  std::cout << result << std::endl;
}

生成コード

C++に不慣れな人にとっては,上記記事のZコンビネータほど複雑であれば実行効率が気になるところかもしれない(ラムダを返すラムダが云々で何となく遅そうなイメージ)が,生成コードを見れば要らぬ心配であることがわかると思う. そして,上記のYコンビネータ,Zコンビネータから生成されるコードは記事の本編で紹介した関数やクラスを用いる方法と同一のコードが生成されていることもわかる. C++はよしなにラムダのインライン展開を行ってくれるのだ!

僕はいなむ神のYコンビネータとZコンビネータの実装に感銘を受け,VimプラグインであるShougo/neosnippet.vimスニペットファイルに下記のスニペットを追加した.

snippet ycombinator
alias ycomb
  [](auto f) {
    return [=](auto&&... args) {
      return f(f, std::forward<decltype(args)>(args)...);
    };
  }([${1:&}](auto ${2:f}, ${3:#:args...}) {
    ${0}
  })

snippet zcombinator
alias zcomb
  [](auto f) {
    return [=](auto g) {
      return [=](auto&&... args) {
        return f(g(g), std::forward<decltype(args)>(args)...);
      };
    }([=](auto g) {
      return [=](auto&&... args) {
        return f(g(g), std::forward<decltype(args)>(args)...);
      };
    });
  }([${1:&}](auto ${2:f}, ${3:#:args...}) {
    ${0};
  })

これでいつでも再帰するラムダを楽に書ける. なお,ラムダを受ける引数はユニヴァーサル参照等の参照系で受けると単にautoで受けるよりも数命令多く,消費スタック量もわずかに多いので,いなむ神の記事の通り,autoで受けたり,コピーキャプチャを行う方が良い.

※Zコンビネータに関して:g++とMSVCではコンパイルは通るが,clang++ではコンパイルが通らないようなので気を付けてほしい.

速度比較

実際に実行時間を計測してみることにしよう. 再帰に関して,C++では様々な方法で実現することが可能であるが,この記事では,以下の4つのような関数オブジェクトによる再帰手法も紹介している. これらも含めて,実行時間を計測することにする.

class Fibonacci01
{
public:
  constexpr int
  operator()(int n) const noexcept
  {
    return n < 2 ? n : (Fibonacci01{}(n - 1) + Fibonacci01{}(n - 2));
  }
};  // struct Fibonacci01


class Fibonacci02
{
public:
  constexpr int
  operator()(int n) const noexcept
  {
    return n < 2 ? n : ((*this)(n - 1) + (*this)(n - 2));
  }
};  // struct Fibonacci02


class Fibonacci03
{
public:
  constexpr int
  operator()(int n) const noexcept
  {
    return n < 2 ? n : (operator()(n - 1) + operator()(n - 2));
  }
};  // struct Fibonacci03


class Fibonacci04
{
public:
  constexpr int
  operator()(Fibonacci04 f, int n) const noexcept
  {
    return n < 2 ? n : (f(f, n - 1) + f(f, n - 2));
  }
};  // struct Fibonacci04

この内 Fibonacci01 は本記事で紹介した FixPoint クラスによる手法と生成コードは同一であるため除外する. また Fibonacci02Fibonacci03 の生成コードは同一であり,比較する意味はないため, Fibonacci02 は除外する. 他にも,本記事において同一のコードが生成されたもの同士は意味が無いので,ユニークになるように除外して計測する.

計測に際して,以下のコードを用意した.

記事中のコードではフィボナッチ数の型を一律 int としていたが,計測コードでは std::uint64_t とした. x64環境であれば,64bitレジスタに乗せるだけなので,32bitから64bitに変更しても大きな影響はない. Wandboxは実行時間制限があるため,fib(42) をそれぞれ1回計算するコードにしてあるが,手元では fib(45) をそれぞれ5回計算するのにかかった平均時間を計測した.

使用したコンパイラは g++ 7.3.0 であり,コンパイルオプションは下記の通りである.

$ g++ -O2 -march=native main.cpp -o main.out
$ ./main.out

計測結果は以下の表に示す通りである.

手法 実行時間
普通の関数 2832.08 ms
std::function<> 7481.06 ms
fix() 省略
FixPoint クラス 2475.44 ms
FixPoint クラス (参照受け) 2715.21 ms
Yコンビネータ 省略
Zコンビネータ 省略
Fibonacci01 省略
Fibonacci02 省略
Fibonacci03 2903.60 ms
Fibonacci04 省略

つまり,

FixPoint クラス(追記のZコンビネータも含む) < FixPoint クラス(本体のラムダにおいて参照で受ける) < 普通の関数 < Fibonacci03 < std::function

という結果になった.

表の結果からも分かるように std::function は特に遅い. 普通の関数で実装したよりも3倍の実行時間を必要とする. そして,本記事で紹介したようなラムダで再帰を行う手法は,通常の関数で再帰を行う手法よりも高速である.

ただし,ラムダや関数オブジェクトを引数で受けるときに参照で受けた場合,わずかに遅くなる. 実際,参照で受ける場合のコードを確認してみると,余分なlea命令が生成されていた. 関数オブジェクトによる再帰の記事にもあるように,ラムダや関数オブジェクトはコピーで受け渡す方,あるいは都度生成する方がよいのだろう.

ただし,これはフィボナッチ数列の計算という外部の変数のキャプチャを必要としない例だからであり,値渡しの引数で受け取ると通常はラムダ式のコピーが発生する. これが問題になるのは,キャプチャを行っているときである. ラムダ式のキャプチャは,関数オブジェクトのメンバと同等なものであるので,ラムダ式のコピーが1回行われると,そのラムダ式がキャプチャした変数全てのコピーも行われる. intdouble といったプリミティブな型の変数1つのコピーキャプチャや,任意の型の変数の参照キャプチャ1つであれば,コピーコストとしてはほぼ問題が無いと思われるが,それ以外の場合(std::vector 等のコピーに際してメモリアロケーションが関係するオブジェクトのコピーキャプチャや参照キャプチャであっても多数のキャプチャを行っている場合)は問題になる. 基本的には auto&&const auto& 等を使って,参照で受ける方がよいと思う.

追記 (2019/03/18,2020/05/23)

速度比較がg++だけであったので,MSVCおよびclang++でも速度比較を行った. また,MSVCやclang++では再帰関数の呼出自体が省略されてしまうことがあったので,前述の計測コードに少し手を加え,以下のようにした.

計測コードはkoturn/CppRecursiveLambdaにも同一のものを置いている. 手元で試してみたい方は上記のリポジトリを利用するとよいだろう.

前と同じく fib(45) をそれぞれ5回計算するのにかかった平均時間を計測し,結果は以下の表のようになった. g++では同一コードが生成されていたものもコンパイラが違えば別のコードを生成していたので,全ての再帰実装の結果を示す.

手法 g++ clang++ MSVC
普通の関数 3042.32 ms 5011.04 ms 4427.97 ms
std::function<> 5965.21 ms 5869.21 ms 6658.59 ms
fix() 2474.56 ms 4900.02 ms 5001.03 ms
FixPoint クラス 2471.09 ms 4899.59 ms 4970.68 ms
FixPoint クラス (参照受け) 2661.03 ms 4785.61 ms 4923.05 ms
Boost.Hanaの fix 実装 2469.58 ms 4902.19 ms 4737.15 ms
Boost.Hanaの fix 実装(参照受け) 2711.37 ms 4790.04 ms 4960.24 ms
Yコンビネータ 2387.47 ms 5271.59 ms 4997.06 ms
Zコンビネータ 2468.44 ms ※1 15567.03 ms
Fibonacci01 2382.15 ms 4461.81 ms 4230.46 ms
Fibonacci02 3496.07 ms 4784.06 ms 4511.31 ms
Fibonacci03 3491.11 ms 4784.08 ms 4510.35 ms
Fibonacci04 2376.32 ms 4669.02 ms 4983.88 ms

※1:コンパイルできないため,記録無し

g++とclang++はおおむね同じような結果を示しているが,MSVCは全く異なった結果を示している. 特にZコンビネータのパフォーマンスが std::function<> よりも悪いことに驚かされる. MSVCの場合,fix() や Yコンビネータが最も高速であり,次に普通の関数および Fibonacci01が続き,次に FixPoint クラスが続くようだ. このことから,MSVCはあまりラムダのインライン展開等が得意ではないのではないかと思われる.

std::function による再帰がどのコンパイラでも遅いのは予想通りであった.

また,clang++がg++の2倍に近い実行時間となっているのは,単に1回の再帰につき2回フィボナッチ関数を呼び出す部分の最適化ができていないためであるが,clang++の結果のみでいえばどの再帰スタイルでも同じ最適化になっているので,今回の再帰の書き方によるパフォーマンスの違いに関する結果とは別のものである.

MSVCを無視するなら,個人的には一番使い勝手のよい FixPoint クラスを推したいと思う.

まとめ

  • C++11でない限り,ラムダの再帰std::function を用いるべきでない(約3倍の実行時間になる)
  • ジェネリックラムダと operator() を用いた実装を使うべき

いくつかの実装を紹介したが,C++14のジェネリックラムダによる再帰はいずれも同一のコード生成がなされ,また無駄な処理がなく,普通の関数による再帰よりも高速に動作することがわかった.

競プロの人向け

AtCoderの提出コードで,このブログに書いているような FixPoint の実装を見かけることがあった. 実際は記事中ほど丁寧に実装する必要はなく,競プロのテンプレートとしては以下のコードでよい.

ヘルパ関数の makeFixPoint という名前は長すぎるので,fix にする.

競プロのコードごときに,わざわざ丁寧に単一引数のコンストラクタに explicit を付けて暗黙の変換を避ける必要はない. これにより,fix 関数に暗黙の型変換を利用することができるようになり,文字数を多少削ることが可能になる.

FixPoint クラスでは public: を書くのが面倒なので,struct にして省略するとよい. また,noexcept が無いからといって,単純なコードでは最適化に影響はないはずだし,constexpr が無ければコンパイル時計算はできなくなるが,競プロで再帰関数のコンパイル時計算が必要な場面も無いと思われる.

#include <utility>


template <typename F>
struct FixPoint : F
{
  template <typename G>
  FixPoint(G&& g)
    : F{std::forward<G>(g)}
  {}

  template <typename... Args>
  decltype(auto)
  operator()(Args&&... args) const
  {
    return F::operator()(*this, std::forward<Args>(args)...);
  }
};


#if defined(__cpp_deduction_guides)
template <typename F>
FixPoint(F&&)
  -> FixPoint<std::decay_t<F>>;
#endif  // defined(__cpp_deduction_guides)


// C++17以降でクラステンンプレートの型推論を使うなら以下は不要
template <typename F>
inline FixPoint<std::decay_t<F>>
fix(F&& f)
{
  return std::forward<std::decay_t<F>>(f);
}


#include <iostream>


int
main()
{
  auto result = fix([](auto&& f, int n) -> int {
    return n < 2 ? n : (f(n - 1) + f(n - 2));
  })(10);
  std::cout << result << std::endl;
}

using namespace std 派の人は適宜 std:: を除去してもよい. C++14の範囲でしか書かない,すなわちC++17のクラステンプレートのテンプレート引数推論を使わない場合,推論補助やコンストラクタにテンプレートは必要ないので,以下のようにするとよい.

#include <utility>


template <typename F>
struct FixPoint : F
{
  FixPoint(F&& f)
    : F{std::forward<F>(f)}
  {}

  template <typename... Args>
  decltype(auto)
  operator()(Args&&... args) const
  {
    return F::operator()(*this, std::forward<Args>(args)...);
  }
};


template <typename F>
inline FixPoint<std::decay_t<F>>
fix(F&& f)
{
  return std::forward<std::decay_t<F>>(f);
}


#include <iostream>


int
main()
{
  auto result = fix([](auto&& f, int n) -> int {
    return n < 2 ? n : (f(n - 1) + f(n - 2));
  })(10);
  std::cout << result << std::endl;
}

記事中でも述べているようにBoost.Hanaに FixPoint と同等の実装があるので,そもそもAtCoderのようなBoostが使える環境では FixPoint クラスの実装すら必要ない. (ローカルでBoostを用意するのが面倒な人もいるかもしれないが)

#include <boost/hana/functional/fix.hpp>
using boost::hana::fix;


#include <iostream>


int
main()
{
  auto result = fix([](auto&& f, int n) -> int {
    return n < 2 ? n : (f(n - 1) + f(n - 2));
  })(10);
  std::cout << result << std::endl;
}

参考文献

タブ番号,バッファ番号,ウィンドウ番号,ウィンドウIDの相互変換

はじめに

先日,「Vimで既に対象バッファを開いているウィンドウがあるとき,そのウィンドウに移動する」「Vimのタブ番号,ウィンドウ番号,ウィンドウID,バッファ番号の一覧情報を表示する」の2記事で,タブ番号,バッファ番号,ウィンドウ番号,ウィンドウIDを取り扱った. そして,Vim scriptの組み込み関数で,これらの相互変換を行った. しかし,単純に変換する関数が用意されているわけではなく,そこそこ工夫が必要だったので,この忘備録として,この記事にまとめようと思う.

Vimのタブ番号,ウィンドウ番号,ウィンドウID,バッファ番号について

Vimのタブ番号,ウィンドウ番号,ウィンドウID,バッファ番号について,簡単にまとめる.

種類 説明
タブ番号 :tabnew 等でオープンされたタブに振られる番号.左から1, 2, 3, ...と振られ,タブ消去,タブ移動を行うと振り直される
ウィンドウ番号 1つのタブにおけるウィンドウの番号.ウィンドウの移動,消去で振り直される
ウィンドウID タブに関係無く,1つのウィンドウに一意に割り当てられる番号.
バッファ番号 バッファに関連付けられた番号.バッファを削除しても振り直しは行われないため,IDとして扱うことも可能.

ウィンドウIDについては,Vim8.0からの機能であり,thincaさんのVim 8.0 Advent Calendar 7 日目 ウィンドウ IDという記事で解説されている.

相互変換表

本題の相互変換について,簡単に表にまとめた. 変換先が一意ではなく,複数あるものは「(複数)」と表記してある.

個別の詳細は,次の章で紹介する.

変換元 変換先 手法 制限
bufnr winnr(複数) buf_winnr() / win_findbuf()win_id2tabwin() から逆引き辞書作成 前者は同一タブ最初のウィンドウのみ
bufnr winid(複数) bufwinid() / win_findbuf() 前者は同一タブ最初のウィンドウのみ
bufnr tabnr(複数) tabpagebuflist() から逆引き辞書作成
winnr bufnr winbufnr() / win_getid()win_findbuf() 前者は同一タブのみ
winnr winid win_getid()
winnr tabnr 無意味であるため省略
winid bufnr win_findbuf() から逆引き辞書を作成
winid winnr win_id2tabwin()
winid tabnr win_id2win() / win_id2tabwin() 前者は同一タブのみ
tabnr bufnr(複数) tabpagebuflist()
tabnr winnr(複数) tabpagewinnr()
tabnr winid(複数) tabpagewinnr()win_getid()

個々の変換手法

bufnr から winnr (複数)

指定したバッファ番号のバッファを開いているウィンドウの番号を得る.

buf_winnr() だと同一タブ内における若い番号のウィンドウ番号を1つしか取得することができない. 複数個取得しようと思うと, win_findbuf()win_id2tabwin() で逆引き辞書を作成しておく必要がある.

まず,ウィンドウ番号は個々のタブで1から始まるので,インプットとしてはバッファ番号の他にタブ番号も受け取るものとする. (タブ番号は省略可能で,省略時はカレントタブの番号を受け取ったものとする)

function! s:create_bufnr2tabwin_dict() abort " {{{
  let bufnr2tabwin_dict = {}
  for bnr in filter(range(1, bufnr('$')), 'bufexists(v:val)')
    let bufnr2tabwin_dict[bnr] = map(win_findbuf(bnr), 'win_id2tabwin(v:val)')
  endfor
  return bufnr2tabwin_dict
endfunction " }}}

function! s:bufnr2winnr(bnr, ...) abort " {{{
  if bufexists(a:bnr)
    throw 'E86 Buffer ' . a:bnr . 'does not exist'
  endif
  let tnr = a:0 > 0 ? a:1 : tabpagenr()
  return map(filter(s:create_bufnr2tabwin_dict()[a:bnr], 'v:val[0] == tnr'), 'v:val[1]')
endfunction " }}}

bufnr から winid (複数)

指定したバッファ番号のバッファを開いているウィンドウのIDを得る.

win_findbuf() を呼び出すだけでよい.

function! s:bufnr2winid(bnr) abort " {{{
  return win_findbuf(a:bnr)
endfunction " }}}

bufnr から tabnr (複数)

指定したバッファ番号のバッファを開いているウィンドウが存在するタブページの番号を得る.

tabpagebuflist() でtabnrからbufnrのリストが得られるので,逆引き辞書を作成する.

function! s:create_bufnr2tabnr_dict() abort " {{{
  let bufnr2tabnr_dict = {}
  for tnr in range(1, tabpagenr('$'))
    for bnr in tabpagebuflist(tnr)
      let bufnr2tabnr_dict[bnr] = has_key(bufnr2tabnr_dict, bnr) ? add(bufnr2tabnr_dict[bnr], tnr) : [tnr]
    endfor
  endfor
  for val in values(bufnr2tabnr_dict)
    call uniq(sort(val))
  endfor
  return bufnr2tabnr_dict
endfunction " }}}

function! s:bufnr2tabnr(bnr) abort " {{{
  return s:create_bufnr2tabnr_dict()[a:bnr]
function " }}}

bufnr から winnrの事例のように,win_findbuf()win_id2tabwin() で逆引き辞書を作ってもよい.

function! s:create_bufnr2tabwin_dict() abort " {{{
  let bufnr2tabwin_dict = {}
  for bnr in filter(range(1, bufnr('$')), 'bufexists(v:val)')
    let bufnr2tabwin_dict[bnr] = map(win_findbuf(bnr), 'win_id2tabwin(v:val)')
  endfor
  return bufnr2tabwin_dict
endfunction " }}}

function! s:bufnr2winnr02(bnr) abort " {{{
  if bufexists(a:bnr)
    throw 'E86 Buffer ' . a:bnr . 'does not exist'
  endif
  return map(s:create_bufnr2tabwin_dict()[a:bnr], 'v:val[0]')
endfunction " }}}

winnr から bufnr

指定したウィンドウ番号のウィンドウが開いているバッファの番号を得る.

まず,ウィンドウ番号からウィンドウIDに変換し,後述するウィンドウIDからバッファ番号の辞書を作成する手法を利用する.

function! s:create_winid2bufnr_dict() abort " {{{
  let winid2bufnr_dict = {}
  for bnr in range(1, bufnr('$'))
    for wid in win_findbuf(bnr)
      let winid2bufnr_dict[wid] = bnr
    endfor
  endfor
  return winid2bufnr_dict
endfunction " }}}

function! s:winid2tabnr(wid, ...) abort " {{{
  return a:0 > 0 ? s:create_winid2bufnr_dict()[win_getid(a:wid, tnr)] : winbufnr(a:wid)
endfunction " }}}

winnr から winid

指定したウィンドウ番号のウィンドウIDを得る.

当然,タブを指定する必要はあるよねという話. win_getid() を呼び出すだけ(タブ番号を省略すると,カレントタブのウィンドウ番号からウィンドウIDを引くことになる). 関数を呼び出すだけなので,自作関数を定義する必要はないが,とりあえず作っておく.

function! s:winnr2winid(wnr, ...) abort " {{{
  return a:0 > 0 ? win_getid(a:wnr, a:1) : win_getid(a:wnr)
endfunction " }}}

winid から tabnr

例えば,「番号2のウィンドウ」を持つタブのリストを作成することはできるが意味が無いので割愛. それは,2個以上のウィンドウに分割されているタブの検索にしかならない.

winid から bufnr

指定したウィンドウIDのウィンドウで開かれているバッファの番号を得る.

直接取得できる関数は無いため, winid2bufnr_dict() を利用して,winidからbufnrの逆引き辞書を作成する. (ループして一致したらreturnというスタンスでもよいが,他への転用がしんどいので辞書を作成する)

function! s:create_winid2bufnr_dict() abort " {{{
  let winid2bufnr_dict = {}
  for bnr in range(1, bufnr('$'))
    for wid in win_findbuf(bnr)
      let winid2bufnr_dict[wid] = bnr
    endfor
  endfor
  return winid2bufnr_dict
endfunction " }}}

function! s:winid2bufnr(wid) abort " {{{
  return s:create_winid2bufnr_dict()[a:wid]
endfunction " }}}

winid から winnr

指定したウィンドウIDのウィンドウの番号を得る.

win_id2tabwin() を呼び出すだけでよい.

function! s:winid2tabnr(wid) abort " {{{
  return win_id2tabwin(a:wid)[1]
endfunction " }}}

winid から tabnr

指定したウィンドウIDのウィンドウを持つタブページの番号を得る.

win_id2win() は同一タブ内のウィンドウしか検索対象にしないため,1つ前と同じく win_id2tabwin() を利用する. そもそも,ウィンドウ番号はタブと共に無いとあまり意味がないので, win_id2tabwin() の結果を個々に取得するのは微妙だろう.

function! s:winid2tabnr(wid) abort " {{{
  return win_id2tabwin(a:wid)[0]
endfunction " }}}

tabnr から bufnr (複数)

指定したタブページ番号ののバッファ番号のリストを得る.

単に関数を呼び出すだけでよい.

function! s:tabnr2bufnr_list(tnr) abort " {{{
  return tabpagebuflist(a:tnr)
endfunction " }}}

tabnr から winnr (複数)

指定したタブページ番号のウィンドウ番号のリストを得る.

ウィンドウIDはウィンドウ削除時に振り直されるため,必ず1, 2, 3, ...という風に整数が連続するので,最後のウィンドウ番号を得るとよい.

function! s:tabnr2winid_list(tnr) abort " {{{
  return range(1, tabpagewinnr(a:tnr, '$'))
endfunction " }}}

tabnr から winid (複数)

指定したタブページ番号のウィンドウIDのリストを得る.

function! s:tabnr2winid_list(tnr) abort " {{{
  return map(range(1, tabpagewinnr(a:tnr, '$')), 'win_getid(v:val, a:tnr)')
endfunction " }}}

まとめ

個人的なメモとして,タブ番号,バッファ番号,ウィンドウ番号,ウィンドウIDの相互変換についてまとめた. 今後,.vimrcに書く設定やプラグインの作成に活かすことがあるかもしれない.

Vimのタブ番号,ウィンドウ番号,ウィンドウID,バッファ番号の一覧情報を表示する

はじめに

先日の記事Vimで既に対象バッファを開いているウィンドウがあるとき,そのウィンドウに移動するでは,「タブ開きすぎると,どのタブでどのバッファ開いているかわからなくなるから,同じバッファを複数のウィンドウで開いてしまう」というIQ1的な事柄を問題にし,それを解決するコマンドを提示した. しかし,そもそも全てのタブ配下の全てのウィンドウが,どのバッファに関連付けられているかの情報をパパッと見ることができれば,そういう問題も少しは減るはずだ. この記事では,全てのタブが管理している情報を表示するコマンドを提案する.

実装

何はともあれ実装である.このような実装を考えた. tabpagewinnr(タブ番号, '$') で対象タブにおけるウィンドウの個数が得られるので,あとは個数分イテレーションを行い, win_getid(ウィンドウ番号, タブ番号) とすることで,ウィンドウIDが得られる寸法だ. Window IDは一意であるため,そこからバッファ番号を得るとよい. ただし,Window IDからバッファ番号を得る関数は無く,その逆の win_findbuf() という関数があるため,逆引き辞書を作る.

" Window IDからバッファ番号を引く逆引き辞書を作成
function! s:create_winid2bufnr_dict() abort " {{{
  let winid2bufnr_dict = {}
  for bnr in filter(range(1, bufnr('$')), 'v:val')
    for wid in win_findbuf(bnr)
      let winid2bufnr_dict[wid] = bnr
    endfor
  endfor
  return winid2bufnr_dict
endfunction " }}}

function! s:show_tab_info() abort " {{{
  echo "====== Tab Page Info ======"
  let current_tnr = tabpagenr()
  let winid2bufnr_dict = s:create_winid2bufnr_dict()
  for tnr in range(1, tabpagenr('$'))
    let current_winnr = tabpagewinnr(tnr)
    echo (tnr == current_tnr ? '>' : ' ') 'Tab:' tnr
    echo '    Buffer number | Window Number | Window ID | Buffer Name'
    for wininfo in map(map(range(1, tabpagewinnr(tnr, '$')), '{"wnr": v:val, "wid": win_getid(v:val, tnr)}'), 'extend(v:val, {"bnr": winid2bufnr_dict[v:val.wid]})')
      echo '   ' (wininfo.wnr == current_winnr ? '*' : ' ') printf('%11d | %13d | %9d | %s', wininfo.bnr, wininfo.wnr, wininfo.wid, bufname(wininfo.bnr))
    endfor
  endfor
endfunction " }}}
command! -bar TabInfo call s:show_tab_info()

表示イメージとしては以下の通り. カレントタブの左に > を表示し,それぞれのタブにおけるカレントウィンドウの左には * を表示している.

f:id:koturn:20180212141845p:plain

カレントタブに関する情報欄に :echohl で色を付けるなどすると,見やすさが向上するかもしれない.

NG実装

これはNG実装なので,真似してはいけない. 問題点としては,各タブの情報収集において,対象タブへの移動を伴っている点であり,行数の多いバッファと関連付いたウィンドウをカレントウィンドウに持つタブに移動したときに,数秒固まってしまう.

function! s:show_tab_info() abort " {{{
  echo "====== Tab Page Info ======"
  let current_tnr = tabpagenr()
  for tnr in range(1, tabpagenr('$'))
    execute 'tabnext' tnr
    echo (tnr == current_tnr ? '>' : ' ') 'Tab:' tnr
    echo '    Buffer number | Window Number | Window ID | Buffer Name'
    let current_winnr = winnr()
    for wnr in range(1, winnr('$'))
      echo '   ' (wnr == current_winnr ? '*' : ' ') printf('%11d | %13d | %9d | %s', winbufnr(wnr), wnr, win_getid(wnr), bufname(winbufnr(wnr)))
    endfor
  endfor
  execute 'tabnext' current_tnr
endfunction " }}}
command! -bar TabInfo call s:show_tab_info()

まとめ

全てのタブが管理している全てのウィンドウの番号とID,およびそれらのウィンドウに関連付けられているバッファの番号とバッファ名を表示するコマンドを提案した. それらの情報を見ることができれば,無駄に同じバッファを複数のウィンドウ(特に異なるタブ間において)開き直すことは無くなるだろう.

Vimで既に対象バッファを開いているウィンドウがあるとき,そのウィンドウに移動する

はじめに

僕はVimのタブ機能をそこそこ活用する方である. だが,「このバッファは既に開いている」ということを忘れることが多々あり,複数のタブで同じバッファを開くことがある.

Vim8.0になり, win_gotoid() というVim scriptの関数が追加された. これは,指定したIDのウィンドウにフォーマスを当てるという関数であり,まさに「Vimで既に対象バッファを開いているウィンドウがあるとき,そのウィンドウに移動する」という機能に持ってこいである.

そこで,そういった機能を実現するコマンドを考えてみた.

実装

何はともあれ,実装は以下のようになる. win_gotoid() はVim8.0からの機能であるため,Vim7.3, Vim7.4向けの実装も用意しておく.

if exists('*win_gotoid')
  function! s:buf_open_existing(qmods, bname) abort " {{{
    let bnr = bufnr(a:bname)
    if bnr == -1
      echoerr 'Buffer not found:' a:bname
      return
    endif
    let wids = win_findbuf(bnr)
    if empty(wids)
      execute a:qmods 'new'
      execute 'buffer' bnr
    else
      call win_gotoid(wids[0])
    endif
  endfunction " }}}
  command! -bar -nargs=1 -complete=buffer Buffer  call s:buf_open_existing(<q-mods>, <f-args>)
else
  function! s:buf_open_existing(bname) abort " {{{
    let bnr = bufnr(a:bname)
    if bnr == -1
      echoerr 'Buffer not found:' a:bname
      return
    endif
    let tindice = map(filter(map(range(1, tabpagenr('$')), '{"tindex": v:val, "blist": tabpagebuflist(v:val)}'), 'index(v:val.blist, bnr) != -1'), 'v:val.tindex')
    if empty(tindice)
      new
      execute 'buffer' bnr
    else
      execute 'tabnext' tindice[0]
      execute bufwinnr(bnr) 'wincmd w'
    endif
  endfunction " }}}
  command! -bar -nargs=1 -complete=buffer Buffer  call s:buf_open_existing(<f-args>)
endif

コマンドとしては,以下のように使用する. <バッファ名> はTabキーで補完可能である.

:Buffer <バッファ名>

バッファが :hide 等で隠れている場合は,新たにウィンドウを作成して,開き直す. Vim8.0からは,Exコマンド定義において <mod> が利用できるようになったため, :topleft:botright と併用することも可能になっている.

ターミナルへの応用

Vim8.0からは :terminal が実装された. この :terminal についても,複数タブを開いている場合,必要ないのに新たにターミナルを立ち上げてしまうかもしれない. そこで,前述と同様,ターミナルが既に起動していれば,そこにフォーカスを当てるコマンドを考えた.

実は,バンビちゃん氏のVim で :terminal の使い勝手をよくしたという記事に影響を受けている.

if has('terminal')
  function! s:complete_term_bufname(arglead, cmdline, cursorpos) abort " {{{
    let arglead = tolower(a:arglead)
    return filter(map(term_list(), 'bufname(v:val)'), '!stridx(tolower(v:val), arglead)')
  endfunction " }}}

  function! s:term_open_existing(qmods, ...) abort " {{{
    if a:0 == 0
      let bnrs = term_list()
      if empty(bnrs)
        execute a:qmods 'terminal'
      else
        let wids = win_findbuf(bnrs[0])
        if empty(wids)
          terminal
        else
          call win_gotoid(wids[0])
        endif
      endif
    else
      let bnr = bufnr(a:1)
      if bnr == -1
        throw 'E94: No matching buffer for ' . a:1
      elseif index(term_list(), bnr) == -1
        throw a:1 . ' is not a terminal buffer'
      endif
      let wids = win_findbuf(bnr)
      if empty(wids)
        execute a:qmods term_getsize(bnr)[0] 'new'
        execute 'buffer' bnr
      else
        call win_gotoid(wids[0])
      endif
    endif
  endfunction " }}}
  command! -bar -nargs=? -complete=customlist,s:complete_term_bufname Terminal  call s:term_open_existing(<q-mods>, <f-args>)
endif

このコマンドはターミナルバッファ名を引数に取り,ターミナルであるバッファのみをTab補完候補に出す.

:Terminal <ターミナルバッファ名>

まとめ

「既に開いているバッファがあるなら,そこに移動する」という機能は win_gotoid() を利用すれば簡単に実装できる.

参考文献

VimのCtrl-X補完を使えるようになりたい

はじめに

Vimにはデフォルトで補完機能が備わっている. 実践Vim 思考のスピードで編集しよう!であったり,daisuzuさんの2015年のVim Advent Calendarの記事VimのCTRL-X補完について - daisuzu's notesを読むと,neocompletedeopleteに頼るだけではなく,デフォルトの補完機能を活用したいと考えるようになるものである.

しかし, ins-completion を見るとわかるように,Vim<C-x> から始まる補完は12種類もあり,初めのうちはとても覚えきれるものではない. そこで,この記事では, <C-x> 補完初心者が使いこなせるようになるまでのヒント表示キーマッピングを考えた.

ins-completion

まずは,補完にはどのような種類があるかを簡単にまとめる. 以下の表は ins-completion の内容と同じなので,先程見た人は無視してよい.

キーマッピング 補完 必須オプション
<C-X><C-l> 行全体
<C-X><C-n>, <C-X><C-n> 現在のファイルのキーワード
<C-X><C-k> 'dictionary' のキーワード 'dictionary'
<C-X><C-t> 'thesaurus' のキーワード 'thesaurus'
<C-X><C-i> 編集中と外部参照(インクルード)しているファイルのキーワード
<C-X><C-]> タグファイル('tags' で設定したパスで見つかるファイル)
<C-X><C-f> ファイル名
<C-X><C-d> 定義もしくはマクロ
<C-X><C-v> Vimコマンドライン
<C-X><C-u> ユーザ定義補完 'completefunc'
<C-X><C-o> オムニ補完 'omnifunc'
<C-X>s, <C-X><C-s> スペリング補完 'spell'
<C-n>, <C-p> 'complete' のキーワード

この内, <C-n>, <C-p><C-x> の先行入力を伴わないので,ヒント表示の対象から外す.

ヒント表示キーマッピング

とにかく,以下のようにしただけという話.

" 入力キーの辞書
let s:compl_key_dict = {
      \ char2nr("\<C-l>"): "\<C-x>\<C-l>",
      \ char2nr("\<C-n>"): "\<C-x>\<C-n>",
      \ char2nr("\<C-p>"): "\<C-x>\<C-p>",
      \ char2nr("\<C-k>"): "\<C-x>\<C-k>",
      \ char2nr("\<C-t>"): "\<C-x>\<C-t>",
      \ char2nr("\<C-i>"): "\<C-x>\<C-i>",
      \ char2nr("\<C-]>"): "\<C-x>\<C-]>",
      \ char2nr("\<C-f>"): "\<C-x>\<C-f>",
      \ char2nr("\<C-d>"): "\<C-x>\<C-d>",
      \ char2nr("\<C-v>"): "\<C-x>\<C-v>",
      \ char2nr("\<C-u>"): "\<C-x>\<C-u>",
      \ char2nr("\<C-o>"): "\<C-x>\<C-o>",
      \ char2nr('s'): "\<C-x>s",
      \ char2nr("\<C-s>"): "\<C-x>s"
      \}
" 表示メッセージ
let s:hint_i_ctrl_x_msg = join([
      \ '<C-l>: While lines',
      \ '<C-n>: keywords in the current file',
      \ "<C-k>: keywords in 'dictionary'",
      \ "<C-t>: keywords in 'thesaurus'",
      \ '<C-i>: keywords in the current and included files',
      \ '<C-]>: tags',
      \ '<C-f>: file names',
      \ '<C-d>: definitions or macros',
      \ '<C-v>: Vim command-line',
      \ "<C-u>: User defined completion ('completefunc')",
      \ "<C-o>: omni completion ('omnifunc')",
      \ "s: Spelling suggestions ('spell')"
      \], "\n")
function! s:hint_i_ctrl_x() abort
  echo s:hint_i_ctrl_x_msg
  let c = getchar()
  return get(s:compl_key_dict, c, nr2char(c))
endfunction

inoremap <expr> <C-x>  <SID>hint_i_ctrl_x()

途中で :echo した上でキー入力待ち状態を作るには, <expr> を活用するのがよい. 上記のキーマッピングを行うと,以下のようになる.

f:id:koturn:20180210164917g:plain

これで,補完を行おうとしたときに「あの補完はどの <C-x> の後にどのキーを押下すればよかったんだっけ?」とならず,とりあえず <C-x> を押下してみて,「お~このキーであの補完ができるんだった!」となるのではないかと思う. もちろん,これは <C-x> 補完に不慣れな人向けのキーマッピングである. いちいち,複数行の :echo が表示されるのは,常用するレベルになると鬱陶しいことこの上ないだろう. 慣れるまではヒント表示を使い,慣れた頃に .vimrc から削除するのがよいと思われる.

おまけ

本記事におけるヒントの表示は,レジスタ内容やマーク位置の表示にも応用できるのではないかと思い,以下のキーマッピングを考えた.

function! s:hint_cmd_output(prefix, cmd) abort
  redir => str
    execute a:cmd
  redir END
  echo str
  return a:prefix . nr2char(getchar())
endfunction

" カーソル位置のマーク
nnoremap <expr> m  <SID>hint_cmd_output('m', 'marks')
" マーク位置へのジャンプ
nnoremap <expr> `  <SID>hint_cmd_output('`', 'marks')
" マーク位置へのジャンプ
nnoremap <expr> '  <SID>hint_cmd_output("'", 'marks')
" レジスタ参照(ヤンクや削除)
nnoremap <expr> "  <SID>hint_cmd_output('"', 'registers')
" マクロ記録
nnoremap <expr> q  <SID>hint_cmd_output('q', 'registers')
" マクロ再生
nnoremap <expr> @  <SID>hint_cmd_output('@', 'registers')

これらは次のようにヒントを表示する.

まずは,マークの例.

f:id:koturn:20180210164931g:plain

次に,ヤンクや削除時のレジスタ指定. 4"ayy のような行数指定を行っても問題ない.

f:id:koturn:20180210164938g:plain

まとめ

この記事では,VimのCtrl-X補完を使いこなせるようになるための初心者練習用ヒント表示のキーマッピングを紹介した. また,ヒント表示を応用して,マークやレジスタの内容を表示するキーマッピングも紹介した. あくまで初心者向けのキーマッピングであり,熟達するにつれて,ヒント表示がだんだん鬱陶しくなると思う. その頃に,この記事で紹介したキーマッピング.vimrc から削除し,よりスパルタンを目指すとよいだろう.

参考文献

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さんの記事になります.楽しみですね.

小さいHello Worldバイナリを作る

はじめに

先日は,少し不思議なHello Worldを紹介した. そこで思ったのが,この程度の小さいプログラムならば,gccは必要ないのではないかと思い至った. そこで,小さいHello Worldの実行ファイルを作ることにした.

方針

終了の仕方

先日の記事では,crt*.o ありきであったため,プログラマ視点でのプログマムのエントリポイントが main であったため,単に return 0; すればよかった. しかし,今回は crt*.o とのリンクは行わないため, return 0; に代わり, exit システムコールを呼び出す必要がある.

文字列データのアドレス

実行ファイルを生成するならば,"Hello World!\n" のアドレスも既知となる. したがって,ripから現在の実行中のアドレスを取得する必要はない.

また,前回と同様,文字列データは,コードの末尾に置くものとする.

セクションヘッダを削る

通常の実行ファイルは,

  1. ELFヘッダ
  2. プログラムヘッダ(複数)
  3. コードデータ
  4. セクション名テーブル
  5. セクションヘッダ(複数)

の5つを含むが 4.と5.は無くても良いので,以下の3つで実行ファイルを構成する.

  1. ELFヘッダ
  2. プログラムヘッダ(複数)
  3. コードデータ

セクションヘッダを削るため,$ objdump -d が効かなくなるが,まぁ良しとしよう.

実行ファイルの生成

上記の方針を踏まえ,実行ファイルを作るプログラムを書く. C言語で書く必要は無いのだが,ELFヘッダ,プログラムヘッダの構造体を利用することが可能なため,意外とCで書くのが楽になる. ただし,前回と同様,x64向けのプログラムを作る.

生成する実行ファイル名は a.out で固定しており,また,生成後に実行権限を付与し,実行するようにしてある.

これで,サイズにして171 bytesのHello Worldプログラムを作成できる.

コードの説明は特にしないが,構造体のメンバを見れば,どこで指定しているかは容易にわかる.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <elf.h>

#define BASE_ADDR    0x08000000
#define N_PROGRAM_HEADER  1
#define N_SECTION_HEADER  0
#define HEADER_SIZE  (sizeof(Elf64_Ehdr) + sizeof(Elf64_Phdr) * N_PROGRAM_HEADER)


int
main(void)
{
  unsigned int str_addr = BASE_ADDR + HEADER_SIZE + 37;
  Elf64_Ehdr ehdr;
  Elf64_Phdr phdr;
  char code[] =
    // mov $0x01,%rax  # System call number (write)
    "\x48\xc7\xc0\x01\x00\x00\x00"
    // mov $0x0d,%edx  # Third argument
    "\xba\x0d\x00\x00\x00"
    // mov $0x********,%rsi  # Second argument
    "\x48\xc7\xc6\x00\x00\x00\x00"
    // mov $0x01,%edi  # First argument
    "\xbf\x01\x00\x00\x00"
    // syscall
    "\x0f\x05"
    // mov $0x3c,%rax  # System call number (exit)
    "\x48\xc7\xc0\x3c\x00\x00\x00"
    // xor %edi,%edi   # First argument
    "\x31\xff"
    // syscall
    "\x0f\x05"
    // String data
    "Hello World!\n";
  FILE *f = fopen("a.out", "wb");
  if (f == NULL) {
    fputs("Failed to open a.out\n", stderr);
    return EXIT_FAILURE;
  }

  // ELF header
  ehdr.e_ident[EI_MAG0] = ELFMAG0;
  ehdr.e_ident[EI_MAG1] = ELFMAG1;
  ehdr.e_ident[EI_MAG2] = ELFMAG2;
  ehdr.e_ident[EI_MAG3] = ELFMAG3;
  ehdr.e_ident[EI_CLASS] = ELFCLASS64;
  ehdr.e_ident[EI_DATA] = ELFDATA2LSB;
  ehdr.e_ident[EI_VERSION] = EV_CURRENT;
  ehdr.e_ident[EI_OSABI] = ELFOSABI_LINUX;
  ehdr.e_ident[EI_ABIVERSION] = 0x00;
  ehdr.e_ident[EI_PAD] = 0x00;
  ehdr.e_type = ET_EXEC;
  ehdr.e_machine = EM_X86_64;
  ehdr.e_version = EV_CURRENT;
  ehdr.e_entry = BASE_ADDR + HEADER_SIZE;
  ehdr.e_phoff = sizeof(Elf64_Ehdr);
  ehdr.e_shoff = 0;
  ehdr.e_flags = 0x00000000;
  ehdr.e_ehsize = sizeof(Elf64_Ehdr);
  ehdr.e_phentsize = sizeof(Elf64_Phdr);
  ehdr.e_phnum = N_PROGRAM_HEADER;
  ehdr.e_shentsize = 0;
  ehdr.e_shnum = N_SECTION_HEADER;
  ehdr.e_shstrndx = SHN_UNDEF;
  fwrite(&ehdr, sizeof(ehdr), 1, f);

  // Program header
  phdr.p_type = PT_LOAD;
  phdr.p_flags = PF_R | PF_X;
  phdr.p_offset = 0x000000000000000000;
  phdr.p_vaddr = BASE_ADDR;
  phdr.p_paddr = BASE_ADDR;
  phdr.p_filesz = HEADER_SIZE + sizeof(code);
  phdr.p_memsz = HEADER_SIZE + sizeof(code);
  phdr.p_align = 0x0000000000000100;
  fwrite(&phdr, sizeof(phdr), 1, f);

  // Put string data address into code
  memcpy(code + 15, &str_addr, sizeof(str_addr));
  // Write .text section
  fwrite(code, 1, sizeof(code), f);

  fclose(f);
  f = NULL;

  // Show file size
  printf("Size of a.out: %lu bytes\n", HEADER_SIZE + sizeof(code));
  fflush(stdout);

  // Give execution permission
  chmod("a.out", 0755);
  // Execute created binary
  system("./a.out");

  return EXIT_SUCCESS;
}

コンパイルと実行結果は以下の通り.

$ gcc -O2 gen_hello_x64.c -o gen_hello_x64
$ ./gen_hello_x64
Size of a.out: 171 bytes
Hello World!

また,逆コンパイル結果は以下の通り. 前述した通り,セクションヘッダが無いため,プログラムデータ本体を見ることができない.

$ objdump -d a.out

a.out:     file format elf64-x86-64

おまけ: セクションヘッダを付ける

丁寧にセクションヘッダも付けるとしたら,プログラムは以下のようになる.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <elf.h>

#define BASE_ADDR    0x08000000
#define N_PROGRAM_HEADER  1
#define N_SECTION_HEADER  3
#define HEADER_SIZE  (sizeof(Elf64_Ehdr) + sizeof(Elf64_Phdr) * N_PROGRAM_HEADER)
#define FOOTER_SIZE  (sizeof(Elf64_Shdr) * N_SECTION_HEADER)

const char SH_STR_TBL[] = "\0.text\0.shstrtbl";


int
main(void)
{
  unsigned int str_addr = BASE_ADDR + HEADER_SIZE + 37;
  Elf64_Ehdr ehdr;
  Elf64_Phdr phdr;
  Elf64_Shdr shdr;
  char code[] =
    /* mov $0x01,%rax  # System call number (write) */
    "\x48\xc7\xc0\x01\x00\x00\x00"
    /* mov $0x0d,%edx  # Third argument */
    "\xba\x0d\x00\x00\x00"
    /* mov $0x********,%rsi  # Second argument */
    "\x48\xc7\xc6\x00\x00\x00\x00"
    /* mov $0x01,%edi  # First argument */
    "\xbf\x01\x00\x00\x00"
    /* syscall */
    "\x0f\x05"
    /* mov $0x3c,%rax  # System call number (exit) */
    "\x48\xc7\xc0\x3c\x00\x00\x00"
    /* xor %edi,%edi  # First argument */
    "\x31\xff"
    /* syscall */
    "\x0f\x05"
    /* String data */
    "Hello World!\n";
  FILE *f = fopen("a.out", "wb");
  if (f == NULL) {
    fputs("Failed to open a.out\n", stderr);
    return EXIT_FAILURE;
  }

  /* ELF header */
  ehdr.e_ident[EI_MAG0] = ELFMAG0;
  ehdr.e_ident[EI_MAG1] = ELFMAG1;
  ehdr.e_ident[EI_MAG2] = ELFMAG2;
  ehdr.e_ident[EI_MAG3] = ELFMAG3;
  ehdr.e_ident[EI_CLASS] = ELFCLASS64;
  ehdr.e_ident[EI_DATA] = ELFDATA2LSB;
  ehdr.e_ident[EI_VERSION] = EV_CURRENT;
  ehdr.e_ident[EI_OSABI] = ELFOSABI_LINUX;
  ehdr.e_ident[EI_ABIVERSION] = 0x00;
  ehdr.e_ident[EI_PAD] = 0x00;
  ehdr.e_type = ET_EXEC;
  ehdr.e_machine = EM_X86_64;
  ehdr.e_version = EV_CURRENT;
  ehdr.e_entry = BASE_ADDR + HEADER_SIZE;
  ehdr.e_phoff = sizeof(Elf64_Ehdr);
  ehdr.e_shoff = HEADER_SIZE + sizeof(code) + sizeof(SH_STR_TBL);
  ehdr.e_flags = 0x00000000;
  ehdr.e_ehsize = sizeof(Elf64_Ehdr);
  ehdr.e_phentsize = sizeof(Elf64_Phdr);
  ehdr.e_phnum = N_PROGRAM_HEADER;
  ehdr.e_shentsize = sizeof(Elf64_Shdr);
  ehdr.e_shnum = N_SECTION_HEADER;
  ehdr.e_shstrndx = 1;
  fwrite(&ehdr, sizeof(ehdr), 1, f);

  /* Program header */
  phdr.p_type = PT_LOAD;
  phdr.p_flags = PF_R | PF_X;
  phdr.p_offset = 0x000000000000000000;
  phdr.p_vaddr = BASE_ADDR;
  phdr.p_paddr = BASE_ADDR;
  phdr.p_filesz = HEADER_SIZE + sizeof(code) + sizeof(SH_STR_TBL) + FOOTER_SIZE;
  phdr.p_memsz = HEADER_SIZE + sizeof(code) + sizeof(SH_STR_TBL) + FOOTER_SIZE;
  phdr.p_align = 0x0000000000000100;
  fwrite(&phdr, sizeof(phdr), 1, f);

  /* Put string data address into code */
  memcpy(code + 15, &str_addr, sizeof(str_addr));
  /* Write .text section */
  fwrite(code, 1, sizeof(code), f);

  /* Write section header names */
  fwrite(SH_STR_TBL, 1, sizeof(SH_STR_TBL), f);

  /* First section header */
  shdr.sh_name = 0;
  shdr.sh_type = SHT_NULL;
  shdr.sh_flags = 0x0000000000000000;
  shdr.sh_addr = 0x0000000000000000;
  shdr.sh_offset = 0x0000000000000000;
  shdr.sh_size = 0x0000000000000000;
  shdr.sh_link = 0x00000000;
  shdr.sh_info = 0x00000000;
  shdr.sh_addralign = 0x0000000000000000;
  shdr.sh_entsize = 0x0000000000000000;
  fwrite(&shdr, sizeof(shdr), 1, f);

  /* Second section header (.shstrtbl) */
  shdr.sh_name = 7;
  shdr.sh_type = SHT_STRTAB;
  shdr.sh_flags = 0x0000000000000000;
  shdr.sh_addr = 0x0000000000000000;
  shdr.sh_offset = HEADER_SIZE + sizeof(code);
  shdr.sh_size = sizeof(SH_STR_TBL);
  shdr.sh_link = 0x00000000;
  shdr.sh_info = 0x00000000;
  shdr.sh_addralign = 0x0000000000000001;
  shdr.sh_entsize = 0x0000000000000000;
  fwrite(&shdr, sizeof(shdr), 1, f);

  /* Third section header (.text) */
  shdr.sh_name = 1;
  shdr.sh_type = SHT_PROGBITS;
  shdr.sh_flags = SHF_EXECINSTR | SHF_ALLOC;
  shdr.sh_addr = BASE_ADDR + HEADER_SIZE;
  shdr.sh_offset = HEADER_SIZE;
  shdr.sh_size = sizeof(code);
  shdr.sh_link = 0x00000000;
  shdr.sh_info = 0x00000000;
  shdr.sh_addralign = 0x0000000000000004;
  shdr.sh_entsize = 0x0000000000000000;
  fwrite(&shdr, sizeof(shdr), 1, f);

  fclose(f);
  f = NULL;

  /* Show file size */
  printf("Size of a.out: %lu bytes\n", HEADER_SIZE + sizeof(code) + sizeof(SH_STR_TBL) + FOOTER_SIZE);
  fflush(stdout);

  /* Give execution permission */
  chmod("a.out", 0755);
  /* Execute created binary */
  system("./a.out");

  return EXIT_SUCCESS;
}

上記のプログラムから a.out を生成し,objdump で逆アセンブルしてみると,以下のようになる.

$ gcc -O2 gen_hello_x64.c -o gen_hello_x64
$ ./gen_hello_x64
Size of a.out: 380 bytes
Hello World!
$ objdump -d a.out

a.out:     ファイル形式 elf64-x86-64


セクション .text の逆アセンブル:

0000000008000078 <.text>:
 8000078:       48 c7 c0 01 00 00 00    mov    $0x1,%rax
 800007f:       ba 0d 00 00 00          mov    $0xd,%edx
 8000084:       48 c7 c6 9d 00 00 08    mov    $0x800009d,%rsi
 800008b:       bf 01 00 00 00          mov    $0x1,%edi
 8000090:       0f 05                   syscall
 8000092:       48 c7 c0 3c 00 00 00    mov    $0x3c,%rax
 8000099:       31 ff                   xor    %edi,%edi
 800009b:       0f 05                   syscall
 800009d:       48                      rex.W
 800009e:       65                      gs
 800009f:       6c                      insb   (%dx),%es:(%rdi)
 80000a0:       6c                      insb   (%dx),%es:(%rdi)
 80000a1:       6f                      outsl  %ds:(%rsi),(%dx)
 80000a2:       20 57 6f                and    %dl,0x6f(%rdi)
 80000a5:       72 6c                   jb     0x8000113
 80000a7:       64 21 0a                and    %ecx,%fs:(%rdx)
        ...

無事に .text セクションの中身がわかるようになった. Hello World! の部分については無理に解釈されているため,でたらめなニーモニックが出力されているが,気にしなくてもよい.

まとめ

実行ファイルにはELFヘッダとプログラムヘッダが必須となるが,セクションヘッダは無くてもよい. ヘッダでエントリポイントを指定し,その位置から機械語を配置することで,実行ファイルができる.

なお,今回の記事の実行ファイル生成プログラムは以下のリポジトリにある.

また,工夫次第ではもっと小さい実行ファイルを作れるようだ. 以下の記事では終了ステータスを返すだけのx86プログラムだが,x64でも同様の手法がとれるだろう.