วิธีจัดเก็บอาร์กิวเมนต์แม่แบบตัวแปร


89

เป็นไปได้ไหมที่จะจัดเก็บชุดพารามิเตอร์เพื่อใช้ในภายหลัง?

template <typename... T>
class Action {
private:        
    std::function<void(T...)> f;
    T... args;  // <--- something like this
public:
    Action(std::function<void(T...)> f, T... args) : f(f), args(args) {}
    void act(){
        f(args);  // <--- such that this will be possible
    }
}

จากนั้นในภายหลัง:

void main(){
    Action<int,int> add([](int x, int y){std::cout << (x+y);}, 3, 4);

    //...

    add.act();
}

3
ใช่; จัดเก็บทูเพิลและใช้ชุดดัชนีบางประเภทเพื่อโทรออก
Kerrek SB

สิ่งนี้ตอบคำถามของคุณหรือไม่? C ++ วิธีการจัดเก็บชุดพารามิเตอร์เป็นตัวแปร
user202729

คำตอบ:


67

เพื่อให้บรรลุสิ่งที่คุณต้องการที่นี่คุณจะต้องเก็บอาร์กิวเมนต์แม่แบบของคุณไว้ในทูเพิล:

std::tuple<Ts...> args;

นอกจากนี้คุณจะต้องเปลี่ยนตัวสร้างของคุณเล็กน้อย โดยเฉพาะอย่างยิ่งการเริ่มต้นargsด้วยstd::make_tupleและยังอนุญาตให้มีการอ้างอิงสากลในรายการพารามิเตอร์ของคุณ:

template <typename F, typename... Args>
Action(F&& func, Args&&... args)
    : f(std::forward<F>(func)),
      args(std::forward<Args>(args)...)
{}

ยิ่งไปกว่านั้นคุณจะต้องตั้งค่าตัวสร้างลำดับดังนี้:

namespace helper
{
    template <int... Is>
    struct index {};

    template <int N, int... Is>
    struct gen_seq : gen_seq<N - 1, N - 1, Is...> {};

    template <int... Is>
    struct gen_seq<0, Is...> : index<Is...> {};
}

และคุณสามารถใช้วิธีการของคุณในแง่ของการใช้เครื่องกำเนิดไฟฟ้าดังกล่าว:

template <typename... Args, int... Is>
void func(std::tuple<Args...>& tup, helper::index<Is...>)
{
    f(std::get<Is>(tup)...);
}

template <typename... Args>
void func(std::tuple<Args...>& tup)
{
    func(tup, helper::gen_seq<sizeof...(Args)>{});
}

void act()
{
   func(args);
}

และนั่นมัน! ตอนนี้ชั้นเรียนของคุณควรมีลักษณะดังนี้:

template <typename... Ts>
class Action
{
private:
    std::function<void (Ts...)> f;
    std::tuple<Ts...> args;
public:
    template <typename F, typename... Args>
    Action(F&& func, Args&&... args)
        : f(std::forward<F>(func)),
          args(std::forward<Args>(args)...)
    {}

    template <typename... Args, int... Is>
    void func(std::tuple<Args...>& tup, helper::index<Is...>)
    {
        f(std::get<Is>(tup)...);
    }

    template <typename... Args>
    void func(std::tuple<Args...>& tup)
    {
        func(tup, helper::gen_seq<sizeof...(Args)>{});
    }

    void act()
    {
        func(args);
    }
};

นี่คือโปรแกรมเต็มของคุณใน Coliru


อัปเดต: นี่คือวิธีการช่วยเหลือที่ไม่จำเป็นต้องใช้คุณสมบัติของอาร์กิวเมนต์เทมเพลต:

template <typename F, typename... Args>
Action<Args...> make_action(F&& f, Args&&... args)
{
    return Action<Args...>(std::forward<F>(f), std::forward<Args>(args)...);
}

int main()
{
    auto add = make_action([] (int a, int b) { std::cout << a + b; }, 2, 3);

    add.act();
}

และอีกครั้งนี่คือการสาธิตอื่น


1
คุณช่วยขยายคำตอบเล็กน้อยได้ไหม
Eric B

