“ เปิดออก” tuple เพื่อเรียกตัวชี้ฟังก์ชั่นการจับคู่


254

ฉันพยายามที่จะเก็บในstd::tupleจำนวนที่แตกต่างกันของค่าซึ่งในภายหลังจะใช้เป็นข้อโต้แย้งสำหรับการโทรไปยังตัวชี้ฟังก์ชั่นที่ตรงกับประเภทที่เก็บไว้

ฉันได้สร้างตัวอย่างแบบง่าย ๆ ที่แสดงปัญหาที่ฉันพยายามแก้ไข:

#include <iostream>
#include <tuple>

void f(int a, double b, void* c) {
  std::cout << a << ":" << b << ":" << c << std::endl;
}

template <typename ...Args>
struct save_it_for_later {
  std::tuple<Args...> params;
  void (*func)(Args...);

  void delayed_dispatch() {
     // How can I "unpack" params to call func?
     func(std::get<0>(params), std::get<1>(params), std::get<2>(params));
     // But I *really* don't want to write 20 versions of dispatch so I'd rather 
     // write something like:
     func(params...); // Not legal
  }
};

int main() {
  int a=666;
  double b = -1.234;
  void *c = NULL;

  save_it_for_later<int,double,void*> saved = {
                                 std::tuple<int,double,void*>(a,b,c), f};
  saved.delayed_dispatch();
}

ปกติแล้วสำหรับปัญหาที่เกี่ยวข้องกับstd::tupleหรือเทมเพลตแบบแปรผันฉันจะเขียนเทมเพลตอื่นที่ต้องการtemplate <typename Head, typename ...Tail>ประเมินประเภททั้งหมดทีละครั้ง แต่ฉันไม่เห็นวิธีการทำเช่นนั้นในการส่งการเรียกใช้ฟังก์ชัน

แรงจูงใจที่แท้จริงสำหรับสิ่งนี้ค่อนข้างซับซ้อนและส่วนใหญ่เป็นเพียงการฝึกการเรียนรู้อยู่แล้ว คุณสามารถสันนิษฐานได้ว่าฉันส่ง tuple ด้วยสัญญาจากอินเทอร์เฟซอื่นดังนั้นจึงไม่สามารถเปลี่ยนแปลงได้ กฎนี้ใช้std::bindเป็นวิธีที่ประหยัดเพื่อหลีกเลี่ยงปัญหาพื้นฐาน

อะไรคือวิธีที่สะอาดในการส่งการโทรโดยใช้std::tupleหรือวิธีอื่นที่ดีกว่าในการบรรลุผลลัพธ์สุทธิเดียวกันของการจัดเก็บ / การส่งต่อค่าบางอย่างและตัวชี้ฟังก์ชันจนกว่าจะถึงจุดในอนาคตโดยพลการ


5
ทำไมคุณไม่สามารถเพียงแค่ใช้auto saved = std::bind(f, a, b, c);... จากนั้นก็เพียงโทรsaved()?
Charles Salvia

อินเทอร์เฟซของฉันไม่สามารถควบคุมได้เสมอ ฉันได้รับ tuple โดยสัญญาจากบุคคลอื่นและต้องการทำสิ่งต่าง ๆ กับมันในภายหลัง
เฟล็กโซ

คำตอบ:


275

คุณต้องสร้างชุดพารามิเตอร์ตัวเลขและแกะออกมา

template<int ...>
struct seq { };

template<int N, int ...S>
struct gens : gens<N-1, N-1, S...> { };

template<int ...S>
struct gens<0, S...> {
  typedef seq<S...> type;
};


// ...
  void delayed_dispatch() {
     callFunc(typename gens<sizeof...(Args)>::type());
  }

  template<int ...S>
  void callFunc(seq<S...>) {
     func(std::get<S>(params) ...);
  }
// ...

4
ว้าวฉันไม่ทราบว่าตัวดำเนินการที่เปิดออกมานั้นสามารถใช้ได้อย่างนี้ดีมาก!
Luc Touraille

