Cara merangkai dan membuat serialisasi fungsi dengan membebani | operator

Saya mencoba mencari cara untuk membebani operator|() secara umum untuk objek kelas dasar tertentu untuk membuat serial atau pemanggilan fungsi rantai yang mirip dengan cara kerja pipes atau operator<<()... Saya ingin menghubungkannya melalui operator pipa.. . Dengan cara ini saya dapat memiliki serangkaian fungsi mandiri, dan memanggilnya pada satu objek data... Dengan kata lain, untuk melakukan beberapa transformasi pada tipe data yang sama, seperti dalam sistem streaming...

Pertimbangkan contoh kode semu berikut: kode ini mungkin tidak dapat dikompilasi, saya tidak memiliki kompiler dan saya mungkin menggunakan sintaks yang salah untuk penunjuk fungsi atau objek fungsi sebagai parameter di operator... Ini hanya untuk mengilustrasikan pola dan perilaku yang saya kejar.

template<typename T>
typedef T(*Func)(T); // Function Pointer for functors-lambdas-etc... 

template<typename T>
struct pipe_object {
    T operator|(T(*Func)(T) func) {
        return func(T);
    }

    T operator()(T(*Func)(T) func) {
        return this->operator|(t, func);
    }
};

Maka saya mungkin ingin menggunakannya seperti ini:

constexpr int add_one_f(int x) {
    return (x+1);
}

constexpr int add_two_f(int x) {
   return (x+2);
}


void foo() {
    pipe_object<int> p1 = {};
    pipe_object<int> p2 = {};

    int result = p1(&add_one) | p2(&add_two); 

    // or something like...

    int result = p1 | p2; // ... etc ...

    // or something like:
    p1 = add_one | add_two | p2; // ... etc ...
}

Saya hanya tidak tahu cara menyebarkan intput - output di operator |()... Apakah saya harus membebani dua versi secara berlebihan agar dapat mengenali |(lhs, rhs) dan juga |(rhs, lhs)?

Lebih dari itu, bagaimana jika saya ingin memperluas ini sehingga functors atau lambdas saya mengambil banyak argumen...

Saya telah melakukan pencarian Google tentang ini dan hanya menemukan beberapa sumber tetapi tidak ada yang konkret, sederhana, elegan, dan terkini setidaknya dengan fitur C++17...

Jika Anda mengetahui sumber materi yang bagus mengenai hal ini, harap beri tahu saya!


