[迷信] アルゴリズム関数内で関数オブジェクトはコピーされない

まずは次のコードをご覧ください。

#include <iostream>
#include <algorithm>
#include <iterator>
 
class countup
{
public:
  explicit countup(int init = 0)
    : value_(init)
  {
  }
  int operator()()
  {
    return value_++;
  }
private:
  int value_;
};
 
int main()
{
  int array[10];
  std::generate(array + 0, array + 10, countup());
  std::copy(array + 0, array + 10, std::ostream_iterator<int>(std::cout, "n"));
  return 0;
}

簡単に上のコードの説明をします。countup クラスは、コンストラクタで与えられた値を初期値として、関数呼出し演算子を用いて呼出されるごとにインクリメントした値を返します。main 関数では、アルゴリズム関数 generatecountup オブジェクトを適用して、配列 array の各要素の値を生成しています。

(エラーチェックがないことを除けば)一見何の問題もなさそうなコードですが、実は、深刻な移植性の問題を抱えています。

このコードでは、countup クラスの一時オブジェクトを generate 関数の実引数として渡したときに一度コピーが発生することを除けば、generate 関数内ではそれ以上コピーが発生しないことを前提としています。しかし、標準規格ではそのようなことは保証されていません。例えば、generate 関数が、

template<class ForwardIterator, class Generator>
void generate(ForwardIterator first, ForwardIterator last, Generator gen)
{
  while (first != last)
  {
    Generator g(gen);
    *first++ = g();
  }
}

といった実装であったとしても、規格上は特に問題がありません。実際には、効率を考えるとこんなことをする理由はないのですが、もしかすると、処理系定義の何らかの方法で、関数オブジェクトをループの一周ごとにチェックできるようなケースもないとはいいいきれません。

こうした問題を解消するには、アルゴリズム関数内で関数オブジェクトは唯一つのインスタンスが用いられるという前提を捨てなければなりません。具体的には、

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

class countup
{
public:
  explicit countup(int& init)
    : ptr_(&init)
  {
  }
  int operator()()
  {
    return (*ptr_)++;
  }
private:
  int* ptr_;
};

int main()
{
  int array[10];
  int x = 0;
  std::generate(array + 0, array + 10, countup(x));
  std::copy(array + 0, array + 10, std::ostream_iterator<int>(std::cout, "n"));
  return 0;
}

といった対策が必要になってきます。あるいは、元の使い勝手と同じにするには、coutup クラスの内部で参照カウンタ方式のスマートポインタを用いるなど、非常にオーバーヘッドの大きな方法を選択せざるを得なくなります。

トラックバック


URL から "-nospam" を削除してトラックバックを送信してください。

少し改良してみました

このような方法ではだめなのでしょうか?

#include <iostream>
#include <algorithm>
#include <iterator>
 
class countup
{
public:
  explicit countup(int init = 0)
    : value_(init),value_p(&value_)
  {
  }
  countup(countup &src)
    : value_(0),value_p(&(src.value_))
  {
  }
  countup(const countup &src)
    : value_(src.value_),value_p(&value_)
  {
  }
  countup& operator=(countup &src)
  {
    value_=0;
    value_p=&(src.value_);
    return *this;
  }
  countup& operator=(const countup& src)
  {
    value_=src.value_;
    value_p=value_;
  }
  countup
  int operator()()
  {
    return (*value_p)++;
  }
private:
  int value_;
  int *value_p;
};

namespace my{
  template<class ForwardIterator, class Generator>
  void generate(ForwardIterator first, ForwardIterator last, Generator gen)
  {
    while (first != last)
    {
      Generator g(gen);
      *first++ = g();
    }
  }
}

int main()
{
  int array[10];
  my::generate(array + 0, array + 10, countup());
  std::copy(array + 0, array + 10, std::ostream_iterator<int>(std::cout, "n"));
  return 0;
}

Re:少し改良してみました

だめではないですが、ちょっと分かりにくいですね。
これは、アルゴリズム関数呼び出しの際に渡した実引数の生存期間が、アルゴリズム関数から戻るまでの間持続することに依存しています。それで間違いありませんが、直感的ではないと思います。
ただ、オーバーヘッドは少ないので、効率の点ではいいですね。問題が生じるケースがないか、もう少し考えてみる必要はありそうですが...。

やはりダメそうです。

const参照を使ったコピーだと、カウント値がコピーされることになるので、解決にならないようです。

_

うーん、考え方そのものはまずくないはずなのですが…
const&によるコピーの可能性を捨て切れなかったので、仕方ないのでconst&のコピーコンストラクタとoperator=を用意する必要がありました。

このエントリーを含むはてなブックマーク