5
โยฮันเนฉันตระหนักรับ 2+ ปีนับตั้งแต่คุณโพสต์นี้ แต่สิ่งหนึ่งที่ฉันกำลังดิ้นรนกับเป็นstruct gensความหมายทั่วไป (หนึ่งที่สืบทอดมาจากการขยายตัว ที่มาของกล่าวว่าเดียวกัน) ฉันเห็นว่าในที่สุดมันก็เข้าสู่ความชำนาญด้วย 0 หากอารมณ์ที่เหมาะกับคุณและคุณมีวงจรสำรองถ้าคุณสามารถขยายตัวออกไปและวิธีการใช้ประโยชน์จากสิ่งนี้ฉันจะขอบคุณตลอดไป และฉันหวังว่าฉันจะสามารถลงคะแนนนี้ได้ร้อยครั้ง ฉันสนุกกับการเล่นกับแทนเจนต์จากรหัสนี้มากขึ้น ขอบคุณ
WhozCraig

22
@WhozCraig: มันสร้างประเภทseq<0, 1, .., N-1>อะไร มันทำงานอย่างไร: gens<5>: gens<4, 4>: gens<3, 3, 4>: gens<2, 2, 3, 4> : gens<1, 1, 2, 3, 4> : gens<0, 0, 1, 2, 3, 4>. seq<0, 1, 2, 3, 4>ประเภทสุดท้ายเป็นผู้เชี่ยวชาญในการสร้าง เคล็ดลับฉลาด ๆ
mindvirus

2
@NirFriedman: แน่นอนแค่แทนที่รุ่นที่ไม่ได้กำหนดgensโดย:template <int N, int... S> struct gens { typedef typename gens<N-1, N-1, S...>::type type; };
marton78

11
มันคุ้มค่าที่จะสะท้อนคำตอบและความคิดเห็นของวอลเตอร์: ชาวบ้านไม่จำเป็นต้องคิดค้นล้อของตัวเองอีกต่อไป สร้างลำดับเป็นเพื่อร่วมกันว่ามันเป็นมาตรฐานใน C ++ 14 std::integer_sequence<T, N>และความเชี่ยวชาญดังกล่าวสำหรับstd::size_t, std::index_sequence<N>- บวกฟังก์ชั่นช่วยที่เกี่ยวข้องของพวกเขาและstd::make_in(teger|dex)_sequence<>() std::index_sequence_for<Ts...>()และใน C ++ 17 มีสิ่งดีๆอีกมากมายที่รวมเข้ากับไลบรารี - โดยเฉพาะอย่างยิ่งรวมถึงstd::applyและstd::make_from_tupleซึ่งจะจัดการบิตการเปิดและการเรียกบิต
underscore_d

61

โซลูชัน C ++ 17 ใช้งานง่ายstd::apply:

auto f = [](int a, double b, std::string c) { std::cout<<a<<" "<<b<<" "<<c<< std::endl; };
auto params = std::make_tuple(1,2.0,"Hello");
std::apply(f, params);

เพิ่งรู้สึกว่าควรระบุหนึ่งครั้งในคำตอบในกระทู้นี้ (หลังจากที่ปรากฏในความคิดเห็น)


โซลูชัน C ++ 14 พื้นฐานยังคงขาดหายไปในชุดข้อความนี้ แก้ไข: ไม่มันมีอยู่จริงในคำตอบของวอลเตอร์

ฟังก์ชั่นนี้ได้รับ:

void f(int a, double b, void* c)
{
      std::cout << a << ":" << b << ":" << c << std::endl;
}

เรียกว่าด้วยตัวอย่างต่อไปนี้:

template<typename Function, typename Tuple, size_t ... I>
auto call(Function f, Tuple t, std::index_sequence<I ...>)
{
     return f(std::get<I>(t) ...);
}

template<typename Function, typename Tuple>
auto call(Function f, Tuple t)
{
    static constexpr auto size = std::tuple_size<Tuple>::value;
    return call(f, t, std::make_index_sequence<size>{});
}

ตัวอย่าง:

int main()
{
    std::tuple<int, double, int*> t;
    //or std::array<int, 3> t;
    //or std::pair<int, double> t;
    call(f, t);    
}

การสาธิต


