Không thể giải nén được một bộ dữ liệu để gọi một con trỏ hàm phù hợp


254

Tôi đang cố lưu trữ một std::tuplesố lượng giá trị khác nhau, sau này sẽ được sử dụng làm đối số cho một cuộc gọi đến một con trỏ hàm khớp với các loại được lưu trữ.

Tôi đã tạo một ví dụ đơn giản hóa cho thấy vấn đề tôi đang đấu tranh để giải quyết:

#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();
}

Thông thường đối với các vấn đề liên quan đến std::tuplehoặc các mẫu biến đổi, tôi sẽ viết một mẫu khác template <typename Head, typename ...Tail>để đánh giá đệ quy tất cả các loại từng cái một, nhưng tôi không thể thấy cách thực hiện điều đó để gửi một lệnh gọi hàm.

Động lực thực sự cho việc này có phần phức tạp hơn và dù sao nó cũng chỉ là một bài tập học tập. Bạn có thể giả sử rằng tôi đã trao bộ dữ liệu bằng hợp đồng từ một giao diện khác, vì vậy không thể thay đổi nhưng mong muốn giải nén nó thành một cuộc gọi chức năng là của tôi. Điều này loại trừ việc sử dụng std::bindnhư một cách rẻ tiền để vượt qua vấn đề tiềm ẩn.

Cách tốt nhất để gửi cuộc gọi bằng cách sử dụng std::tuplehoặc cách khác tốt hơn để đạt được cùng một kết quả ròng là lưu trữ / chuyển tiếp một số giá trị và con trỏ hàm cho đến một điểm tương lai tùy ý?


5
Tại sao bạn không thể sử dụng auto saved = std::bind(f, a, b, c);... sau đó chỉ cần gọi saved()?
Charles Salvia

Không phải lúc nào giao diện của tôi để kiểm soát. Tôi nhận được một tuple bằng hợp đồng từ người khác và muốn làm mọi thứ với nó sau đó.
Flexo

Câu trả lời:


275

Bạn cần xây dựng một gói tham số gồm các số và giải nén chúng

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
Ồ, tôi không biết toán tử giải nén có thể được sử dụng như thế, điều này thật tuyệt!
Luc Touraille

5
Julian, tôi nhận ra đã hơn 2 năm kể từ khi bạn đăng bài này, nhưng điều duy nhất tôi đang đấu tranh là struct gensđịnh nghĩa chung (cái được thừa hưởng từ một dẫn xuất mở rộng đã nói như vậy). Tôi thấy nó cuối cùng đạt đến chuyên môn với 0. Nếu tâm trạng phù hợp với bạn và bạn có chu kỳ dự phòng, nếu bạn có thể mở rộng về điều đó, và nó được sử dụng như thế nào cho điều này, tôi sẽ biết ơn mãi mãi. Và tôi ước tôi có thể bỏ phiếu này hàng trăm lần. Tôi đã có nhiều niềm vui hơn khi chơi với các tiếp tuyến từ mã này. Cảm ơn.
WhozCraig

22
@WhozCraig: Những gì nó làm là tạo ra một loại seq<0, 1, .., N-1>. Cách thức hoạt động : 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>. Loại cuối cùng là chuyên ngành, tạo ra seq<0, 1, 2, 3, 4>. Thủ thuật khá thông minh.
mindvirus

2
@NirFriedman: Chắc chắn, chỉ cần thay thế phiên bản không chuyên biệt của gens:template <int N, int... S> struct gens { typedef typename gens<N-1, N-1, S...>::type type; };
marton78

11
Thật đáng để lặp lại câu trả lời và bình luận của Walter trên đó: dân gian không cần phải phát minh ra bánh xe của riêng họ nữa. Việc tạo ra một chuỗi là phổ biến đến mức nó đã được chuẩn hóa trong C ++ 14 std::integer_sequence<T, N>và chuyên môn hóa cho nó std::size_t, std::index_sequence<N>- cộng với các chức năng trợ giúp liên quan của chúng std::make_in(teger|dex)_sequence<>()std::index_sequence_for<Ts...>(). Và trong C ++ 17, có rất nhiều thứ hay ho khác được tích hợp vào thư viện - đặc biệt bao gồm std::applystd::make_from_tuple, sẽ xử lý việc giải nén và gọi bit
underscore_d

61

Giải pháp C ++ 17 chỉ đơn giản là sử dụng 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);

Chỉ cảm thấy rằng nên được nêu một lần trong một câu trả lời trong chủ đề này (sau khi nó đã xuất hiện trong một trong các ý kiến).


Giải pháp C ++ 14 cơ bản vẫn còn thiếu trong chủ đề này. EDIT: Không, nó thực sự có trong câu trả lời của Walter.

Chức năng này được đưa ra:

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

Gọi nó với đoạn mã sau:

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>{});
}

Thí dụ:

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

BẢN GIỚI THIỆU


Tôi không thể có bản demo này để làm việc với con trỏ thông minh - có gì sai ở đây? http://coliru.stacked-crooked.com/a/8ea8bcc878efc3cb
Xeverous

@Xeverous: bạn có muốn nhận được một cái gì đó như thế này ở đây ?
davidhigh

cảm ơn, tôi có 2 câu hỏi: 1. Tại sao tôi không thể vượt qua std::make_uniquetrực tiếp? Có cần ví dụ chức năng cụ thể? 2. Tại sao std::move(ts)...nếu chúng ta có thể thay đổi [](auto... ts)thành [](auto&&... ts)?
Xeverous

@Xeverous: 1. không hoạt động từ các chữ ký: bạn std::make_uniquemong đợi một tuple và một tuple có thể được tạo từ một tuple chưa giải nén chỉ thông qua một cuộc gọi khác std::make_tuple. Đây là những gì tôi đã thực hiện trong lambda (mặc dù nó rất dư thừa, vì bạn cũng có thể chỉ cần sao chép bộ dữ liệu vào con trỏ duy nhất mà không cần sử dụng call).
davidhigh

1
Bây giờ điều này nên các câu trả lời.
Fureeish

44

Đây là phiên bản hoàn chỉnh có thể biên dịch được của giải pháp của Julian đối với câu hỏi của awoodland, với hy vọng nó có thể hữu ích cho ai đó. Điều này đã được thử nghiệm với ảnh chụp nhanh g ++ 4.7 trên nén Debian.

###################
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

Người ta có thể sử dụng tệp SConstruct sau đây

#####################
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"])

Trên máy của tôi, cái này cho

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

Tại sao bạn cần biến s và g?
shoosh

@shoosh Tôi đoán họ không cần thiết. Tôi quên tại sao tôi thêm những thứ đó; đã gần ba năm. Nhưng tôi cho rằng, để cho thấy rằng việc khởi tạo hoạt động.
Faheem Mitha

42

Đây là một giải pháp 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...>{}); }
};

Điều này vẫn cần một hàm trợ giúp ( call_func). Vì đây là một thành ngữ phổ biến, có lẽ tiêu chuẩn nên hỗ trợ trực tiếp khi std::callcó thể thực hiện

// 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...>{}); }

Sau đó công văn bị trì hoãn của chúng tôi trở thành

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
Nâng cao cho việc thực hiện (đề xuất) của std::call. Sở thú integer_sequenceindex_sequencecác loại người trợ giúp hỗn loạn của C ++ 14 được giải thích ở đây: en.cppreference.com/w/cpp/utility/integer_ resultence Lưu ý sự vắng mặt dễ thấy của std::make_index_sequence(Args...), đó là lý do Walter buộc phải sử dụng cú pháp clunkier std::index_sequence_for<Args...>{}.
Quuxplusone

3
Và rõ ràng đã bình chọn vào C ++ 17 kể từ 3/2016 dưới dạng std :: áp dụng (func, tup
ddevienne

18

Điều này hơi phức tạp để đạt được (mặc dù có thể). Tôi khuyên bạn nên sử dụng một thư viện nơi việc này đã được triển khai, cụ thể là Boost.Fusion ( chức năng gọi ). Là một phần thưởng, Boost Fusion cũng hoạt động với trình biên dịch C ++ 03.


7

giải pháp. Đầu tiên, một số nồi hơi tiện ích:

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>{} );
}

Chúng cho phép bạn gọi một lambda với một loạt các số nguyên thời gian biên dịch.

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

và chúng ta đã xong.

index_uptoindex_overcho phép bạn làm việc với các gói tham số mà không phải tạo ra quá tải bên ngoài mới.

Tất nhiên, trong bạn chỉ

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

Bây giờ, nếu chúng ta thích điều đó, trong chúng tôi có thể viết:

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))...
        );
      }
    );
  }
}

tương đối dễ dàng và có được chất tẩy rửa cú pháp sẵn sàng để vận chuyển.

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

chỉ cần thay thế notstdvới stdkhi nâng cấp trình biên dịch của bạn và bob là chú của bạn.


std::apply<- nhạc đến tai tôi
Flexo

@Flexo Chỉ ngắn hơn một chút index_uptovà kém linh hoạt. ;) Hãy thử gọi funcvới các đối số ngược index_uptostd::applytương ứng. Phải thừa nhận rằng, ai đang muốn gọi một hàm từ một tuple ngược.
Yakk - Adam Nevraumont

Điểm nhỏ: std::tuple_size_vlà C ++ 17, do đó, đối với giải pháp C ++ 14 sẽ phải được thay thế bằngtypename std::tuple_size<foo>::value
basteln

@basteln Tôi hy vọng valuekhông phải là một loại. Nhưng dù sao cũng đã sửa.
Yakk - Adam Nevraumont

@Yakk Không, nó sizeof...(Types). Tôi thích giải pháp của bạn mà không có typename.
basteln

3

Suy nghĩ về vấn đề thêm một số dựa trên câu trả lời tôi đã tìm thấy một cách khác để giải quyết vấn đề tương tự:

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...);
  }
};

Điều này đòi hỏi phải thay đổi việc thực hiện delayed_dispatch()thành:

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

Điều này hoạt động bằng cách chuyển đổi đệ quy std::tuplethành một gói tham số theo cách riêng của mình. call_or_recurselà cần thiết như một chuyên môn hóa để chấm dứt đệ quy với cuộc gọi thực, chỉ cần giải nén gói tham số đã hoàn thành.

Tôi không chắc đây là giải pháp "tốt hơn", nhưng đó là một cách nghĩ khác và giải quyết nó.


Là một giải pháp thay thế khác mà bạn có thể sử dụng enable_if, để hình thành một thứ đơn giản hơn nhiều so với giải pháp trước đây của tôi:

#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();
}

Quá tải đầu tiên chỉ cần thêm một đối số từ bộ dữ liệu và đặt nó vào một gói tham số. Quá tải thứ hai có một gói tham số phù hợp và sau đó thực hiện cuộc gọi thực sự, với quá tải đầu tiên bị vô hiệu hóa trong trường hợp thứ nhất và duy nhất trong đó trường hợp thứ hai sẽ khả thi.


1
Tôi đã làm việc trên một cái gì đó rất giống với điều này một thời gian trở lại. Nếu tôi có thời gian tôi sẽ có một cái nhìn thứ hai và xem nó so sánh với các câu trả lời hiện tại như thế nào.
Michael Giá

@Michaelprice - hoàn toàn từ góc độ học tập Tôi muốn thấy bất kỳ giải pháp thay thế nào không làm hỏng một số hack khủng khiếp làm hỏng con trỏ ngăn xếp (hoặc gọi thủ thuật cụ thể theo quy ước tương tự).
Flexo

2

Biến thể của tôi về giải pháp từ Johannes sử dụng C ++ 14 std :: index_ resultence (và kiểu trả về hàm làm tham số mẫu 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;
}

Tất cả những giải pháp đó có thể giải quyết vấn đề ban đầu, nhưng thành thật mà nói, không phải công cụ mẫu này đi sai hướng - về mặt đơn giản và khả năng bảo trì ?
xy

Tôi nghĩ rằng các mẫu đã trở nên tốt hơn và dễ hiểu hơn với C ++ 11 và 14. Một vài năm trước khi tôi xem xét những gì tăng cường với các mẫu dưới mui xe, tôi đã thực sự nản lòng. Tôi đồng ý rằng việc phát triển các mẫu tốt khó hơn đáng kể so với việc chỉ sử dụng chúng.
schwart

1
@xy Thứ nhất, về độ phức tạp của mẫu, điều này khônggì cả . Thứ hai, hầu hết các mẫu trợ giúp là một khoản đầu tư ban đầu cho hàng tấn thời gian được lưu khi khởi tạo chúng sau này. Cuối cùng, những gì, bạn sẽ không có khả năng để làm những gì mẫu cho phép bạn làm? Bạn chỉ có thể không sử dụng nó, và không để lại những bình luận không liên quan mà dường như đang kiểm soát các lập trình viên khác.
gạch dưới
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.