person Francis Cugler    schedule 22.07.2020    source sumber
comment
@IgorTandetnik Saya tahu, itu hanya kode semu... Saya tidak memiliki atm kompiler yang berguna... Tapi tujuannya adalah untuk mengambil objek seperti fungsi... atau mungkin nilai dan fungsi...   -  person Francis Cugler    schedule 22.07.2020
comment
Tak satu pun contoh penggunaan Anda masuk akal bagi saya. Berapakah nilai result pada akhirnya? Apa yang Anda tambahkan satu atau dua ke? Peran apa yang seharusnya dimainkan oleh p1 dan p2?   -  person Igor Tandetnik    schedule 22.07.2020
comment
@Abaikan anggap suatu objek seperti vektor 2D... katakanlah sudah diisi dengan nilai... seperti vec2 v2 = {3,5}... maka saya ingin dapat melakukan sesuatu seperti: v2 = rotate(30) | scale(5) | translate(15); maka akan memutarnya 30 derajat atau radian, skalakan dengan 5 unit lalu terjemahkan dengan 15... Hampir seperti cara linux's pipes bekerja...   -  person Francis Cugler    schedule 22.07.2020
comment
Apakah Anda mengontrol definisi vec2? Bisakah Anda memberinya operator penugasan yang akan menerima objek template ekspresi yang mewakili rangkaian transformasi ini?   -  person Igor Tandetnik    schedule 22.07.2020
comment
@IgorTandetnik Ini akan menjadi kelas khusus saya, jadi ya... Saya sudah memiliki hierarki kelas Component yang menggunakan desain CRTP dan ini adalah operator yang ingin saya tambahkan ke kelas dasar sehingga semua kelas komponen dapat menggunakan operasi ini dengan cara yang berubah... Operator dapat tinggal di luar kelas dan hanya mengambil objek atau dapat didefinisikan di dalam... metode mana pun yang berfungsi untuk saya...   -  person Francis Cugler    schedule 22.07.2020
comment
Kemudian a) Anda mungkin ingin memasukkan contoh motivasi Anda yang sebenarnya ke dalam pertanyaan, karena apa yang Anda miliki di sana sekarang tidak masuk akal, dan b) seperti saya katakan, teknik yang Anda cari disebut templat ekspresi. Anda harus menemukan beberapa contoh jika Anda mencarinya.   -  person Igor Tandetnik    schedule 22.07.2020
comment
@Igor Ya, saya kenal dengan CRTP... dan saya melakukan penelusuran tentang operator|() kelebihan beban, pipelining, rangkaian, dll... Saya harus melihat ke expression templates... Sekarang, apakah ada fitur baru di dalamnya c++17 yang dapat digunakan untuk mempermudah ini? Saya belum memiliki kompiler C++20 untuk menggunakan concepts, ranges dll.   -  person Francis Cugler    schedule 22.07.2020
comment
@IgorTandetnik Jika menemukan tautan ini, ada contoh yang berguna... pfultz2 .com/blog/2014/09/05/pipable-functions yang mirip dengan yang saya cari... Tapi menurut saya itu menambah terlalu banyak kerumitan... Saya punya satu kelas dasar Component dan dari itu, saya mungkin memiliki 4 tipe abstrak, dan masing-masing dapat memiliki 3-10 varian... Saat itulah Anda mulai membuat template semuanya untuk membuat kode menjadi generik yang mulai sedikit membingungkan...   -  person Francis Cugler    schedule 22.07.2020
comment
@IgorTandetnik Saya telah memberikan jawaban saya sendiri sekarang karena kompiler saya - IDE tersedia untuk saya. Saya rasa dari jawabannya, Anda dapat melihat apa yang ingin saya capai... Beri tahu saya pendapat Anda dengan meninggalkan komentar di bawah jawaban saya.   -  person Francis Cugler    schedule 23.07.2020
comment
@MooingDuck Ya, itulah yang saya coba tiru tetapi melalui penggunaan opeator|() karena jarang kelebihan beban... Saya tidak ingin menggunakan operator << atau >> karena saya akan menggunakannya untuk input dan output milik saya kelas! Dan mengingat perintah Linux memiliki teknik perpipaan, saya ingin menirunya dalam kode sumber c++ saya karena | biasanya digunakan untuk pemipaan atau rangkaian perintah.   -  person Francis Cugler    schedule 23.07.2020
comment
@FrancisCugler: Saya menghapus komentar saya, karena setelah membaca lebih lanjut, pertanyaan Anda membingungkan saya dan saya tidak memahaminya dan saya tidak yakin itu sama sekali. int result = p1(&add_one) | p2(&add_two); Operasi apa yang harus dilakukan | di sini? Sepertinya tidak ada yang bisa memahami apa yang ingin Anda lakukan dengan kedua bilangan bulat itu.   -  person Mooing Duck    schedule 23.07.2020
comment
@MooingDuck Agak sulit untuk diungkapkan dengan kata-kata... tetapi pertimbangkan untuk memiliki tipe yang sudah dibuat seperti vec2 v2{3,5} Lalu katakanlah saya ingin melakukan serangkaian terjemahan pada vektor itu... Lalu saya akan memiliki sesuatu seperti ini : `v2 | terjemahkan(2.5) | putar(30) | terjemahkan(3) | skala(2); Kemudian ia akan menerjemahkan vektor tersebut sebanyak 2,5 satuan, memutarnya sebanyak 30 derajat atau radian, lalu menerjemahkannya sebanyak 3 satuan, lalu menskalakannya sebanyak 2 dalam urutan tersebut. Ini adalah urutan operasi yang dilakukan pada satu tipe data! Vektor hanyalah representasi...   -  person Francis Cugler    schedule 23.07.2020
comment
@FrancisCugler, itu mudah, tergantung apakah Anda dapat mengedit transform dan rotate dan semacamnya   -  person Mooing Duck    schedule 23.07.2020
comment
@Mooing jadi alih-alih memiliki kode seperti v2.translate(2.5); v2.rotate(30); v2.scale(10) Saya ingin menggunakan operator | untuk melakukan fungsi-fungsi tersebut pada pipa data itu dalam satu baris yang merangkai atau menyalurkan perintah.   -  person Francis Cugler    schedule 23.07.2020
comment
@MooingDuck Ya, saya tidak menggunakan predefined library ini proyek saya sendiri jadi semua kelas saya adalah milik saya sendiri... Saya memiliki kendali penuh atas implementasi dan antarmukanya... Saya hanya ingin tahu cara melakukan ini secara umum sehingga dapat bekerja untuk objek mana pun saya tanpa harus menulis ulang operator ini untuk setiap kelas....   -  person Francis Cugler    schedule 23.07.2020
comment
@MooingDuck sekarang dengan dua kelas yang saya tunjukkan di bawah, saya dapat membuat kelas saya yang lain mewarisinya dengan cara CRTP dan itu memungkinkan saya untuk memiliki objek kelas saya yang lain yang memiliki properti ini...   -  person Francis Cugler    schedule 23.07.2020


Jawaban (3)


Pertama saya berasumsi Anda memiliki beberapa dasar yang terlihat seperti ini

#include <iostream>
struct vec2 {
    double x;
    double y;
};
std::ostream& operator<<(std::ostream& stream, vec2 v2) {return stream<<v2.x<<','<<v2.y;}

//real methods
vec2 translate(vec2 in, double a) {return vec2{in.x+a, in.y+a};} //dummy placeholder implementations
vec2 rotate(vec2 in, double a) {return vec2{in.x+1, in.y-1};}
vec2 scale(vec2 in, double a) {return vec2{in.x*a, in.y*a};}

Jadi yang Anda inginkan adalah kelas proksi untuk operasi, di mana objek proksi dibuat dengan fungsi dan parameter lainnya. (Saya menjadikan fungsi tersebut sebagai parameter templat, yang mencegah penggunaan penunjuk fungsi, dan membantu pengoptimal untuk melakukan inline, sehingga overhead ini hampir nol.)

#include <type_traits>
//operation proxy class
template<class rhst, //type of the only parameter
     vec2(*f)(vec2,rhst)> //the function to call
class vec2_op1 {
    std::decay_t<rhst> rhs; //store the parameter until the call
public:
    vec2_op1(rhst rhs_) : rhs(std::forward<rhst>(rhs_)) {}
    vec2 operator()(vec2 lhs) {return f(lhs, std::forward<rhst>(rhs));}
};

//proxy methods
vec2_op1<double,translate> translate(double a) {return {a};}
vec2_op1<double,rotate> rotate(double a) {return {a};}
vec2_op1<double,scale> scale(double a) {return {a};}

Dan kemudian Anda membuatnya bisa dirantai

//lhs is a vec2, rhs is a vec2_operation to use
template<class rhst, vec2(*f)(vec2,rhst)>
vec2& operator|(vec2& lhs, vec2_op1<rhst, f>&& op) {return lhs=op(lhs);}

Penggunaannya sederhana:

int main() {
    vec2 v2{3,5};
    v2 | translate(2.5) | rotate(30) | translate(3) | scale(2);
    std::cout << v2;
}

http://coliru.stacked-crooked.com/a/9b58992b36ff12d3

Catatan: Tidak ada alokasi, tidak ada petunjuk, tidak ada salinan atau pemindahan. Ini akan menghasilkan kode yang sama seperti jika Anda baru saja melakukan v2.translate(2.5); v2.rotate(30); v2.scale(10); secara langsung.

person Mooing Duck    schedule 23.07.2020
comment
Tidak 100% persis seperti yang saya cari, namun teknik ini bisa sangat berguna! Saya juga suka bagaimana Anda menunjukkan Zero Overhead tanpa perlu menggunakan penunjuk fungsi... Satu-satunya hal di sini adalah jika saya memiliki kumpulan kelas yang berbeda, saya harus menerapkan ini untuk setiap set... Namun, begitu saya menjadi akrab dengan polanya, seharusnya tidak sulit untuk menirunya! - person Francis Cugler; 23.07.2020
comment
Nanti setelah saya memiliki kompiler C++20 yang sepenuhnya patuh (masih menggunakan kompiler C+17), saya seharusnya dapat menggunakan concepts, ranges, dll... untuk menjadikannya sedikit lebih sepele! - person Francis Cugler; 23.07.2020
comment
Anda dapat menggunakan SFINAE untuk memperluas implementasi tunggal ke kelas mana pun yang cocok dengan serangkaian batasan. Juga tidak terlalu sulit untuk memperluas ini ke hal-hal yang nilai pengembaliannya berbeda. myclass | classToString | stringToInteger;. - person Mooing Duck; 23.07.2020
comment
@FrancisCugler: Opsi lainnya adalah std::bind, yang memiliki lebih sedikit keajaiban templat yang Anda tulis, dan lebih fleksibel, namun juga lebih bertele-tele di lokasi panggilan - person Mooing Duck; 23.07.2020

Untuk contoh vektor yang Anda tambahkan di komentar, Anda dapat memiliki sintaks seperti ini

MyVec vec = {1, 2};
auto vec2 = vec | rotate(90) | scale(2.0) | translate(1.0,2.0);

Cara kerjanya adalah dengan logika berikut:

class Transform {
public:
  virtual ~Transform () = default;
  virtual MyVector apply (const MyVector& in) const = 0;
};
inline MyVector operator| (const MyVector& v, const Transform& t) {
   return t.apply(v);
}

class Rotation : public Transform {
public:
  Rotation (int deg): m_deg(deg) {}
  MyVector apply (const MyVector& v) override {...}
private:
  int m_deg;
}:
Rotation rotate(int deg) { return Rotation(deg); }

dan sesuatu yang serupa untuk operator penskalaan dan operator terjemahan.

Perhatikan bahwa | bersifat asosiatif dari kiri ke kanan, jadi masukannya harus dimasukkan ke kiri. Namun Anda mungkin berpikir untuk menambahkan kelebihan beban ke masing-masing fungsi yang mengambil parameter transformasi dan vektor masukan, sehingga Anda dapat melakukan:

auto vec2 = rotate(90,vec) | scale(2.0) | translate(1.0,2.0);

Sintaks yang terakhir mungkin sedikit lebih intuitif daripada memulai perpipaan dengan, pada dasarnya, dan operator identitas.

person bartgol    schedule 22.07.2020

Sekarang kompiler saya tersedia untuk saya dan lagi setelah mengerjakan proyek saya sebentar; Inilah yang dapat saya temukan...

Ini tidak sempurna karena function pointer untuk operator() memerlukan 2 parameter khusus seperti yang dapat dilihat pada kode contoh ini...

(Saya mungkin harus menggunakan templat variadik untuk ini serta tanda tangan untuk penunjuk fungsi agar memungkinkan penunjuk fungsi, objek fungsi, fungsi, atau ekspresi lambda dengan sejumlah parameter dari berbagai jenis untuk dapat diterima...)

Ini kode sumber kerja saya...

#pragma once    

#include <exception>
#include <iostream>
#include <memory>

template<typename T>
class pipe;

template<typename T>
class pipe_object {
private:
    T t_;
public:
    explicit pipe_object(T t) : t_{ t } {}

    explicit pipe_object(pipe<T> pipe) : t_{ pipe.current()->value() } {}

    pipe_object(const pipe_object<T>& other) {
        this->t_ = other.t_;
    }

    pipe_object<T>& operator=(const pipe_object<T>& other) {
        this->t_ = other.t_;
        return *this;
    }

    T value() const { return t_; }

    T operator()() {
        return t_;
    }
};

template<typename T>
class pipe {
private:
    std::shared_ptr<pipe_object<T>> current_;
    std::shared_ptr<pipe_object<T>> next_;

public:
    explicit pipe(T t) : 
        current_{ nullptr },
        next_{ nullptr } 
    {
        current_.reset(new pipe_object<T>(t));
    }

    pipe_object<T>* current() const { return current_.get(); }
    pipe_object<T>* next() const { return next_.get(); }

    T operator|(pipe<T> in) {
        pipe_object<T>* temp = current_.get();
        next_.reset(new pipe_object<T>(in));
        current_ = next_;
        return temp->value();
    }

    T operator()(T a, T b, T(*Func)(T,T)) {
        return Func(a,b);
    }
};

constexpr int add(int a, int b) {
    return a + b;
}

int main() {
    try {    
        pipe<int> p1(1);
        pipe<int> p2(3);

        for (int i = 0; i < 10; i++) {
            for (int j = 0; j < 10; j++) {
                int x = p1(i,j, &add) | p2(i,j, &add);
                std::cout << x << ' ';                
            }
            std::cout << '\n';
        }
        // Game game;      
        // game.run();      
    }
    catch (const std::exception& e) {
        std::cerr << e.what() << std::endl;
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

Dan inilah hasil yang diberikannya kepada saya:

0 1 2 3 4 5 6 7 8 9
1 2 3 4 5 6 7 8 9 10
2 3 4 5 6 7 8 9 10 11
3 4 5 6 7 8 9 10 11 12
4 5 6 7 8 9 10 11 12 13
5 6 7 8 9 10 11 12 13 14
6 7 8 9 10 11 12 13 14 15
7 8 9 10 11 12 13 14 15 16
8 9 10 11 12 13 14 15 16 17
9 10 11 12 13 14 15 16 17 18

Pipa operator|() tampaknya berfungsi untuk kelas pipe saya... dan saya harus menggunakan shared_ptr dari pipe_object untuk instance saat ini dan instance berikutnya... pipe_object hanyalah pembungkus dasar untuk beberapa tipe T. Satu-satunya fitur khusus dari kelas pipe_object adalah salah satu konstruktornya... Konstruktor itulah yang mengambil objek pipe dan mengekstrak nilai pipe tersebut menggunakannya untuk membuat pipe_object baru. Saya harus menggunakan petunjuk ganda karena sifat opeator|() adalah asosiatif tangan kanan...

Seperti yang Anda lihat dari kode sumber di atas dalam loop for ganda... Saya menggunakan objek pipe operator() untuk meneruskan nilai dan alamat ke suatu fungsi... Saya juga dapat pipe objek-objek ini tanpa memanggil operator()-nya. .. Langkah selanjutnya dari sini adalah menerapkan konsep ini tetapi menjadikannya umum untuk objek apa pun... kecuali saya bisa menggunakan kelas-kelas ini sebagai pembungkus untuk menggunakan teknik rangkaian pipa! Maksud saya dengan membuat kelas saya yang lain mewarisi dari kelas pipe ini seperti yang dilakukan seseorang dengan menggunakan CRTP.

Biarkan aku tahu apa yang Anda pikirkan!

person Francis Cugler    schedule 23.07.2020
comment
Bahkan setelah membaca kode Anda, saya masih tidak tahu apa yang ingin operator| Anda lakukan. Tampaknya membuat kiri sama dengan kanan, dan mengembalikan kiri seperti semula. Saya cukup yakin hal ini dapat disederhanakan secara drastis - person Mooing Duck; 23.07.2020
comment
@MooingDuck Saya ingin mengevaluasi fungsi yang diterapkan dari kiri ke kanan dan menerapkan fungsi itu seperti yang ditemui... - person Francis Cugler; 23.07.2020