简体   繁体   中英

template function with corresponding parameters to subset of tuple types

I would like to write function as this find :

multi_set<int, string, double, myType> m; //vector of tuples
m.insert(/*some data*/);
m.find<1,2>("something",2.123);

Or

m.find<0,3>(1,instanceOfMyType);
m.find<1>("somethingelse");

Where find can be parametrized corresponding to any subset of tuple parameters.

My code so far:

template <typename ... T>
class multi_set{
    typedef  tuple < T... > Tuple;
    vector<tuple<T...>> data = vector<tuple<T...>>();

public:
    void insert(T... t){
        data.push_back(tuple<T...>(t...));
    }


    template<size_t ... Pos>
    void find(???){
    // then I would like to use those params to search through data and 
    // return first matching item
    }
}
// test whether a particular tuple is a match
template<size_t... Pos>
static bool is_match(const Tuple& tuple, const typename std::tuple_element<Pos, Tuple>::type &... args) {
    std::initializer_list<bool> results = { (std::get<Pos>(tuple) == args)... };
    return std::all_of(results.begin(), results.end(), [](bool p) { return p; });
}

// Find the first one that is a match.
template<size_t... Pos>
typename vector<Tuple>::const_iterator find(const typename std::tuple_element<Pos, Tuple>::type &... args) const {
    return std::find_if(data.begin(), data.end(), [&](const Tuple & tup) { return is_match<Pos...>(tup, args...); });
}

It's also possible to have find take a type parameter pack and perfectly forward, rather than taking fixed types with tuple_element . The benefit is that you can avoid an unnecessary conversion if == is transparent. The cost is that you can't take anything that can't be perfectly forwarded any more (eg, braced initializer lists, 0 as a null pointer constant). A side benefit appears to be that MSVC 2013 doesn't choke on this version:

// test whether a particular tuple is a match
template<size_t... Pos, class... Args>
static bool is_match(const Tuple& tuple, Args&&... args) {
    std::initializer_list<bool> results = { (std::get<Pos>(tuple) == std::forward<Args>(args))... };
    return std::all_of(results.begin(), results.end(), [](bool p) { return p; });
}

// Find the first one that is a match.
template<size_t... Pos, class... Args>
typename vector<Tuple>::const_iterator find(Args&&... args) const {
    return std::find_if(data.begin(), data.end(), [&](const Tuple & tup) { return is_match<Pos...>(tup, std::forward<Args>(args)...); });
}

You should look into boost::multi_index . It is very close to what you are looking for.

http://www.boost.org/doc/libs/1_54_0/libs/multi_index/doc/tutorial/index.html

This is a function that takes a seed value, and a set of lambdas. It feeds that seed value through each of the lambdas in turn:

template<class... Fs, class R>
R chain( R r, Fs&&... fs ) {
  using in_order = int[];
  (void)(in_order{0,
    (
      (r = std::forward<Fs>(fs)( r ))
      , void(), 0
    )...
  });
  return r;
}

Inside your class, we use the above:

template<size_t... Pos, class...Us>
typename std::vector<Tuple>::const_iterator
find(Us const&... us) const {
  return std::find_if(
    data.begin(), data.end(),
    [&](const Tuple & tup) {
      return chain(
        true,
        [&](bool old){
          return old && (std::get<Pos>(tup) == us);
        }...
      );
    }
  );
}

this compiles in clang, but not g++ 4.9.2 -- g++ doesn't like parameter packs inside lambdas.

Note the fact we take Us const&... -- this allows for transparent == , which is important in some cases. std::string == char const* is a classic example, where if you force find to take the same value as in the tuple, you'll force a needless allocation in calling find .


In C++1z, the chain call can be replaced with:

( ... && (std::get<Pos>(tup) == us) )

which is conceptually identical, but much easier to read. This is known as a "fold expression".


Now, a problem with the above is that it uses forwarding references, which causes imperfect forwarding problems of perfect forwarding.

The most annoying of which is the inability to use {} to construct arguments.

If we use matching types, we instead force non-transparent comparison, which can be expensive (examine std::string compared to "hello this is ac string" -- it causes possibly allocation if we force the c string into a std::string .)

A way around this is to type erase down to the concept of equality with a given type .

template<class...>struct voider{using type=void;};
template<class...Ts>using void_t=typename voider<Ts...>::type;
template<class T>struct tag{using type=T;};

template<class...>struct types{using type=types;};

template<class T>
using block_deduction = typename tag<T>::type;

template<class F, class Sig, class T=void>
struct erase_view_op;

template<class F, class R, class...Ts, class T>
struct erase_view_op<F, R(Ts...), T>
{
  using fptr = R(*)(void const*, Ts&&...);

  fptr f;
  void const* ptr;

private:
  template<class U>
  erase_view_op(U&& u, int):
    f([](void const* p, Ts&&...ts)->R{
      U& u = reinterpret_cast<U&>( *static_cast<std::decay_t<U>*>(const_cast<void*>(p)) );
      return F{}( u, std::forward<Ts>(ts)... );
    }),
    ptr( static_cast<void const*>(std::addressof(u)) )
  {}
public:
  template<class U, class=std::enable_if_t< !std::is_same<std::decay_t<U>,erase_view_op>{} && std::is_convertible< std::result_of_t<F(U,Ts...)>, R >{} >>
  erase_view_op(U&& u):erase_view_op( std::forward<U>(u), 0 ){}

  template<class U=T, class=std::enable_if_t< !std::is_same<U, void>{} >>
  erase_view_op( block_deduction<U>&& u ):erase_view_op( std::move(u), 0 ){}

  erase_view_op( erase_view_op const& ) = default;
  erase_view_op( erase_view_op&& ) = default;

  R operator()( Ts... ts ) const {
    return f( ptr, std::forward<Ts>(ts)... );
  }
};

struct equality {
  template<class lhs, class rhs>
  bool operator()(lhs const& l, rhs const& r)const {
    return l==r;
  }
};
template<class T>
using erase_equal_to = erase_view_op< equality, bool(T const&), T >;
using string_equal_to = erase_equal_to< std::string >;

int main() {
  static_assert( std::is_same< bool, std::result_of_t< std::equal_to<>(decltype("hello"), std::string const&) > >{}, "hmm" );
  string_equal_to s = "hello";
  string_equal_to s2 = {{"hello"}};
  (void)s2;
  std::string x = "hello";
  std::string y = "jello";
  std::cout << s(x) << s(y) << '\n';
}

then we rewrite find :

template<size_t... Pos>
typename std::vector<Tuple>::const_iterator
find(erase_equal_to< std::remove_reference_t<std::tuple_element_t<Pos, Tuple>> >... us) const {
  return std::find_if(
    data.begin(), data.end(),
    [&](const Tuple & tup) {
      return chain(
        true,
        [&](bool old){
          return old && us(std::get<Pos>(tup));
        }...
      );
    }
  );
}

which does both transparent equality and allows {} based construction (well, it does require {{}} based construction -- the outer to say we are constructing the eraser, the inner to construct the T ).

the signature of your find function would be

template<size_t ... Pos, typename ... Types> void find(Types... &t)

the implementation of the search is up to you, you can use recursive templates or looping over parameter packs

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM