0

私はこのHaskellスタイルmapの高階関数を持っています。これは、結果タイプを除いて、呼び出し側のクリーンな構文にテンプレートタイプの推論をすでに使用しています。

template<class ResultingContainer, class R, class Fn>
ResultingContainer map(const R &range, const Fn &f) {
    ResultingContainer result;
    using value_type = decltype(*std::begin(range));
    for_each(range, [&](const value_type &v){
        result.push_back(f(v));
    });
    return result;
}

(私はそのようなインターフェースを提供しないコンテナーを使用するので、またはいくつかのイテレーター特性よりも好みます:Qtコンテナーdecltype(*std::begin(range))。)R::value_type

この関数の使用は簡単ですが、私が望むほど単純ではありません。皆さんは私が何を意味するかを知っています。

std::vector<int> result = map<std::vector<int>>(otherVector, someFunction);

もちろん、私はそれを次のようにしたいと思います。

std::vector<int> result = map(otherVector, someFunction);

次のようにAPIを設計した場合、型の推定は完全に機能します。ただし、これはC ++標準ライブラリスタイル(イテレータペア/範囲の違いを除く)であるため、このようにはしたくありませんが、「Haskellスタイル」にしたいです。

std::vector<int> result;
map(otherVector, result, someFunction);

必ずしも結果と同じ型である必要はないという事実を考えるとotherVector(たとえば、文字列である可能性があり、その長さが必要なのでsomeFunction、指定された文字列の長さを返します)ResultingContainer、既知の型(特に、入力の値型)。

私が(そして私がしたくない)単一のタイプのコンテナに固執すれば、それを表現することができます。autoしかし、1つの希望が残っています。戻り型に使用されない限り、コンパイラは結果がどうあるべきかをすでに知っています。(たとえば、オーバーロードされた関数の引数として使用される場合など、結果タイプに複数の可能性がある場合は混乱します)。

そこで、デフォルトのプロキシコンテナについて考えましたResultingContainer(ただし、ユーザーが必要に応じて上書きできますが、それは私の問題ではありません)。ただし、現在の知識では、すべての結果アイテムを一時的に保持ProxyContainer<T>する一部(Tの結果タイプ、fで使用される述語map)を実装することしかできませんでした。これは不要なオーバーヘッドのように聞こえます。ただし、実装は次のようになります。

template<typename T>
class ProxyContainer : public std::vector<T> {
    template<typename FinalContainer>
    operator FinalContainer() const {
        FinalContainer result;
        for (auto x : *this)
            result.push_back(x);
        return result;
    }
};

しかし、これは正しく感じられません。

より良い解決策を思い付くことができますか?

4

2 に答える 2

2

Xeoが提案したように、「レイジーアダプター」を使用して問題を解決しました。しかし、Boostを使用する代わりに、非常に単純なものを作成しました。

template<class R, class Fn>
class MapAdaptor
{
    const R &range;
    const Fn &f;

public:
    MapAdaptor(const R &range, const Fn &f) :
        range(range), f(f)
    {
    }

    template<class ResultingContainer>
    operator ResultingContainer() const {
        ResultingContainer result;
        for(const auto &v : range)
            result.push_back(f(v));
        return result;
    }
};

すると、実際のmap関数は次のようになります。

template<class R, class Fn>
MapAdaptor<R,Fn> map(const R &range, const Fn &f) {
    return MapAdaptor<R,Fn>(range, f);
}

アダプター自体はまだ範囲にしませんでしたが、操作をチェーン/ネストできるようにするために、この機能を追加します。

于 2013-03-26T21:48:24.167 に答える
1

これは一般的なレイジーアダプタの実装であり、container_traitsデータをなどに返すことができるものもありますstd::set

#include <tuple>
#include <iterator>
#include <utility>
#include <algorithm>

