koturnの日記

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

C++の配列・STLコンテナで区切り文字を入れてostreamに出力する

はじめに

C++において,たまに std:coutstd:ofstreamstd::ostringstream といった出力ストリームに,配列や std::vector の要素を何かの区切り文字を入れて出力したいことがある. この「区切り文字を入れて出力」というのは少々面倒で,末端要素の後に区切り文字を入れないようにしないといけない. 可能な限り,そういった処理を簡潔に記述したいものである.

for

インデックスベースのforを利用するなら以下のように書くだろう.

#include <iostream>
#include <vector>


int
main()
{
  std::vector<int> v{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
  if (!v.empty()) {
    for (decltype(v)::size_type i = 0; i < v.size() - 1; i++) {
      std::cout << v[i] << ",";
    }
    std::cout << v[v.size() - 1] << std::endl;
  }

  return 0;
}

std::ostream_iterator

<algorithm>std::copy()<iterator>std::ostream_iterator を利用する方法がある. 個人的にはこの方法が一番良いと思う.

#include <algorithm>
#include <iostream>
#include <iterator>
#include <vector>


int
main()
{
  std::vector<int> v{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
  if (!v.empty()) {
    std::copy(
      std::cbegin(v),
      std::prev(std::cend(v)),
      std::ostream_iterator<const decltype(v)::value_type&>(std::cout, ","));
    std::cout << *std::crbegin(v) << std::endl;
  }

  return 0;
}

例のごとく,C++11にはフリー関数の cbegin(), cend(), crbegin() が無い. この程度で別にconstイテレータを取得する必要はないので, cbegin(), cend()begin(), end()を,crbegin()rbegin() メンバ関数を利用する.

#include <algorithm>
#include <iterator>
#include <vector>


int
main()
{
  std::vector<int> v{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
  if (!v.empty()) {
    std::copy(
      std::begin(v),
      std::prev(std::end(v)),
      std::ostream_iterator<const decltype(v)::value_type&>(std::cout, ","));
    std::cout << *v.rbegin() << std::endl;
  }

  return 0;
}

std::experimental::ostream_joiner

標準ライブラリではないが,試験的機能の1つとして std::experimental::ostream_joiner というものが存在する. gccであればバージョン6.1.0以上,clangであればバージョン3.9.1以上であれば利用可能であることは確認した. そして,当然のようにMSVCでは利用できない.

この ostream_joiner には型推論のためのフリー関数テンプレートである std::experimental::make_ostream_joiner() も存在するので,それを利用する.

#include <algorithm>
#include <iostream>
#include <iterator>
#include <vector>

#include <experimental/iterator>


int
main()
{
  std::vector<int> v{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
  std::copy(
    std::cbegin(v),
    std::cend(v),
    std::experimental::make_ostream_joiner(std::cout, ","));
  std::cout << std::endl;

  return 0;
}

Range-based forで何とかする

前回の記事Range クラスを利用する.

#include <iostream>
#include <iterator>
#include <type_traits>
#include <utility>
#include <vector>


template<typename Iterator>
class Range
{
public:
  Range(Iterator&& begin, Iterator&& end) noexcept
    : m_begin(std::forward<Iterator>(begin))
    , m_end(std::forward<Iterator>(end))
  {}

  Iterator
  begin() const noexcept
  {
    return m_begin;
  }

  Iterator
  end() const noexcept
  {
    return m_end;
  }

private:
  const Iterator m_begin;
  const Iterator m_end;
};  // class Range


template<typename Iterator>
static inline Range<Iterator>
makeRange(Iterator&& begin, Iterator&& end) noexcept
{
  return Range<Iterator>{std::forward<Iterator>(begin), std::forward<Iterator>(end)};
}


int
main()
{
  std::vector<int> v{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
  if (!v.empty()) {
    for (const auto& e : makeRange(std::cbegin(v), std::prev(std::cend(v)))) {
      std::cout << e << ",";
    }
    std::cout << *std::crbegin(v) << std::endl;
  }

  return 0;
}

Range-based forで逆順走査したい

はじめに

C++を書いていると,Range-based forで逆順操作をしたいことがたまにある. Range-based forは順方向の走査のみで,逆順に走査することはできない. なので,通常のインデックスベースのforを用いて,

std::vector<int> v{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
for (auto i = v.size() - 1; i != static_cast<decltype(i)>(-1); i--) {
  std::cout << v[i] << std::endl;
}

と書いたり,イテレータを利用して,

std::vector<int> v{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
for (auto itr = std::rbegin(v); itr != std::rend(v); ++itr) {
  std::cout << *itr << std::endl;
}

と書いたりする.

だが,このようなループにおいて欲しいのはインデックスやイテレータではなく,要素そのものが欲しい. ここにRange-based forで逆順に走査したいというモチベーションがある.

boost::adaptors::reverse

天下のBoostライブラリにはまさにそういうものがある.

#include <iostream>
#include <vector>

#if defined(__GNUC__) && __GNUC_PREREQ(4, 6)
#  pragma GCC diagnostic push
#  pragma GCC diagnostic ignored "-Wconversion"
#  pragma GCC diagnostic ignored "-Weffc++"
#  pragma GCC diagnostic ignored "-Wparentheses"
#  if __cplusplus >= 201103L
#    pragma GCC diagnostic ignored "-Wzero-as-null-pointer-constant"
#    if __GNUC_PREREQ(5, 0)
#      pragma GCC diagnostic ignored "-Wsuggest-override"
#    endif  // __GNUC_PREREQ(5, 0)
#  endif  // __cplusplus >= 201103L
#endif  // defined(__GNUC__) && __GNUC_PREREQ(4, 6)
#include <boost/range/adaptors.hpp>
#if defined(__GNUC__) && __GNUC_PREREQ(4, 6)
#  pragma GCC diagnostic pop
#endif  // defined(__GNUC__) && __GNUC_PREREQ(4, 6)

int
main()
{
  std::vector v{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
  for (const auto& e : boost::adaptors::reverse(v)) {
    std::cout << e << std::endl;
  }

  return 0;
}

ワーニングオプションを盛り沢山にしていると,Boostからそれなりにワーニングが出てくるので,ある程度抑えるようにしておいた.

boostを使わずにやりたい

場合によっては,boostに頼らずにやりたいこともあると思われる. range-based forが受理するのは,begin()end() というメンバ関数を持つクラスのインスタンスである. なので,以下のようにしてみる.

#include <iostream>
#include <iterator>
#include <type_traits>
#include <vector>


// C++11ではrbegin(), rend()を持つかどうかを判定するメタ関数
// C++14以降では,std::rbegin(), std::rend()に受理されるかどうかを判定するメタ関数
template<typename T>
struct has_rbegin_rend
{
private:
  template<typename U>
  static auto
#if __cplusplus >= 201402L
  check(U&& obj) -> decltype(std::rbegin(obj), std::rend(obj), std::true_type{});
#else
  check(U&& obj) -> decltype(obj.rbegin(), obj.rend(), std::true_type{});
#endif

  static std::false_type
  check(...);

public:
  static constexpr bool value = decltype(check(std::declval<T>()))::value;
};


#if __cplusplus >= 201402L
template<typename T>
constexpr bool has_rbegin_rend_v = has_rbegin_rend<T>::value;
#endif


template<typename Iterator>
class Range
{
public:
  Range(Iterator&& begin, Iterator&& end) noexcept
    : m_begin(std::forward<Iterator>(begin))
    , m_end(std::forward<Iterator>(end))
  {}

  Iterator
  begin() const noexcept
  {
    return m_begin;
  }

  Iterator
  end() const noexcept
  {
    return m_end;
  }

private:
  const Iterator m_begin;
  const Iterator m_end;
};  // class Range


template<typename Iterator>
static inline Range<Iterator>
makeRange(Iterator&& begin, Iterator&& end) noexcept
{
  return Range<Iterator>{std::forward<Iterator>(begin), std::forward<Iterator>(end)};
}


#if __cplusplus >= 201402L
// std::initiaizer_listをわざわざ逆順にしたい人は皆無だと思うけど,一応...
template<typename T>
static inline decltype(auto)
makeReversedRange(const std::initializer_list<T>& iniList) noexcept
{
  return makeRange(std::rbegin(iniList), std::rend(iniList));
}


template<
  typename T,
  typename std::enable_if_t<has_rbegin_rend_v<T>, std::nullptr_t> = nullptr
>
static inline decltype(auto)
makeReversedRange(T&& c) noexcept
{
  return makeRange(std::rbegin(c), std::rend(c));
}
#else
template<typename T>
static inline auto
makeReversedRange(const std::initializer_list<T>& iniList) noexcept
  -> decltype(makeRange(iniList.rbegin(), iniList.rend()))
{
  return makeRange(iniList.rbegin(), iniList.rend());
}


template<
  typename T,
  typename std::enable_if<has_rbegin_rend<T>::value, std::nullptr_t>::type = nullptr
>
static inline auto
makeReversedRange(T&& c) noexcept
  -> decltype(makeRange(c.rbegin(), c.rend()))
{
  return makeRange(c.rbegin(), c.rend());
}
#endif


// rbegin(), rend()を持たないものはこっちに分岐させて,エラーメッセージを少なくする
template<
  typename T,
  typename std::enable_if<!has_rbegin_rend<T>::value, std::nullptr_t>::type = nullptr
>
static inline void
makeReversedRange(T&&) noexcept
{
  static_assert(has_rbegin_rend<T>::value, "Specified argument doesn't have reverse iterator.");
}


int
main()
{
  std::vector<int> v{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
  for (const auto& e : makeReversedRange(v)) {
    std::cout << e << std::endl;
  }

  return 0;
}

どうしてもC++11でなくてはならないときに備えて,C++11でもコンパイルが通るようにした. (この部分のコードを削除すれば,上記コードは少し短くなるだろう.) C++11はフリー関数版の rbegin()rend() が無いので,メンバ関数を呼び出すようにしている. そのため上記コードは,C++11において,組み込み配列の逆順走査はできない. わざわざ フリー関数の rbegin()rend() を実装するのは面倒なので,その点は妥協した.

また, makeReversedRange に,STLコンテナや配列以外を与えた場合のコンパイルエラーメッセージを抑えるため,SFINAEを利用している. その部分も省けば,上記コードはもっと短くなる.

おまけ

本記事の趣旨とは外れるが,そもそも std::for_each() で逆順走査ができる. C++14以降なら,

#include <algorithm>
#include <iostream>
#include <vector>


int
main()
{
  std::vector<int> v{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
  std::for_each(std::rbegin(v), std::rend(v), [](const auto& e){
    std::cout << e << std::endl;
  });

  return 0;
}

で,C++11なら,

#include <algorithm>
#include <iostream>
#include <vector>


int
main()
{
  std::vector<int> v{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
  std::for_each(v.rbegin(), v.rend(), [](const decltype(v)::value_type& e){
    std::cout << e << std::endl;
  });

  return 0;
}

と書ける. だが,どうしてもRang-based forで書きたいのだ!という人達もいるはずである(ホンマか?)

一応,std::for_each() のRange-based forに劣る点を挙げるならば,

  1. イテレータの指定が必要な分,どうしてもユーザコードが膨れ上がる.
  2. C++11にはジェネリックラムダが無いため,forの処理部分のラムダの引数の記述が冗長になりがちである.

の2点がある.

まとめ

  1. Boost.Range の boost::adaptors::reverse()STLコンテナ等を渡せば,簡単に逆順走査できる
  2. boost::adaptors::reverse() 相当のものは簡単に作れる

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 から削除し,よりスパルタンを目指すとよいだろう.

参考文献