1
เนื่องจาก Ts ... เป็นพารามิเตอร์เทมเพลตคลาสไม่ใช่พารามิเตอร์เทมเพลตฟังก์ชัน Ts && ... จึงไม่กำหนดชุดของการอ้างอิงสากลเนื่องจากไม่มีการหักประเภทเกิดขึ้นสำหรับชุดพารามิเตอร์ @jogojapan แสดงวิธีที่ถูกต้องเพื่อให้แน่ใจว่าคุณสามารถส่งต่อการอ้างอิงที่เป็นสากลไปยังผู้สร้างได้
masrtis

3
ระวังการอ้างอิงและอายุการใช้งานของวัตถุ! void print(const std::string&); std::string hello(); auto act = make_action(print, hello());ไม่ดี ฉันชอบพฤติกรรมของstd::bindซึ่งทำให้สำเนาของแต่ละอาร์กิวเมนต์ถ้าคุณปิดการใช้งานด้วยหรือstd::ref std::cref
aschepler

1
ฉันคิดว่า @jogojapan มีวิธีแก้ปัญหาที่กระชับและอ่านง่ายกว่านี้
Tim Kuipers

1
@Riddick เปลี่ยนargs(std::make_tuple(std::forward<Args>(args)...))เป็นargs(std::forward<Args>(args)...). BTW ฉันเขียนสิ่งนี้มานานแล้วและฉันจะไม่ใช้รหัสนี้เพื่อจุดประสงค์ในการผูกฟังก์ชันกับอาร์กิวเมนต์บางอย่าง ฉันจะใช้std::invoke()หรือstd::apply()ในปัจจุบัน
0x499602D2

23

คุณสามารถใช้std::bind(f,args...)สำหรับสิ่งนี้ มันจะสร้างอ็อบเจ็กต์ที่เคลื่อนย้ายได้และอาจคัดลอกได้ซึ่งเก็บสำเนาของอ็อบเจ็กต์ฟังก์ชันและอาร์กิวเมนต์แต่ละตัวเพื่อใช้ในภายหลัง:

#include <iostream>
#include <utility>
#include <functional>

template <typename... T>
class Action {
public:

  using bind_type = decltype(std::bind(std::declval<std::function<void(T...)>>(),std::declval<T>()...));

  template <typename... ConstrT>
  Action(std::function<void(T...)> f, ConstrT&&... args)
    : bind_(f,std::forward<ConstrT>(args)...)
  { }

  void act()
  { bind_(); }

private:
  bind_type bind_;
};

int main()
{
  Action<int,int> add([](int x, int y)
                      { std::cout << (x+y) << std::endl; },
                      3, 4);

  add.act();
  return 0;
}

สังเกตว่าstd::bindเป็นฟังก์ชันและคุณต้องจัดเก็บในฐานะสมาชิกข้อมูลผลของการเรียกใช้ ประเภทข้อมูลของผลลัพธ์นั้นไม่ง่ายที่จะคาดเดา (มาตรฐานไม่ได้ระบุไว้อย่างแม่นยำ) ดังนั้นฉันจึงใช้การรวมdecltypeและstd::declvalคำนวณชนิดข้อมูลนั้นในเวลาคอมไพล์ ดูคำจำกัดความของAction::bind_typeด้านบน

สังเกตด้วยว่าฉันใช้การอ้างอิงสากลในตัวสร้างเทมเพลตอย่างไร สิ่งนี้ช่วยให้มั่นใจได้ว่าคุณสามารถส่งผ่านอาร์กิวเมนต์ที่ไม่ตรงกับพารามิเตอร์เทมเพลตคลาสได้ทุกT...ประการ (เช่นคุณสามารถใช้การอ้างอิง rvalue กับบางส่วนTและคุณจะได้รับการส่งต่อตามที่เป็นอยู่ไปยังการbindโทร)

หมายเหตุสุดท้าย: หากคุณต้องการจัดเก็บอาร์กิวเมนต์เป็นข้อมูลอ้างอิง (เพื่อให้ฟังก์ชันที่คุณส่งผ่านสามารถแก้ไขได้แทนที่จะใช้เพียงอย่างเดียว) คุณต้องใช้std::refเพื่อรวมอาร์กิวเมนต์ไว้ในวัตถุอ้างอิง เพียงแค่ส่งT &จะสร้างสำเนาของค่าไม่ใช่ข้อมูลอ้างอิง

รหัสการทำงานบน Coliru