ฉันไม่สามารถรับการสาธิตนี้เพื่อทำงานร่วมกับพอยน์เตอร์พอยน์เตอร์ - นี่คืออะไร http://coliru.stacked-crooked.com/a/8ea8bcc878efc3cb
Xeverous

@ Xeverous: คุณต้องการได้รับสิ่งนี้ที่นี่หรือไม่
davidhigh

ขอบคุณฉันมี 2 คำถาม: 1. ทำไมฉันไม่สามารถผ่านstd::make_uniqueได้โดยตรง มันต้องการอินสแตนซ์ของฟังก์ชันที่เป็นรูปธรรมหรือไม่? 2. ทำไมstd::move(ts)...ถ้าเราสามารถเปลี่ยน[](auto... ts)เป็น[](auto&&... ts)?
Xeverous

@ Xeverous: 1. ไม่ทำงานจากลายเซ็นต์: คุณstd::make_uniqueคาดหวังกับ tuple และ tuple สามารถสร้างได้จาก tuple ที่ยังไม่ได้แพ็คผ่านการเรียกอีกครั้งstd::make_tupleเท่านั้น นี่คือสิ่งที่ฉันทำในแลมบ์ดา (แม้ว่ามันจะซ้ำซ้อนสูงในขณะที่คุณสามารถคัดลอกทูเปิลลงในตัวชี้ที่ไม่ซ้ำกันโดยไม่ต้องใช้call)
davidhigh

1
ตอนนี้ควรจะคำตอบ
Fureeish

44

นี่เป็นคำตอบที่สมบูรณ์แบบของโจฮันเนสในการแก้ปัญหาของ awoodland ด้วยความหวังว่ามันจะมีประโยชน์กับบางคน สิ่งนี้ถูกทดสอบด้วยสแนปชอตของ g ++ 4.7 ใน Debian squeeze

###################
johannes.cc
###################
#include <tuple>
#include <iostream>
using std::cout;
using std::endl;

template<int ...> struct seq {};

template<int N, int ...S> struct gens : gens<N-1, N-1, S...> {};

template<int ...S> struct gens<0, S...>{ typedef seq<S...> type; };

double foo(int x, float y, double z)
{
  return x + y + z;
}

template <typename ...Args>
struct save_it_for_later
{
  std::tuple<Args...> params;
  double (*func)(Args...);

  double delayed_dispatch()
  {
    return callFunc(typename gens<sizeof...(Args)>::type());
  }

  template<int ...S>
  double callFunc(seq<S...>)
  {
    return func(std::get<S>(params) ...);
  }
};

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
#pragma GCC diagnostic ignored "-Wunused-variable"
#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
int main(void)
{
  gens<10> g;
  gens<10>::type s;
  std::tuple<int, float, double> t = std::make_tuple(1, 1.2, 5);
  save_it_for_later<int,float, double> saved = {t, foo};
  cout << saved.delayed_dispatch() << endl;
}
#pragma GCC diagnostic pop

หนึ่งสามารถใช้ไฟล์ SConstruct ต่อไปนี้

#####################
SConstruct
#####################
#!/usr/bin/python

env = Environment(CXX="g++-4.7", CXXFLAGS="-Wall -Werror -g -O3 -std=c++11")
env.Program(target="johannes", source=["johannes.cc"])

บนเครื่องของฉันมันให้

g++-4.7 -o johannes.o -c -Wall -Werror -g -O3 -std=c++11 johannes.cc
g++-4.7 -o johannes johannes.o

ทำไมคุณถึงต้องการตัวแปร s และ g
shoosh

@ shoosh ฉันคิดว่าพวกเขาไม่ต้องการ ฉันลืมว่าทำไมฉันจึงเพิ่มสิ่งเหล่านั้น เกือบสามปีแล้ว แต่ฉันคิดว่าเพื่อแสดงให้เห็นว่าการเริ่มต้นทำงานได้ดี
Faheem Mitha

42

นี่คือโซลูชัน C ++ 14

template <typename ...Args>
struct save_it_for_later
{
  std::tuple<Args...> params;
  void (*func)(Args...);

  template<std::size_t ...I>
  void call_func(std::index_sequence<I...>)
  { func(std::get<I>(params)...); }
  void delayed_dispatch()
  { call_func(std::index_sequence_for<Args...>{}); }
};

