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

メンバ関数版のboost::bindした結果の関数オブジェクトはデフォルトコンストラクトできない

C++

boost::range RangeConcept problem.


先日Boost User MLで質問が出ていたので回答したのですが、その詳細をこちらで書きます。


関数の部分適用を行うboost::bind()は、関数ポインタ、関数オブジェクト、メンバ関数ポインタに対して適用できますが、メンバ関数ポインタ版の場合、bind()の結果として返される関数オブジェクトは、デフォルト構築できません。

#include <boost/bind.hpp>

struct X {
    int f(int x) const { return x; }
};

template <class F>
void function_check(F f)
{
    F x; // コンパイルエラー!デフォルト構築できない
}

int main()
{
    function_check(boost::bind(&X::f, _1, _2));
}

メンバ関数ポインタ版のbind()は内部でmem_fn()のラッパーなのですが、mem_fn()によって返される関数オブジェクトがデフォルト構築できないからこうなってます。元となるメンバ関数ポインタを受け取ってそれを通常の関数呼び出しできるようにするのがmem_fn()なので、関数が空という状態がありえないのでこうなってます。


mem_fn.hpp documentation

namespace boost
{
template<class T> T * get_pointer(T * p);
template<class R, class T> unspecified-1 mem_fn(R (T::*pmf) ());
template<class R, class T> unspecified-2 mem_fn(R (T::*pmf) () const);
template<class R, class T> unspecified-2-1 mem_fn(R T::*pm);
template<class R, class T, class A1> unspecified-3 mem_fn(R (T::*pmf) (A1));
template<class R, class T, class A1> unspecified-4 mem_fn(R (T::*pmf) (A1) const);
template<class R, class T, class A1, class A2> unspecified-5 mem_fn(R (T::*pmf) (A1, A2));
template<class R, class T, class A1, class A2> unspecified-6 mem_fn(R (T::*pmf) (A1, A2) const);

// implementation defined number of additional overloads for more arguments
}

All unspecified-N types mentioned in the Synopsis are CopyConstructible and Assignable. Their copy constructors and assignment operators do not throw exceptions. unspecified-N::result_type is defined as the return type of the member function pointer passed as an argument to mem_fn (R in the Synopsis.) unspecified-2-1::result_type is defined as R.

範囲の各要素に対して遅延的に関数を適用していくBoost.Rangeのfiltered Rangeアダプタは、受け取った関数オブジェクトを内部に持つイテレータの組として定義されます。
デフォルト構築できない関数オブジェクトを内部に持つイテレータもまた、デフォルト構築できないので、イテレータを扱うアルゴリズムがデフォルト構築しようとした場合、コンパイルエラーになります。
C++の標準アルゴリズムは、内部でデフォルト構築するかどうかまでは規定していないので、この問題は処理系によって起きたり起こらなかったりします。
(InputIteratorコンセプトによって、デフォルト構築可能であることがイテレータに要求されています)


今回問題を報告していた人は、std::max_element()の実装がその人の環境でたまたまデフォルト構築していたことで問題が顕在化しました。
問題を単純化した例を出します。ここではイテレータをデフォルト構築するstd::for_each()アルゴリズムの実装例を使用して、メンバ関数ポインタをbind()した関数オブジェクトをtransformedしてます。

#include <boost/bind.hpp>
#include <boost/range.hpp>
#include <boost/range/adaptor/transformed.hpp>

template <class InputIterator, class F>
F for_each_(InputIterator first, InputIterator last, F f)
{
    InputIterator it; // default construct
    it = first; // copy assign

    while (it != last) {
        f(*it);
        ++it;
    }
    return f;
}

template <class Range, class F>
F for_each_(const Range& r, F f)
{
    return for_each_(boost::begin(r), boost::end(r), f);
}

struct X {
    int f(int x) const { return x + 1; }
};

void nop(int x) {}

int main()
{
    std::vector<int> v = {1, 2, 3};
    X x;

    ::for_each_(v | boost::adaptors::transformed(boost::bind(&X::f, &x, _1)), nop); // コンパイルエラー!イテレータがデフォルト構築できない
}

コンパイルエラーになりますね。


なので、このケースでもPStade.Oven由来のregular()が必要になります。
regular()は、関数オブジェクトをデフォルト構築可能な形式に型変換します。
Boost.Rangeの拡張であるOvenToBoostでは、regular()を単純化した演算子operator|+()を持ってます。これを使えば、コンパイルエラーは解消します。

#include <boost/bind.hpp>
#include <boost/range.hpp>
#include <boost/range/adaptor/regular_extension/transformed.hpp>

template <class InputIterator, class F>
F for_each_(InputIterator first, InputIterator last, F f)
{
    InputIterator it; // default construct
    it = first; // copy assign

    while (it != last) {
        f(*it);
        ++it;
    }
    return f;
}

template <class Range, class F>
F for_each_(const Range& r, F f)
{
    return for_each_(boost::begin(r), boost::end(r), f);
}

struct X {
    int f(int x) const { return x + 1; }
};

void nop(int x) {}

int main()
{
    std::vector<int> v = {1, 2, 3};
    X x;

    ::for_each_(v |+ boost::adaptors::transformed(boost::bind(&X::f, &x, _1)), nop); // OK
}

Boost.RangeのRangeアダプタを利用してる人は、filteredやtransformedに渡している関数オブジェクトがデフォルト構築可能かどうか確かめておいたほうがいいですよ。他の環境への移植時にコンパイルエラーになる可能性がありますので。