Как связать и сериализовать функции, перегружая | оператор

Я пытаюсь выяснить, как вообще перегрузить operator|() для данного объекта базового класса для сериализации или цепочки вызовов функций, которые аналогичны тому, как работает pipes или operator<<()... Я хотел бы связать их через оператор канала. , Таким образом, я могу иметь ряд автономных функций и вызывать их для одного объекта данных... Другими словами, выполнять несколько преобразований одного и того же типа данных, как в потоковой системе...

Рассмотрим следующий пример псевдокода: этот код, вероятно, не скомпилируется, у меня нет под рукой моего компилятора, и я могу использовать неправильный синтаксис для указателей функций или объектов функций в качестве параметра в операторах... Это только чтобы проиллюстрировать образец и поведение, которое мне нужно.

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

Тогда я мог бы использовать их примерно так:

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

Я просто не знаю, как распространять intput - output в операторе |()... Придется ли мне перегружать две версии, чтобы он мог распознавать |(lhs, rhs), а также |(rhs, lhs)?

Более того, что, если я захочу расширить это так, чтобы мои functors или lambdas принимали несколько аргументов...

Я выполнял поиск в Google по этому вопросу и нашел только пару ресурсов, но ничего конкретного, простого, элегантного и современного, по крайней мере, с функциями С++ 17...

Если вы знаете какие-либо хорошие исходные материалы по этому вопросу, пожалуйста, дайте мне знать!