สิ่งนี้ยังคงต้องการฟังก์ชันตัวช่วยหนึ่งตัว ( call_func) เนื่องจากนี่เป็นสำนวนทั่วไปบางทีมาตรฐานควรสนับสนุนโดยตรงเช่นเดียวstd::callกับการใช้งานที่เป็นไปได้

// helper class
template<typename R, template<typename...> class Params, typename... Args, std::size_t... I>
R call_helper(std::function<R(Args...)> const&func, Params<Args...> const&params, std::index_sequence<I...>)
{ return func(std::get<I>(params)...); }

// "return func(params...)"
template<typename R, template<typename...> class Params, typename... Args>
R call(std::function<R(Args...)> const&func, Params<Args...> const&params)
{ return call_helper(func,params,std::index_sequence_for<Args...>{}); }

จากนั้นการจัดส่งล่าช้าของเรากลายเป็น

template <typename ...Args>
struct save_it_for_later
{
  std::tuple<Args...> params;
  std::function<void(Args...)> func;
  void delayed_dispatch()
  { std::call(func,params); }
};

8
upvoted สำหรับ (เสนอ) std::callการดำเนินงานของ C ++ 14 สวนสัตว์วุ่นวายของinteger_sequenceและindex_sequenceประเภทผู้ช่วยที่จะมีการอธิบายที่นี่: en.cppreference.com/w/cpp/utility/integer_sequenceหนังสือขาดที่เห็นได้ชัดเจนของซึ่งเป็นเหตุผลที่วอลเตอร์ถูกบังคับให้ไวยากรณ์std::make_index_sequence(Args...) clunkier std::index_sequence_for<Args...>{}
Quuxplusone

3
และเห็นได้ชัดว่าได้รับการโหวตใน C ++ 17 ตั้งแต่ 3/2016 ในฐานะ std :: Apply (func, tup): en.cppreference.com/w/cpp/utility/apply
ddevienne

18

นี่เป็นบิตที่ซับซ้อนเพื่อให้บรรลุ (แม้ว่าจะเป็นไปได้) ผมแนะนำให้คุณใช้ห้องสมุดที่นี้จะดำเนินการแล้วกล่าวคือBoost.Fusion (คนเรียกใช้ฟังก์ชั่น) โบนัสฟิวชั่น Boost ทำงานได้กับคอมไพเลอร์ C ++ 03 เช่นกัน


7

สารละลาย. ครั้งแรกบางส่วนสำเร็จรูปสำเร็จรูป:

template<std::size_t...Is>
auto index_over(std::index_sequence<Is...>){
  return [](auto&&f)->decltype(auto){
    return decltype(f)(f)( std::integral_constant<std::size_t, Is>{}... );
  };
}
template<std::size_t N>
auto index_upto(std::integral_constant<std::size_t, N> ={}){
  return index_over( std::make_index_sequence<N>{} );
}

สิ่งเหล่านี้ให้คุณเรียกแลมบ์ดาที่มีชุดจำนวนเต็มเวลารวบรวม

void delayed_dispatch() {
  auto indexer = index_upto<sizeof...(Args)>();
  indexer([&](auto...Is){
    func(std::get<Is>(params)...);
  });
}

และเราทำเสร็จแล้ว

index_uptoและindex_overให้คุณทำงานกับชุดพารามิเตอร์โดยไม่ต้องสร้างการโอเวอร์โหลดภายนอกใหม่

แน่นอนค่ะ คุณเพียงแค่

void delayed_dispatch() {
  std::apply( func, params );
}

ตอนนี้ถ้าเราชอบสิ่งนั้นใน เราสามารถเขียน:

namespace notstd {
  template<class T>
  constexpr auto tuple_size_v = std::tuple_size<T>::value;
  template<class F, class Tuple>
  decltype(auto) apply( F&& f, Tuple&& tup ) {
    auto indexer = index_upto<
      tuple_size_v<std::remove_reference_t<Tuple>>
    >();
    return indexer(
      [&](auto...Is)->decltype(auto) {
        return std::forward<F>(f)(
          std::get<Is>(std::forward<Tuple>(tup))...
        );
      }
    );
  }
}

