【问题标题】:How to chain and serialize functions by overloading the | operator如何通过重载 | 链接和序列化函数操作员
【发布时间】:2020-11-11 12:02:51
【问题描述】:

我试图弄清楚如何为给定的基类对象一般地重载operator|() 以序列化或链接类似于pipesoperator<<() 工作方式的函数调用...我想通过管道运算符链接它们......这样我可以拥有一系列独立的函数,并在单个数据对象上调用它们......换句话说,对相同的数据类型执行多个转换,就像在流系统...

考虑以下伪代码示例: 这段代码可能无法编译,我没有方便的编译器,而且我可能在函数指针或函数对象中使用了错误的语法作为运算符中的参数......这只是为了说明模式和行为我在追。

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) ?

不仅如此,如果我想扩展它以便我的 functorslambdas 接受多个参数怎么办...

我一直在对此进行 Google 搜索,只找到了一些资源,但没有任何具体、简单、优雅且至少具有 C++17 功能的最新资源...

如果您知道有关此主题的任何好的源材料,请告诉我!

【问题讨论】:

  • @IgorTandetnik 我知道,这只是伪代码......我没有我的编译器方便 atm......但目的是采用像对象这样的函子......或者可能是一个值和一个函子...
  • 您的所有使用示例对我来说都没有多大意义。 result 的值到底应该是多少?你添加一两个是什么? p1p2 应该扮演什么角色?
  • @Ignor 考虑像二维向量这样的对象...假设它已经填充了值...例如vec2 v2 = {3,5}...然后我希望能够做点什么比如:v2 = rotate(30) | scale(5) | translate(15); 然后它会将其旋转 30 度或弧度,将其缩放 5 个单位,然后平移 15...几乎类似于linux's pipes 的工作方式...
  • 你控制vec2的定义吗?你能给它一个赋值运算符来接受一个代表这个转换序列的expression template对象吗?
  • 那么 a) 你可能想把你的实际激励例子放在问题中,因为你现在所拥有的没有意义,并且 b) 就像我说的那样,你正在寻找的技术是称为“表达式模板”。如果您搜索它,您应该会找到一些示例。

标签: c++ operator-overloading c++17 chaining pipelining


【解决方案1】:

首先我假设你有一些看起来像这样的基础知识

#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); 相同的代码 直接。

【讨论】:

  • 不是 100% 完全符合我的要求,但是,这种技术非常有用!我也喜欢您在不需要使用函数指针的情况下指出“零开销”的方式......这里唯一的事情是,如果我有一组不同的类,我必须为每组实现这个......但是,一旦我熟悉了这种模式,复制起来应该不会那么难!
  • 一旦我有一个完全兼容的 C++20 编译器(仍在 C+17 上)编译器,我应该能够使用conceptsranges 等...让这更简单一点!
  • 您可以使用 SFINAE 将单个实现扩展到与一组约束匹配的任何类。将其扩展到返回值不同的事物也不是那么难。 myclass | classToString | stringToInteger;.
  • @FrancisCugler:另一种选择是std::bind,它的模板魔法更少,而且更灵活,但在调用点也更冗长
【解决方案2】:

对于您在评论中添加的矢量示例,您可以使用这样的语法

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

后一种语法可能比使用基本和身份运算符开始管道更直观。

【讨论】:

    【解决方案3】:

    现在我的编译器可供我使用,并且在我的项目工作了一段时间后再次使用;这是我能想到的......

    这并不完美,因为 operator()function pointer 需要特别的 2 个参数,如此示例代码中所示...

    (我可能不得不为此使用可变参数模板以及函数指针的签名,以允许函数指针、函数对象、仿函数或具有任意数量参数的 lambda 表达式不同的类型是可以接受的...)

    这是我的工作源代码...

    #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_ptrpipe_object...pipe_object只是某种类型T 的基本包装。 pipe_object 类的唯一特殊功能是它的构造函数之一……它接受pipe 对象并提取pipe 的值,并使用它构造一个新的pipe_object。由于opeator|() 是右手关联的性质,我不得不使用双指针...

    从上面的双 for 循环中的源代码中可以看到...我正在使用 pipe 的对象 operator() 将值和地址传递给函数...我可以还有pipe 这些对象而不调用它们的operator()... 从这里开始的下一步将是应用这个概念,但使其适用于任何类型的对象...除非我可以将这些类用作包装器使用管道链接技术!我的意思是让我的另一个类从这个pipe 类继承,就像使用CRTP 一样。

    让我知道你的想法!

    【讨论】:

    • 即使在阅读了您的代码之后,我仍然无法弄清楚您想要 operator| 做什么。它似乎是“使左等于右,并返回左原来的样子”。我相当肯定这可以大大简化,但是
    • @MooingDuck 我希望它评估从左到右应用的函数,并在遇到该函数时应用它......
    猜你喜欢
    • 1970-01-01
    • 2015-02-25
    • 2011-08-15
    • 1970-01-01
    • 2021-11-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多