การผูกค่า rvalues ​​ไม่เป็นอันตรายหรือไม่? สิ่งเหล่านี้จะไม่ถูกยกเลิกเมื่อaddถูกกำหนดในขอบเขตอื่นแล้วact()จะเรียกว่าที่ไหน? ผู้สร้างไม่ควรได้รับConstrT&... argsมากกว่าConstrT&&... args?
Tim Kuipers

1
@Angelorf ขอโทษที่ตอบช้า คุณหมายถึงค่า rvalues ​​ในการโทรไปbind()? เนื่องจากbind()รับประกันว่าจะทำสำเนา (หรือย้ายไปยังวัตถุที่สร้างขึ้นใหม่) ฉันไม่คิดว่าจะมีปัญหา
jogojapan

@jogojapan บันทึกย่อ MSVC17 ต้องการฟังก์ชันในตัวสร้างเพื่อส่งต่อไปยัง bind_ เช่นกัน (เช่น bind_ (std :: forward <std :: function <void (T ... ) >> (f), std :: forward < ConstrT> (args) ... ))
Outshined

1
ใน initializer bind_(f, std::forward<ConstrT>(args)...)เป็นพฤติกรรมที่ไม่ได้กำหนดตามมาตรฐานเนื่องจากตัวสร้างนั้นถูกกำหนดให้ใช้งานได้ bind_typeถูกระบุให้เป็นสำเนาและ / หรือย้าย - สร้างได้ แต่bind_{std::bind(f, std::forward<ConstrT>(args)...)}ก็ยังควรใช้งานได้
joshtch

6

คำถามนี้มาจาก C ++ 11 วัน แต่สำหรับผู้ที่พบในผลการค้นหาการอัปเดตบางอย่าง:

std::tupleสมาชิกยังคงเป็นวิธีตรงไปตรงมาในการจัดเก็บข้อโต้แย้งโดยทั่วไป ( std::bindโซลูชันที่คล้ายกับ@ jogojapanจะใช้ได้เช่นกันหากคุณต้องการเรียกใช้ฟังก์ชันเฉพาะ แต่ไม่ใช่หากคุณต้องการเข้าถึงอาร์กิวเมนต์ด้วยวิธีอื่นหรือส่งผ่านอาร์กิวเมนต์ไปยังฟังก์ชันมากกว่าหนึ่งฟังก์ชันเป็นต้น)

ใน C ++ 14 ขึ้นไปstd::make_index_sequence<N>หรือstd::index_sequence_for<Pack...>สามารถแทนที่helper::gen_seq<N>เครื่องมือที่เห็นในโซลูชันของ 0x499602D2 :

#include <utility>

template <typename... Ts>
class Action
{
    // ...
    template <typename... Args, std::size_t... Is>
    void func(std::tuple<Args...>& tup, std::index_sequence<Is...>)
    {
        f(std::get<Is>(tup)...);
    }

    template <typename... Args>
    void func(std::tuple<Args...>& tup)
    {
        func(tup, std::index_sequence_for<Args...>{});
    }
    // ...
};

ใน C ++ 17 และใหม่กว่าstd::applyสามารถใช้เพื่อดูแลการแกะทูเปิลได้:

template <typename... Ts>
class Action
{
    // ...
    void act() {
        std::apply(f, args);
    }
};

นี่คือโปรแกรม C ++ 17 เต็มรูปแบบที่แสดงการใช้งานที่ง่ายขึ้น ฉันยังอัปเดตmake_actionเพื่อหลีกเลี่ยงประเภทการอ้างอิงในtupleซึ่งไม่ดีเสมอสำหรับอาร์กิวเมนต์ rvalue และค่อนข้างเสี่ยงสำหรับอาร์กิวเมนต์ lvalue


3

ฉันคิดว่าคุณมีปัญหา XY ทำไมต้องไปหาปัญหาในการจัดเก็บชุดพารามิเตอร์ในเมื่อคุณสามารถใช้แลมบ์ดาที่ callite ได้ กล่าวคือ

#include <functional>
#include <iostream>

typedef std::function<void()> Action;

void callback(int n, const char* s) {
    std::cout << s << ": " << n << '\n';
}

int main() {
    Action a{[]{callback(13, "foo");}};
    a();
}

เนื่องจากในแอปพลิเคชันของฉัน Action มี functors 3 แบบที่เกี่ยวข้องกันทั้งหมดและฉันต้องการเรียนที่มี 1 Action ไม่ใช่ 3 std :: function
Eric B
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.