ค่อนข้างง่ายและทำความสะอาด ไวยากรณ์พร้อมที่จะจัดส่ง

void delayed_dispatch() {
  notstd::apply( func, params );
}

เพียงแทนที่notstdด้วยstdเมื่ออัพเกรดคอมไพเลอร์และบ๊อบของคุณลุงของคุณ


std::apply<- ดนตรีที่หูของฉัน
เฟล็กโซ

@Flexo สั้นกว่าindex_uptoและยืดหยุ่นน้อยกว่าเล็กน้อย ;) ลองโทรfuncโดยใช้อาร์กิวเมนต์ย้อนหลังด้วยindex_uptoและstd::applyตามลำดับ เป็นที่ยอมรับว่าใครอยากจะเรียกฟังก์ชั่นจาก tuple ไปข้างหลัง
Yakk - Adam Nevraumont

จุดเล็กน้อย: std::tuple_size_vคือ C ++ 17 ดังนั้นสำหรับโซลูชัน C ++ 14 ที่จะต้องแทนที่ด้วยtypename std::tuple_size<foo>::value
basteln

@basteln ฉันหวังว่าvalueจะไม่ใช่ประเภท แต่คงอยู่ แต่อย่างใด
Yakk - Adam Nevraumont

@Yakk sizeof...(Types)ไม่มัน typenameผมชอบวิธีการแก้ปัญหาของคุณได้โดยไม่ต้อง
basteln

3

คิดเกี่ยวกับปัญหาบางอย่างขึ้นอยู่กับคำตอบให้ฉันได้พบวิธีอื่นในการแก้ปัญหาเดียวกัน:

template <int N, int M, typename D>
struct call_or_recurse;

template <typename ...Types>
struct dispatcher {
  template <typename F, typename ...Args>
  static void impl(F f, const std::tuple<Types...>& params, Args... args) {
     call_or_recurse<sizeof...(Args), sizeof...(Types), dispatcher<Types...> >::call(f, params, args...);
  }
};

template <int N, int M, typename D>
struct call_or_recurse {
  // recurse again
  template <typename F, typename T, typename ...Args>
  static void call(F f, const T& t, Args... args) {
     D::template impl(f, t, std::get<M-(N+1)>(t), args...);
  }
};

template <int N, typename D>
struct call_or_recurse<N,N,D> {
  // do the call
  template <typename F, typename T, typename ...Args>
  static void call(F f, const T&, Args... args) {
     f(args...);
  }
};

ซึ่งต้องการเปลี่ยนการใช้งานของdelayed_dispatch()เป็น:

  void delayed_dispatch() {
     dispatcher<Args...>::impl(func, params);
  }

สิ่งนี้ทำงานโดยการแปลงซ้ำstd::tupleเป็นแพ็คพารามิเตอร์ในสิทธิของตนเอง call_or_recurseจำเป็นต้องใช้เป็นความเชี่ยวชาญในการยกเลิกการสอบถามซ้ำด้วยการโทรจริงซึ่งเพียงปลดแพ็คพารามิเตอร์ที่สมบูรณ์

ฉันไม่แน่ใจว่านี่เป็นวิธีที่ดีกว่า แต่ก็เป็นอีกวิธีหนึ่งในการคิดและแก้ไข


ในฐานะที่เป็นโซลูชันทางเลือกอื่นคุณสามารถใช้enable_ifเพื่อสร้างสิ่งที่ง่ายกว่าโซลูชันก่อนหน้าของฉัน:

#include <iostream>
#include <functional>
#include <tuple>

void f(int a, double b, void* c) {
  std::cout << a << ":" << b << ":" << c << std::endl;
}

template <typename ...Args>
struct save_it_for_later {
  std::tuple<Args...> params;
  void (*func)(Args...);

  template <typename ...Actual>
  typename std::enable_if<sizeof...(Actual) != sizeof...(Args)>::type
  delayed_dispatch(Actual&& ...a) {
    delayed_dispatch(std::forward<Actual>(a)..., std::get<sizeof...(Actual)>(params));
  }

  void delayed_dispatch(Args ...args) {
    func(args...);
  }
};

