web-dev-qa-db-ja.com

パラメータリストから関数オブジェクトの戻り値の型を推測するにはどうすればよいですか?

_vector<T>_を_vector<R>_に変換できる射影関数を作成しようとしています。次に例を示します。

_auto v = std::vector<int> {1, 2, 3, 4};
auto r1 = select(v, [](int e){return e*e; }); // {1, 4, 9, 16}
auto r2 = select(v, [](int e){return std::to_string(e); }); // {"1", "2", "3", "4"}
_

最初の試み:

_template<typename T, typename R>
std::vector<R> select(std::vector<T> const & c, std::function<R(T)> s)
{
   std::vector<R> v;
   std::transform(std::begin(c), std::end(c), std::back_inserter(v), s);
   return v;
}
_

しかし、

_auto r1 = select(v, [](int e){return e*e; });
_

私は得る:

エラーC2660: '選択':関数は2つの引数を取りません

動作させるには、明示的に_select<int,int>_を呼び出す必要があります。タイプが冗長なので、私はこれが好きではありません。

_auto r1 = select<int, int>(v, [](int e){return e*e; }); // OK
_

2回目の試行:

_template<typename T, typename R, typename Selector>
std::vector<R> select(std::vector<T> const & c, Selector s)
{
   std::vector<R> v;
   std::transform(std::begin(c), std::end(c), std::back_inserter(v), s);
   return v;
}
_

結果は同じエラーです。関数は2つの引数を取りません。この場合、実際には3番目の型引数を指定する必要があります。

_auto r1 = select<int, int, std::function<int(int)>>(v, [](int e){return e*e; });
_

3回目の試み:

_template<typename T, typename R, template<typename, typename> class Selector>
std::vector<R> select(std::vector<T> const & c, Selector<T,R> s)
{
   std::vector<R> v;
   std::transform(std::begin(c), std::end(c), std::back_inserter(v), s);
   return v;
}
_

For

_auto r1 = select<int, int, std::function<int(int)>>(v, [](int e){return e*e; });
_

エラーは次のとおりです。

'select': 'Selector'のテンプレート引数が無効です。クラステンプレートが必要です

For

_auto r1 = select(v, [](int e){return e*e; });
_

エラーC2660: '選択':関数は2つの引数を取りません

(最後の2つの試みは特に素晴らしいものではないことを私は知っています。)

このselect()テンプレート関数を記述して、最初に配置したサンプルコードで機能させるにはどうすればよいですか?

22
Marius Bancila

オプション1:

基本的なdecltype()の使用法:

_template <typename T, typename F>
auto select(const std::vector<T>& c, F f)
    -> std::vector<decltype(f(c[0]))>
{
    using R = decltype(f(c[0]));
    std::vector<R> v;
    std::transform(std::begin(c), std::end(c), std::back_inserter(v), f);
    return v;
}
_

オプション#2:

基本的な_std::result_of<T>_の使用法:

_template <typename T, typename F, typename R = typename std::result_of<F&(T)>::type>
std::vector<R> select(const std::vector<T>& c, F f)
{
    std::vector<R> v;
    std::transform(std::begin(c), std::end(c), std::back_inserter(v), f);
    return v;
}
_

オプション#3:

高度なdecltype()の使用法とperfect-forwarding(注*を参照):

_template <typename T, typename A, typename F>
auto select(const std::vector<T, A>& c, F&& f)
    -> std::vector<typename std::decay<decltype(std::declval<typename std::decay<F>::type&>()(*c.begin()))>::type>
{
    using R = typename std::decay<decltype(std::declval<typename std::decay<F>::type&>()(*c.begin()))>::type;
    std::vector<R> v;
    std::transform(std::begin(c), std::end(c)
                 , std::back_inserter(v)
                 , std::forward<F>(f));
    return v;
}
_

オプション#4:

高度な_std::result_of<T>_の使用法と完全転送(注*を参照):

_template <typename T, typename A, typename F, typename R = typename std::decay<typename std::result_of<typename std::decay<F>::type&(typename std::vector<T, A>::const_reference)>::type>::type>
std::vector<R> select(const std::vector<T, A>& c, F&& f)
{
    std::vector<R> v;
    std::transform(std::begin(c), std::end(c)
                 , std::back_inserter(v)
                 , std::forward<F>(f));
    return v;
}
_

