Expression Template と auto

これはC++ Advent Calendar jp 2010への参加記事です。

C++の次世代規格であるC++0xには、新しくautoという機能が加わります。autoは次のように使います。

  auto p = std::make_pair(1, 2.0);

上のコードでは、std::make_pair(1, 2.0)の型が推論され、pの型はstd::pairとなります。
以下のような

std::pair<int, double> p = std::make_pair(1, 2.0);

冗長な型の記述をなくせる非常に便利な機能です。constをつけたり、参照にしたりすることもできます。詳しくはこちらを御覧ください。

このようにautoはとても便利な機能なので、C++0xが普及したら多用されることでしょう。しかし、何も考えずにautoを使うと、ごく稀に分かりにくいバグを入れてしまう場合があります。Expression Templateを使ったライブラリを使用する場合です。

Expression Template

Expression Templateとは、式をテンプレートの階層として表現することにより、様々な機能を実現する手法です。Boost.uBLASのような線形代数ライブラリ、Boost.LambdaやBoost.Spiritなどに用いられています。

次のような、簡単なベクトルを表すクラステンプレートを考えます。

template <typename T>
class Vector {
public:
  typedef T value_type;

  explicit Vector(std::size_t n)
    : buf_(new T[n]), size_(n)
  {}
  Vector(Vector const& v)
    : buf_(new T[v.size_]), size_(v.size_)
  {
    std::copy(v.buf_, v.buf_ + v.size_, buf_);
  }
  Vector& operator=(Vector const& v) {
    Vector tmp(v);
    swap(tmp);
    return *this;
  }
  
  void swap(Vector& v) {
    using std::swap;
    swap(buf_, v.buf_);
    swap(size_, v.size_);
  }

  T& operator[](std::size_t n) {
    return const_cast<T&>(const_cast<Vector const&>(*this)[n]);
  }
  T const& operator[](std::size_t n) const {
    assert(n < size_);
    return buf_[n];
  }

  std::size_t size() const { return size_; }

private:
  boost::scoped_array<T> buf_;
  std::size_t size_;
};

このベクトルに対する加法演算子

template <typename T>
Vector<T> operator+(Vector<T> const& u, Vector<T> const& v)
{
  std::size_t size = u.size();
  assert(size == v.size());
  Vector<T> result(size);
  for (std::size_t i = 0; i < size; ++i)
    result[i] = u[i] + v[i];
  return result;
}

を考えます。Vectorの変数v1, v2, v3の和の値を取る新しい変数uを作るには、当然

Vector<double> u = v1 + v2 + v3;

と書きます。ここでは、v1 + v2が実行されて一時オブジェクトを返し、その一時オブジェクトとv3との和によってuが初期化されます。v1 + v2によって無駄な一時オブジェクトが生成されています。Expression Templateを使うと、シンプルな記述を保ちながら一時オブジェクトの生成を避けることができます。

上の加法演算子の代わりに、次のようなPlusクラステンプレート

template <typename Vec1, typename Vec2>
class Plus {
public:
  typedef typename Vec1::value_type value_type;
  BOOST_MPL_ASSERT((boost::is_same<value_type, typename Vec2::value_type>));

  Plus(Vec1 const& u, Vec2 const& v)
    : lhs_(u), rhs_(v)
  {}

  std::size_t size() const {
    std::size_t const size = lhs_.size();
    assert(size == rhs_.size());
    return size;
  }

  value_type operator[](std::size_t n) {
    assert(n < size());
    return lhs_[n] + rhs_[n];
  }

  operator Vector<value_type>() const {
    Vector<value_type> result(size());
    for (std::size_t i = 0, sz = size(); i < sz; ++i)
      result[i] = lhs_[i] + rhs_[i];
    return result;
  }

private:
  Vec1 const& lhs_;
  Vec2 const& rhs_;
};

および加法演算子

template <typename Vec1, typename Vec2>
Plus<Vec1, Vec2> operator+(Vec1 const& u, Vec2 const& v)
{
  return Plus<Vec1, Vec2>(u, v);
}

を定義します。この2つの定義によって、以下の文

Vector<double> u = v1 + v2 + v3;

においてv1 + v2ではVector型の一時オブジェクトは生成されません。実際の計算はオブジェクトuのコンストラクタに渡されるまで遅延されます。ここで、v1 + v2 + v3の型はPlus<...>であることに注意してください。Plus<...>から、Plus<...>の型変換演算子によってVectorが生成されます。この変換はuの型がVectorであると書かれているから起きるのです。

autoを使う

型をVectorと明示的に決めずに、autoを使ったらどうなるでしょうか。

auto u = v1 + v2 + v3;

このとき、uの型はVectorではなくPlus<...>です。これはv1, v2, v3への参照を持っており、たとえばv1の値を変更を変更したらuを評価したときの値も変わります。また、

auto u = Vector<double>(3) + Vector<double>(3);

などとすると、動作未定義への道が開かれます。

補足を書きました。