int main() {
  int a=666;
  double b = -1.234;
  void *c = NULL;

  save_it_for_later<int,double,void*> saved = {
                                 std::tuple<int,double,void*>(a,b,c), f};
  saved.delayed_dispatch();
}

เกินพิกัดตัวแรกจะรับอาร์กิวเมนต์อีกหนึ่งตัวจาก tuple และวางลงในชุดพารามิเตอร์ การโอเวอร์โหลดครั้งที่สองใช้ชุดพารามิเตอร์ที่ตรงกันจากนั้นทำการโทรจริงโดยการโอเวอร์โหลดครั้งแรกจะถูกปิดใช้งานในกรณีเดียวและกรณีเดียวที่กรณีที่สองจะทำงานได้


1
ฉันทำงานกับบางสิ่งบางอย่างที่คล้ายกันมากกับเรื่องนี้สักพัก ถ้าฉันมีเวลาฉันจะไปดูครั้งที่สองแล้วดูว่ามันเปรียบเทียบกับคำตอบปัจจุบันได้อย่างไร
Michael Price

@MichaelPrice - หมดจดจากมุมมองการเรียนรู้ฉันสนใจที่จะเห็นทางเลือกอื่น ๆ ที่ไม่ได้ต้มลงไปในการแฮ็คที่น่ากลัวบางอย่างในขณะที่กำลังพุดตัวชี้สแต็ก
เฟล็กโซ

2

ความหลากหลายของโซลูชันของฉันจาก Johannes ใช้ C ++ 14 std :: index_sequence (และฟังก์ชันส่งคืนชนิดเป็นพารามิเตอร์เทมเพลต RetT):

template <typename RetT, typename ...Args>
struct save_it_for_later
{
    RetT (*func)(Args...);
    std::tuple<Args...> params;

    save_it_for_later(RetT (*f)(Args...), std::tuple<Args...> par) : func { f }, params { par } {}

    RetT delayed_dispatch()
    {
        return callFunc(std::index_sequence_for<Args...>{});
    }

    template<std::size_t... Is>
    RetT callFunc(std::index_sequence<Is...>)
    {
        return func(std::get<Is>(params) ...);
    }
};

double foo(int x, float y, double z)
{
  return x + y + z;
}

int testTuple(void)
{
  std::tuple<int, float, double> t = std::make_tuple(1, 1.2, 5);
  save_it_for_later<double, int, float, double> saved (&foo, t);
  cout << saved.delayed_dispatch() << endl;
  return 0;
}

การแก้ปัญหาทั้งหมดนั้นอาจช่วยแก้ปัญหาเบื้องต้นได้ แต่โดยความจริงแล้วสิ่งที่แม่แบบนี้ไม่ได้เป็นไปในทิศทางที่ผิด - ในแง่ของความเรียบง่ายและการบำรุงรักษา ?
xy

ฉันคิดว่าเทมเพลตนั้นดีขึ้นและเข้าใจได้มากขึ้นด้วย C ++ 11 และ 14 เมื่อไม่กี่ปีที่ผ่านมาเมื่อฉันดูว่าเทมเพลตอะไรที่เพิ่มประสิทธิภาพให้กับเทมเพลตฉันรู้สึกท้อแท้จริงๆ ฉันยอมรับว่าการพัฒนาเทมเพลตที่ดีนั้นยากกว่าการใช้เทมเพลต
schwart

1
@xy ประการแรกในแง่ของความซับซ้อนแม่แบบนี้เป็นอะไร ประการที่สองเทมเพลตผู้ช่วยส่วนใหญ่เป็นการลงทุนเริ่มต้นสำหรับประหยัดเวลาหนึ่งตันเมื่อสร้างอินสแตนซ์เหล่านั้นในภายหลัง สุดท้ายสิ่งที่คุณจะค่อนข้างไม่มีความสามารถในการทำสิ่งที่แม่แบบอนุญาตให้คุณทำ? คุณไม่สามารถใช้มันได้และไม่แสดงความคิดเห็นที่ไม่เกี่ยวข้องซึ่งดูเหมือนว่าจะทำการตรวจสอบโปรแกรมเมอร์คนอื่น ๆ
underscore_d
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.