person Francis Cugler    schedule 22.07.2020    source источник
comment
@IgorTandetnik Я знаю, это просто псевдокод ... У меня нет под рукой моего компилятора ... Но цель состоит в том, чтобы взять функтор, подобный объекту ... или, может быть, значение и функтор ...   -  person Francis Cugler    schedule 22.07.2020
comment
Ни один из ваших примеров использования не имеет для меня особого смысла. Какое значение result должно быть в конце? Что вы добавляете один или два к? Какую роль должны играть p1 и p2?   -  person Igor Tandetnik    schedule 22.07.2020
comment
@Ignor рассматривает объект как 2D-вектор ... скажем, он уже заполнен значениями ... такими как vec2 v2 = {3,5} ... тогда я хотел бы иметь возможность сделать что-то вроде: v2 = rotate(30) | scale(5) | translate(15); тогда он повернет его на 30 градусов или радианы, увеличьте его на 5 единиц, а затем переведите на 15... Примерно так работает linux's pipes...   -  person Francis Cugler    schedule 22.07.2020
comment
Вы контролируете определение vec2? Можете ли вы дать ему оператор присваивания, который будет принимать объект шаблон выражения, представляющий эту последовательность преобразований?   -  person Igor Tandetnik    schedule 22.07.2020
comment
@IgorTandetnik Это будут мои пользовательские классы, так что да ... У меня уже есть иерархия классов Component, в которой используется дизайн CRTP, и это оператор, который я хотел бы добавить в базовый класс, чтобы все классы компонентов может использовать эту операцию изменяющимся образом... Оператор может жить вне класса и просто принимать объекты, или он может быть определен внутри... любой метод работает для меня...   -  person Francis Cugler    schedule 22.07.2020
comment
Тогда а) вы, вероятно, захотите поместить в вопрос свой фактический мотивирующий пример, потому что то, что у вас есть сейчас, не имеет смысла, и б) как я уже сказал, техника, которую вы ищете, называется шаблонами выражений. Вы должны найти несколько примеров, если вы ищете это.   -  person Igor Tandetnik    schedule 22.07.2020
comment
@Igor Да, я знаком с CRTP ... и я выполнял поиск по operator|() перегрузке, конвейерной обработке, цепочке и т. Д. ... Мне придется изучить expression templates ... Теперь, есть ли какие-либо новые функции внутри С++ 17, который можно использовать, чтобы сделать это проще? У меня еще нет компилятора С++ 20 для использования concepts, ranges и т.д.   -  person Francis Cugler    schedule 22.07.2020
comment
@IgorTandetnik Если вы нашли эту ссылку, у нее есть несколько полезный пример ... pfultz2 .com/blog/2014/09/05/pipable-functions, это похоже на то, что мне нужно... Но я думаю, что это добавляет слишком много сложности... У меня есть один базовый класс Component и из что у меня может быть 4 абстрактных типа, и каждый из них может иметь 3-10 вариантов... Когда вы начинаете шаблонизировать все, чтобы сделать код универсальным, это начинает немного сбивать с толку...   -  person Francis Cugler    schedule 22.07.2020
comment
@IgorTandetnik Я дал свой собственный ответ теперь, когда мне доступен мой компилятор - IDE. Я думаю, из ответа вы можете увидеть, к чему я пытался добраться ... Дайте мне знать, что вы думаете, оставив комментарий под моим ответом.   -  person Francis Cugler    schedule 23.07.2020
comment
@MooingDuck Да, это то, что я пытался имитировать, но с помощью opeator|(), поскольку он редко перегружается ... Я не хочу использовать операторы << или >>, так как я буду использовать их для input и output моего классы! И учитывая, что Linux команды имеют технику передачи по конвейеру, я хотел имитировать это в своем исходном коде на С++, поскольку | обычно используется для передачи по конвейеру или цепочки команд.   -  person Francis Cugler    schedule 23.07.2020
comment
@FrancisCugler: я удалил свой комментарий, потому что при дальнейшем чтении ваш вопрос сбивает меня с толку, и я его не понимаю, и я не уверен, что это то же самое. int result = p1(&add_one) | p2(&add_two); Какую операцию здесь должен выполнять |? Кажется, никто не может понять, что вы хотите сделать с этими двумя целыми числами.   -  person Mooing Duck    schedule 23.07.2020
comment
@MooingDuck Это сложно выразить словами ... но подумайте о том, чтобы иметь уже сконструированный тип, например vec2 v2{3,5} Тогда, скажем, я хочу выполнить серию переводов для этого вектора ... Тогда у меня будет что-то вроде этого : `v2 | перевести(2.5) | вращать(30) | перевести(3) | шкала (2); Затем он переместит этот вектор на 2,5 единицы, повернет его на 30 градусов или радиан, затем переведет его на 3 единицы, а затем масштабирует на 2 в указанном порядке. Это последовательность операций, выполняемых над одним типом данных! Вектор был просто представлением...   -  person Francis Cugler    schedule 23.07.2020
comment
@FrancisCugler, это легко, в зависимости от того, можете ли вы редактировать transform и rotate и тому подобное   -  person Mooing Duck    schedule 23.07.2020
comment
@Mooing, поэтому вместо того, чтобы иметь такой код, как v2.translate(2.5); v2.rotate(30); v2.scale(10), я хочу использовать оператор | для выполнения этих функций в этом канале данных в одной цепочке строк или передаче команд.   -  person Francis Cugler    schedule 23.07.2020
comment
@MooingDuck Ну, я не использую predefined library, это мой собственный проект, поэтому все мои классы мои собственные... У меня есть полный контроль над их реализацией и интерфейсами... Я просто хочу знать, как это сделать в общем, чтобы это может работать для любого из моих объектов без необходимости переписывать этот оператор для каждого класса....   -  person Francis Cugler    schedule 23.07.2020
comment
@MooingDuck теперь с двумя классами, которые я показал ниже, я мог бы наследовать от них другие классы в стиле CRTP, и это может позволить мне иметь другие объекты моего класса, имеющие это свойство...   -  person Francis Cugler    schedule 23.07.2020


Ответы (3)


Сначала я предполагаю, что у вас есть некоторые основы, которые выглядят так

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

Итак, вам нужен прокси-класс для операций, где прокси-объект создается с функцией и другими параметрами. (Я сделал функцию параметром шаблона, что предотвращает использование указателей на функции и помогает оптимизатору встраиваться, сводя накладные расходы почти к нулю.)

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

И тогда вы просто делаете это цепным

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

Использование простое:

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

Примечание. Никаких выделений, указателей, копий и перемещений. Это должно сгенерировать тот же код, как если бы вы только что сделали v2.translate(2.5); v2.rotate(30); v2.scale(10); напрямую.

person Mooing Duck    schedule 23.07.2020
comment
Не на 100% именно то, что я искал, однако эта техника может быть очень полезной! Мне также нравится, как вы указали Zero Overhead без необходимости использования указателей на функции... Единственное здесь то, что если у меня есть другой набор классов, мне придется реализовать это для каждого набора... Однако, как только я ознакомьтесь с шаблоном, его не должно быть так сложно воспроизвести! - person Francis Cugler; 23.07.2020
comment
В будущем, когда у меня будет полностью совместимый компилятор C++20 (все еще на C+17), я смогу использовать concepts, ranges и т. д., чтобы сделать это еще более тривиальным! - person Francis Cugler; 23.07.2020
comment
Вы можете использовать SFINAE для расширения одной реализации на любые классы, соответствующие набору ограничений. Также не так уж сложно распространить это на вещи, где возвращаемое значение отличается. myclass | classToString | stringToInteger;. - person Mooing Duck; 23.07.2020
comment
@FrancisCugler: Другой вариант — std::bind, в котором меньше магии шаблонов, чем вы пишете, и является более гибким, но также более подробным на телефонной станции - person Mooing Duck; 23.07.2020

Для вашего векторного примера, который вы добавили в комментарий, у вас может быть такой синтаксис

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

Это будет работать со следующей логикой:

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

и что-то подобное для оператора масштабирования и оператора перевода.

Обратите внимание, что | ассоциативен слева направо, поэтому ввод должен подаваться слева. Но вы можете подумать о добавлении перегрузки к каждой из этих функций, которые принимают параметры преобразования и входной вектор, чтобы вы могли сделать:

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

Последний синтаксис может быть немного более интуитивным, чем запуск конвейера, в основном, с оператора идентификации.

person bartgol    schedule 22.07.2020

Теперь, когда у меня есть компилятор, и снова после небольшой работы над моим проектом; Вот что я смог придумать...

Это не идеально, поскольку function pointer для operator() требует именно 2 параметра, как видно из этого примера кода...

(Мне, вероятно, придется использовать вариативный шаблон для этого, а также для подписи для указателя функции, чтобы разрешить указатель функции, объект функции, функтор или лямбда-выражение с любым количеством параметров различных типов для быть приемлемым...)

Вот мой рабочий исходный код...

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

И вот вывод, который он мне дает:

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

Кажется, что канал operator|() работает для моего класса pipe... и мне пришлось использовать shared_ptr из pipe_object для текущего экземпляра и следующего экземпляра... pipe_object — это просто базовая оболочка для некоторого типа T. Единственной особенностью класса pipe_object является один из его конструкторов... Это конструктор, который берет объект pipe и извлекает значение этого pipe, используя его для создания нового pipe_object. Мне пришлось использовать двойные указатели из-за того, что opeator|() является правосторонним ассоциативным...

Как видно из приведенного выше исходного кода в двойном цикле for... Я использую объекты pipe operator() для передачи значений и адреса функции... Я также могу pipe эти объекты не вызывать их operator(). .. Следующим шагом здесь будет применение этой концепции, но сделать ее общей для любого типа объектов... если только я не смогу просто использовать эти классы в качестве оболочки для использования метода цепочки конвейеров! Я имею в виду, что мой другой класс наследуется от этого класса pipe, как это делается с использованием CRTP.

Дайте мне знать, что вы думаете!

person Francis Cugler    schedule 23.07.2020
comment
Даже после прочтения вашего кода я до сих пор не могу понять, что вы хотите, чтобы operator| делал. Кажется, нужно сделать левое равным правом и вернуть то, что было раньше. Я совершенно уверен, что это можно значительно упростить, однако - person Mooing Duck; 23.07.2020
comment
@MooingDuck Я хочу, чтобы он оценивал применяемую функцию слева направо и применял эту функцию по мере ее обнаружения ... - person Francis Cugler; 23.07.2020