// Standard metaprogramming boilerplate:
template<std::size_t...>
struct seq {};
template<std::size_t max, std::size_t... s>
struct make_seq:make_seq<max-1, max-1, s...> {};
template<std::size_t... s>
struct make_seq<0, s...> {
  typedef seq<s...> type;
};
// helper to make creating a sequence from 0,...,max-1 take less typing:
template<std::size_t max>
using MakeSeq = typename make_seq<max>::type;

// takes a sequence of indexes, a tuple, a Result type, and a Functor, and calls the Functor passing
// in the Result type with the fowarded args in the tuple, as indicated by the sequence of indexes:
template<typename Result, template<typename Result>class Functor, typename... Args, std::size_t... s>
Result apply( seq<s...>, std::tuple<Args...>& args ) {
  return Functor<Result>()(std::forward<Args>(std::get<s>(args))...);
}

// and here it is, a generic lazy evaluator:
template<template<typename Result>class Functor, typename... Args>
struct LazyEvaluation {
  std::tuple<Args...> stored_args;
  LazyEvaluation( Args... args ):stored_args(std::forward<Args>(args)...) {};
  template<typename R>
  operator R() {
    return apply<R, Functor>( MakeSeq<sizeof...(Args)>(), stored_args );
  }
};

// The start of some container traits templates:
template<typename T, typename=void>
struct const_iterator_type:std::false_type {};
template<typename T>
struct const_iterator_type<T, typename std::enable_if<
  std::is_same< typename T::const_iterator, typename T::const_iterator >::value
>::type>:std::true_type {
   typedef typename T::const_iterator const_iterator;
};
template<typename T, size_t n>
struct const_iterator_type< T[n] >:std::true_type {
   typedef T const* const_iterator;
};


template<typename T,typename=void>
struct has_push_back:std::false_type {};
template<typename T>
struct has_push_back<T, typename std::enable_if<
  std::is_same<
     decltype(std::declval<T>().push_back(*std::declval<typename const_iterator_type<T>::const_iterator>())),
     decltype(std::declval<T>().push_back(*std::declval<typename const_iterator_type<T>::const_iterator>()))
  >::value
>::type>:std::true_type{};
template<typename T,typename=void>
struct has_insert:std::false_type {};
template<typename T>
struct has_insert<T, typename std::enable_if<
  std::is_same<
     decltype(std::declval<T>().insert(*std::declval<typename const_iterator_type<T>::const_iterator>())),
     decltype(std::declval<T>().insert(*std::declval<typename const_iterator_type<T>::const_iterator>()))
  >::value
>::type>:std::true_type {};

template<typename Container, typename=void>
struct container_traits;

template<typename Container>
struct container_traits<Container, typename std::enable_if<has_push_back<Container>::value>::type> {
   template<typename V>
   static void add_to_container( Container& c, V&& v ) {
     c.push_back( std::forward<V>(v) );
   }
};
template<typename Container>
struct container_traits<Container, typename std::enable_if<!has_push_back<Container>::value && has_insert<Container>::value>::type> {
   template<typename V>
   static void add_to_container( Container& c, V&& v ) {
     c.insert( std::forward<V>(v) );
   }
};

// supporting emplace_back and emplace is harder, but probably worth it.
// the trick with both of them is that you only know if they exist or are valid
// after you try to call add_to_container with the arguments!  So instead of
// "does it exist", you end up with "can we call emplace_back with these arguments".
// This requires a bit of slight of hand in the template code, as we want to fall back
// on insert and push_back if emplace_back doesn't exist.

// Another improvement to the above might be to have the const_iterator traits class
// fall back on a decltype( std::begin(std::declval<C const>()) ) -- or even better,
// do full ADL with a private namespace and a using std::begin.

// your code, basically verbatim, but uses container_traits to figure out
// how to add an element to the container:
template<class ResultingContainer, class Range, class Fn>
ResultingContainer map_now(const Range &range, const Fn &f) {
  ResultingContainer result;
  using value_type = decltype(*std::begin(range));
  for_each(std::begin(range), std::end(range), [&](const value_type &v){
    container_traits<ResultingContainer>::add_to_container(result, f(v));
  });
  return result;
}