*注:オプション#3および#4は、_std::transform_アルゴリズムが関数オブジェクトby-valueを取ることを前提としています、そしてそれを非定数左辺値として使用します。これが、この奇妙な_typename std::decay<F>::type&_構文を見ることができる理由です。関数オブジェクトがselect関数自体の中で呼び出されることになっていて、結果の型がコンテナーのテンプレート引数として使用されない場合(outer-most_std::decay<T>_が使用されます)、戻り値の型を取得するための正しく移植可能な構文は次のとおりです。

_/*#3*/ using R = decltype(std::forward<F>(f)(*c.begin()));

/*#4*/ typename R = typename std::result_of<F&&(typename std::vector<T, A>::const_reference)>::type
_
28
Piotr Skotnicki

最初の問題は、ラムダが_std::function_であると考えることです。 _std::function_とラムダは無関係なタイプです。 std::function<R(A...)>は、(A)コピー可能、(B)破棄可能、(C)_A..._を使用して呼び出すことができ、Rと互換性のある型を返し、消去するものを変換できる型消去オブジェクトです。タイプに関する他のすべての情報。

これは、それらがそれらのテストに合格する限り、完全に無関係なタイプを消費できることを意味します。

ラムダは、破壊可能でコピー可能な匿名クラスであり(C++ 14を除き、これが時々発生する場合があります)、指定したoperator()があります。これは、ラムダを互換性のあるシグネチャを持つ_std::function_に変換できることが多いことを意味します。

ラムダから_std::function_を推定することは良い考えではありません(それを行う方法はありますが、悪い考えです:C++ 14 autoラムダはそれらを壊し、さらに不必要な非効率になります)。

では、どのように問題を解決しますか?ご覧のとおり、問題は関数オブジェクトとコンテナを取得し、各要素に関数オブジェクトを適用した後にtransformが生成する要素の種類を推測することです。そのため、結果を_std::vector_に格納できます。

これはあなたの問題の解決策に最も近い答えです:

_template<typename T, typename R, typename Selector>
std::vector<R> select(std::vector<T> const & c, Selector s) {
  std::vector<R> v;
  std::transform(std::begin(c), std::end(c), std::back_inserter(v), s);
  return v;
}
_

最も簡単な方法は、テンプレートの順序でTRを交換し、_select<double>_のように呼び出し元にRを明示的に渡すことです。これにより、TSelectorが推定されたままになります。これは理想的ではありませんが、わずかな改善が見られます。

完全なソリューションの場合、このソリューションを修正する方法は2つあります。まず、selectを変更して、_operator std::vector<R>_を持つ一時オブジェクトを返し、そのポイントへの変換を遅らせることができます。これは不完全なスケッチです:

_template<typename T, typename Selector>
struct select_result {
  std::vector<T> const& c;
  Selector s;
  select_result(select_result&&)=default;
  select_result(std::vector<T> const & c_, Selector&& s_):
    c(c_), s(std::forward<Selector>(s_)
  {}
  operator std::vector<R>()&& {
    std::vector<R> v;
    std::transform(std::begin(c), std::end(c), std::back_inserter(v), s);
    return v;
  }
};
template<typename T, typename Selector>
select_result<T, Selector> select(std::vector<T> const & c, Selector&& s) {
  return {c, std::forward<Selector>(s)};
}
_

残念ながら未定義の動作に依存する より滑らかなバージョン を提供することもできます(関数内のローカル参照の参照キャプチャには、標準での有効期間の問題があります)。

しかし、それは_auto v = select_構文を取り除きます-あなたは結果ではなく結果を生み出すものを保存することになります。

あなたはまだstd::vector<double> r = select( in_vec, [](int x){return x*1.5;} );を行うことができ、それはかなりうまく機能します。

基本的に、私は控除を2つのフェーズに分割しました。1つは引数用、もう1つは戻り値用です。

ただし、他にも直接的な方法があるため、そのソリューションに依存する必要はほとんどありません。

2番目のアプローチでは、Rを自分で推測できます。

_template<typename T, typename Selector>
std::vector<typename std::result_of<Selector(T)>::type>
select(std::vector<T> const & c, Selector s) {
  using R = typename std::result_of<Selector(T)>::type;
  std::vector<R> v;
  std::transform(std::begin(c), std::end(c), std::back_inserter(v), s);
  return v;
}
_

これはかなり固溶体です。ちょっとしたクリーンアップ:

_// std::transform takes by-value, then uses an lvalue:
template<class T>
using decayed_lvalue = typename std::decay<T>::type&; 
template<
  typename T, typename A,
  typename Selector,
  typename R=typename std::result_of<decayed_lvalue<Selector>(T)>::type
>
std::vector<R> select(std::vector<T, A> const & c, Selector&& s) {
  std::vector<R> v;
  std::transform(begin(c), end(c), back_inserter(v), std::forward<Selector>(s));
  return v;
}
_

これを実用的なソリューションにします。 (Rtemplateタイプリストに移動し、vectorの代替アロケーターを許可し、不要な_std::_を削除し、Selectorで完全な転送を行いました)。

しかし、もっとうまくやることができます。

入力がvectorであるという事実は、かなり無意味です。

_template<
  typename Range,
  typename Selector,
  typename R=typename std::result_of<Selector(T)>::type
>
std::vector<R> select(Range&& in, Selector&& s) {
  std::vector<R> v;
  using std::begin; using std::end;
  std::transform(begin(in), end(in), back_inserter(v), std::forward<Selector>(s));
  return v;
}
_

まだTを判別できないため、コンパイルされません。それでは、それに取り組みましょう:

_namespace details {
  namespace adl_aux {
    // a namespace where we can do argument dependent lookup on begin and end
    using std::begin; using std::end;
    // no implementation, just used to help with ADL based decltypes:
    template<class R>
    decltype( begin( std::declval<R>() ) ) adl_begin(R&&);
    template<class R>
    decltype( end( std::declval<R>() ) ) adl_end(R&&);
  }
  // pull them into the details namespace:
  using adl_aux::adl_begin;
  using adl_aux::adl_end;
}
// two aliases.  The first takes a Range or Container, and gives
// you the iterator type:
template<class Range>
using iterator = decltype( details::adl_begin( std::declval<Range&>() ) );
// the second is syntactic sugar on top of `std::iterator_traits`:
template<class Iterator>
using value_type = typename std::iterator_traits<Iterator>::value_type;
_

これにより、_iterator<Range>_および_value_type<Iterator>_エイリアスが得られます。一緒にそれらは私達にTを簡単に推測させます:

_// std::transform takes by-value, then uses an lvalue:
template<class T>
using decayed_lvalue = typename std::decay<T>::type&; 

template<
  typename Range,
  typename Selector,
  typename T=value_type<iterator<Range&>>,
  typename R=typename std::result_of<decayed_lvalue<Selector>(T)>::type
>
std::vector<R> select(Range&& in, Selector&& s) {
  std::vector<R> v;
  using std::begin; using std::end;
  std::transform(begin(in), end(in), back_inserter(v), std::forward<Selector>(s));
  return v;
}
_

そして ボブはあなたのおじです 。 (_decayed_lvalue_は、コーナーケースでのSelector型の使用方法を反映し、_iterator<Range&>_は、Rangeの左辺値バージョンからイテレーターを取得していることを反映しています)。

VS2013では、上記のdecltypesがC++ 11の半分の実装を混乱させることがあります。 _iterator<Range>_をdecltype(details::adl_begin(std::declval<Range>()))に置き換えると、その問題を修正できます。

_// std::transform takes by-value, then uses an lvalue:
template<class T>
using decayed_lvalue = typename std::decay<T>::type&; 

template<
  typename Range,
  typename Selector,
  typename T=value_type<decltype(details::adl_begin(std::declval<Range&>()))>,
  typename R=typename std::result_of<decayed_lvalue<Selector>(T)>::type
>
std::vector<R> select(Range&& in, Selector&& s) {
  std::vector<R> v;
  using std::begin; using std::end;
  std::transform(begin(in), end(in), back_inserter(v), std::forward<Selector>(s));
  return v;
}
_

結果の関数は、配列、ベクトル、リスト、マップ、またはカスタムで記述されたコンテナーを受け取り、任意の変換関数を受け取り、結果のタイプのベクトルを生成します。

次のステップは、変換をvectorに直接入れるのではなく、怠惰にすることです。遅延評価を取り除く必要がある場合は、範囲を取得してベクトルに書き込む_as_vector_を使用できます。しかし、それはあなたの問題を解決するのではなく、ライブラリ全体を書くことになります。