読者です 読者をやめる 読者になる 読者になる

とくにあぶなくないRiSKのブログ

危ないRiSKのブログだったかもしれない。本当はRiSKだけどググラビリティとか取得できるIDの都合でsscriskも使ったり。

in<T> イディオム

C++

C++ Advent Calendar 2012

このエントリは、C++ Advent Calendar 2012 の29日目参加記事です。
「一体いつから ― C++ Advent Calendar 終了だと錯覚していた?」 *1

では本題。問題

架空の matrix クラスに効率のいい operator+ を定義したいとします。どのように書いたらいいでしょうか?

いくつかの案

(ここではシンプルにするため、定義ではなく宣言にとどめます)

const 参照
matrix operator+(matrix const & m1, matrix const & m2);
値渡し
matrix operator+(matrix m1, matrix m2);
rvalue 参照とconst 参照のオーバーロード
matrix operator+(matrix const & m1, matrix const & m2);
matrix operator+(matrix && m1, matrix const & m2);
matrix operator+(matrix const & m1, matrix && m2);
matrix operator+(matrix && m1, matrix && m2);
完全転送(perfect forwarding)
extern void* enabler;
template<
 class T,
 typename std::enable_if<typename std::is_same<T, matrix>::value>::type*& = enabler>
>
matrix operator+(T&& m1, T&& m2);

新たな案

先に挙げた案は、効率が悪かったり、コードが長くなったり、あまり本質的ではないテンプレートを使わなければならなかったりします。
ここにもう一つ新たな案を加えます。

in イディオム

以下のようなクラステンプレート in を定義し、これを使います。

template<class T>
class in {
 T const & ref;
 bool rv;
 
public:
 in(T const& l) : ref(l), rv(false)  {}
 in(T&& r)      : ref(r), rv(true) {}

 bool lvalue() const { return !rv; }
 bool rvalue() const { return rv; }

 operator T const & () const { return ref; }
 const T& get() const { return ref; }
 T&& rget() const { return std::move(const_cast<T&>(ref)); }

 T move() const { return rv ? rget() : ref; }
};

in を用いた operator+ の定義は次のようになります。

matrix operator+(in<matrix> m1, in<matrix> m2) {
 if(m1.rvalue()) {
  matrix r(m1.rget());
  r += m2.get();
  return r;
 } else if(m2.rvalue()) {
  matrix r(m2.rget());
  r += m1.get();
  return r;
 } else {
  matrix r(m1.get());
  r += m2.get();
  return r;
 }
}

in により、rvalue 参照なのか lvalue 参照なのか関数内で判断することができ、説明的なコードを書けるようになります。
次のような型(ケース)で使えます。

  1. lvalueをconst参照に束縛
  2. rvalueをrvalue参照に束縛
  3. 実行時にlvalueかrvalueかを判断
  4. テンプレートの使用を強制しない
  5. コードを膨れさせない
  6. 組み込み

コード全体

#include<utility>

template<class T>
class in {
 T const & ref;
 bool rv;
 
public:
 in(T const& l) : ref(l), rv(false)  {}
 in(T&& r)      : ref(r), rv(true) {}

 bool lvalue() const { return !rv; }
 bool rvalue() const { return rv; }

 operator T const & () const { return ref; }
 const T& get() const { return ref; }
 T&& rget() const { return std::move(const_cast<T&>(ref)); }

 T move() const { return rv ? rget() : ref; }
};

#include<iostream>

struct matrix {
 int i;
 matrix(int i) : i(i) { std::cout << "matrix(int i) {" << i << "}\n"; }
 matrix(matrix const & r): i(r.i) { std::cout << "matrix(matrix const &) {" << i << "}\n"; }
 matrix(matrix&& r) : i(r.i) { std::cout << "matrix(matrix&& r) {" << i << "}\n"; }
 matrix& operator+=(matrix const & r){
  i += r.i;
  return *this;
 }
};

matrix operator+(in<matrix> m1, in<matrix> m2) {
 if(m1.rvalue()) {
  matrix r(m1.rget());
  r += m2.get();
  return r;
 } else if(m2.rvalue()) {
  matrix r(m2.rget());
  r += m1.get();
  return r;
 } else {
  matrix r(m1.get());
  r += m2.get();
  return r;
 }
}

int main(){
 {
  std::cout << "----------\n";
  matrix a{1}, b{2};
  matrix c = a + b;
 }
 {
  std::cout << "----------\n";
  matrix a{1};
  matrix c = a + matrix(2);
 }
 {
  std::cout << "----------\n";
  matrix b{2};
  matrix c = matrix(1) + b;
 }
 {
  std::cout << "----------\n";
  matrix c = matrix(1) + matrix(2);
 }
}

実行結果

----------
matrix(int i) {1}
matrix(int i) {2}
matrix(matrix const &) {1}
matrix(matrix&& r) {3}
----------
matrix(int i) {1}
matrix(int i) {2}
matrix(matrix&& r) {2}
matrix(matrix&& r) {3}
----------
matrix(int i) {2}
matrix(int i) {1}
matrix(matrix&& r) {1}
matrix(matrix&& r) {3}
----------
matrix(int i) {2}
matrix(int i) {1}
matrix(matrix&& r) {1}
matrix(matrix&& r) {3}

それぞれのテストの三行目に注目すると、可能な限りムーブが使われていることが分かります。

注意点

  • パラメータのコピーが条件的になされるときのみ有用。
  • 曖昧な暗黙の型変換が関わってるときにやばい。
  • 他にもやばいことあるかも。

追記

後日(2013/9/16)、パワーアップされていました。
C++ Truths: 21 Ways of Passing Parameters ... Plus One!

*1:本日アキラさんの許可をいただけたので、C++ACに飛び入り参加。