// could make this easier if I could pass in a pointer-to-template-function
// or the equivalent directly.  Know the syntax by any chance?
template<typename Range, typename Fn>
struct map_lazy_helper {
  template<typename ResultingContainer>
  struct Func {
    ResultingContainer operator()(const Range &range, const Fn &f) const {
      return map_now<ResultingContainer>( range, f );
    }
  };
};

// Map lazy is mostly repeating type information:
template<typename Range, typename Fn>
LazyEvaluation<map_lazy_helper<Range, Fn>::template Func, Range, Fn>
map_lazy(Range&&range, Fn&&f) {
  return {std::forward<Range>(range), std::forward<Fn>(f)};
}

#include <iostream>
#include <vector>
#include <set>

int main() {
   std::vector<int> tester {3,2,1};

   std::vector<double> vd = map_lazy( tester, [](int x) { return x*0.5; } );
   std::set<double> sd = map_lazy( tester, [](int x) { return x*0.5; } );
   std::vector<int> vs = map_lazy( tester, [](int x) { return x*2; } );
   for(auto&& x:vd)
     std::cout << x << "\n";
   for(auto&& x:sd)
     std::cout << x << "\n";
   for(auto&& x:vs)
     std::cout << x << "\n";
}

これはあなたが望むようにうまく機能します。:)

面白いことに、この答えは膨大ですが、その40%は処理する一般的な「コンテナに追加」コードでsetあり、10%はメタプログラミングの定型文であり、30%は一般的なものLazyEvaluationであり、10%はテストコードであり、必要なコードは10%のみです。実際に書くmap_lazy

そして今考えてみると、必要なのは一般的な「コンテナにコンテンツを挿入する」イテレータです。これstd::arrayにより、十分なサイズstd::set、、、std::vectorまたは自作のコンテナへの書き込みをサポートできます。それは、 (空の場合でもサイズがある!)のような固定サイズのコンテナに特化して、それをback_insert_iterator失敗してプローブする必要があります。それはさらに複雑になるでしょう、そしてそれはすでにこの投稿の約半分を占めています!insert_iterator( C.end() )std::arraycontainer_traits

最後に、ADL対応の「コンテナサポートは開始されますか」を次に示します。

namespace aux_adl {
  using std::begin; using std::end;
  template<typename C>
  auto adl_begin( C&& c )->decltype( begin(std::forward<C>(c)) );
  template<typename C>
  auto adl_end( C&& c )->decltype( end(std::forward<C>(c)) );
  template<typename C>
  auto adl_cbegin( C const& c )->decltype( begin(c) );
  template<typename C>
  auto adl_cend( C const& c )->decltype( end(c) );
}
template<typename C, typename=void>
struct container_iterator {}
template<typename C>
struct container_iterator<C, std::enable_if<
  std::is_same<
    decltype( adl_aux::adl_begin(std::declval<C>()),
    decltype( adl_aux::adl_end(std::declval<C>())
  >::value
>::type> {
  typedef adl_aux::adl_begin(std::declval<C>()) iterator;
};
template<typename C>
struct container_const_iterator<C, std::enable_if<
  std::is_same<
    decltype( adl_aux::adl_cbegin(std::declval<C>()),
    decltype( adl_aux::adl_cend(std::declval<C>())
  >::value
>::type> {
  typedef adl_aux::adl_cbegin(std::declval<C>()) const_iterator;
};

adl_endタイプ関数は適切なADLタイプルックアップのみを実行し、実際に呼び出すことはできないことに注意してください。

そして、はい、それはハックです。そして、どうやら誰かがC ++委員会を盗聴して、using内部の宣言がdecltypeこのハックを少し修正できるようにしているようです。

于 2013-03-27T02:07:44.147 に答える